Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ on:
# suppression). Doubles as the manual lever: Actions → release → Run
# workflow.
workflow_dispatch:
inputs:
# Recovery lever for the one state neither the driver nor the sweeper
# can reach: a release that was tagged but whose npm publish failed.
# release-please will not re-emit an already-tagged release, so a run
# with this input skips the driver, checks out the named tag, and
# publishes that package alone. One tag per run.
republish:
description: 'Existing tag to re-publish after a publish-side failure (e.g. byteguard-v0.2.0). Leave empty normally.'
required: false
default: ''
# Sweeper: converges anything the dispatch handoff dropped — a release PR
# merged without its follow-up run, or an open release PR whose drive
# failed. A run that finds nothing to do exits in seconds.
Expand Down Expand Up @@ -77,7 +87,7 @@ jobs:
# dropped handoff leaves behind. A ci failure fails this run before any
# merge — fix main, and the next push (or the sweeper) retries.
- name: Drive the release PR to a tested merge
if: ${{ steps.release.outputs.releases_created != 'true' }}
if: ${{ steps.release.outputs.releases_created != 'true' && inputs.republish == '' }}
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
Expand Down Expand Up @@ -139,12 +149,15 @@ jobs:

# Everything below runs only in the run that finds the merged release
# PR — the follow-up run the driver dispatched, the push of a hand
# merge, or a sweeper run catching up. github.sha in those runs is the
# release commit every tag release-please just created points at, so
# the default checkout is the tagged tree, whichever subset of packages
# released.
# merge, or a sweeper run catching up — or in a republish run. In the
# normal case github.sha is the release commit every tag release-please
# just created points at, so the default checkout is the tagged tree,
# whichever subset of packages released; a republish run checks out the
# named tag instead (an empty ref means the default).
- uses: actions/checkout@v4
if: ${{ steps.release.outputs.releases_created == 'true' }}
if: ${{ steps.release.outputs.releases_created == 'true' || inputs.republish != '' }}
with:
ref: ${{ inputs.republish }}

# No registry-url here, deliberately: setup-node's registry-url writes
# an .npmrc whose _authToken (a placeholder when NODE_AUTH_TOKEN is
Expand All @@ -153,19 +166,19 @@ jobs:
# E404. With no auth configured at all, npm falls through to trusted
# publishing.
- uses: actions/setup-node@v4
if: ${{ steps.release.outputs.releases_created == 'true' }}
if: ${{ steps.release.outputs.releases_created == 'true' || inputs.republish != '' }}
with:
node-version: 22

# Node 22 ships npm 10; OIDC exchange landed in npm 11.5.1.
- name: Use an npm that speaks OIDC
if: ${{ steps.release.outputs.releases_created == 'true' }}
if: ${{ steps.release.outputs.releases_created == 'true' || inputs.republish != '' }}
run: |
npm install -g npm@latest
npm --version

- name: Install dependencies
if: ${{ steps.release.outputs.releases_created == 'true' }}
if: ${{ steps.release.outputs.releases_created == 'true' || inputs.republish != '' }}
run: npm ci

# Build every workspace up front, in workspace order (core first):
Expand All @@ -174,16 +187,16 @@ jobs:
# plugin is released. Each publish below re-runs its own build via
# prepublishOnly; this step just guarantees the cross-package input.
- name: Build workspaces
if: ${{ steps.release.outputs.releases_created == 'true' }}
if: ${{ steps.release.outputs.releases_created == 'true' || inputs.republish != '' }}
run: npm run build

# Core before plugin, so a release that includes both never publishes a
# plugin whose dependency range names a version the registry has not
# seen yet.
- name: Publish byteguard to npm
if: ${{ steps.release.outputs['packages/byteguard--release_created'] == 'true' }}
if: ${{ steps.release.outputs['packages/byteguard--release_created'] == 'true' || startsWith(inputs.republish, 'byteguard-v') }}
run: npm publish --provenance --access public --workspace packages/byteguard

- name: Publish vite-plugin-byteguard to npm
if: ${{ steps.release.outputs['packages/vite-plugin-byteguard--release_created'] == 'true' }}
if: ${{ steps.release.outputs['packages/vite-plugin-byteguard--release_created'] == 'true' || startsWith(inputs.republish, 'vite-plugin-byteguard-v') }}
run: npm publish --provenance --access public --workspace packages/vite-plugin-byteguard
21 changes: 16 additions & 5 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,25 @@ left behind — a merged release pull request whose dispatch never fired, an
open one whose drive failed — is picked up and completed. A sweeper run with
nothing to do exits in seconds.

**The one state the sweeper cannot reach** is a release that was tagged but
whose npm publish then failed: release-please will not re-emit an
already-tagged release, so no later run retries the publish on its own. For
that there is the `republish` input on `release.yml`'s manual trigger —
**Actions → release → Run workflow →** enter the existing tag (say
`byteguard-v0.5.0`; one tag per run). That run skips the driver, checks out
the tag, and publishes just that package, authenticating over OIDC as always.
Fix whatever failed the publish first; the tag and the GitHub release need no
touch.

**What a human can still do.** Everything, just none of it is required:
dispatch `ci.yml` onto the release branch from the Actions tab; squash-merge
the release pull request by hand (the resulting push tags and publishes as
always); dispatch `release.yml` on `main` to force a sweep right now. The one
rule: do not push your own commits to the release branch — release-please
owns it and will overwrite. The branch is named by release-please from its
config, so anything scripted reads it from the pull request rather than
hard-coding it: `gh pr view <n> --json headRefName`.
always); dispatch `release.yml` on `main` to force a sweep right now, or with
`republish` set to retry a failed publish. The one rule: do not push your own
commits to the release branch — release-please owns it and will overwrite.
The branch is named by release-please from its config, so anything scripted
reads it from the pull request rather than hard-coding it:
`gh pr view <n> --json headRefName`.

**The PAT escape hatch** remains wired but unused: store a fine-grained
personal access token scoped to this repository (**Contents: read and
Expand Down
Loading