From 70728d7d3fe62db66edfd52fc78ca9f68d2441c8 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 25 Jun 2026 10:56:39 -0700 Subject: [PATCH 1/2] release: require manual registry publication --- .github/RELEASE.md | 52 +++++++++++++++++++++------ .github/workflows/release-autotag.yml | 26 +++++++++----- .github/workflows/release-pr.yml | 10 +++--- scripts/release-preflight.sh | 5 ++- 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/.github/RELEASE.md b/.github/RELEASE.md index b07790ae..4f8ec3bb 100644 --- a/.github/RELEASE.md +++ b/.github/RELEASE.md @@ -6,8 +6,8 @@ End-user learning material belongs in `README.md`, `ARCHITECTURE.md`, ## Release shape -Releases use a branch -> PR -> merge -> automated tag -> release workflow -sequence. +Releases use a branch -> PR -> merge -> automated tag -> manual registry +publish sequence. 1. Create a release-prep branch named `release/vX.Y.Z`. 2. Prepare release content on that branch. @@ -15,8 +15,10 @@ sequence. 4. Merge the PR after review and green CI. 5. The `Release Autotag` workflow sees the merged `release/*` branch on `main`, runs final release preflight, creates the `vX.Y.Z` tag at the merge - commit, and dispatches the `Release` workflow for that tag. -6. The `Release` workflow verifies publishability, publishes npm and JSR, and + commit, and prints the manual publish command. +6. A maintainer whose GitHub account is a JSR `@git-stunts` scope member + manually dispatches the `Release` workflow for that tag. +7. The `Release` workflow verifies publishability, publishes npm and JSR, and creates or updates the GitHub Release. The tag must point at the exact `main` commit that passed the release prep PR. @@ -72,7 +74,7 @@ npm run release:prep CI also runs release-prep validation on PRs. The PR preview comment reports the package version and npm dist-tag that will be used if the release branch merges -and the autotag workflow runs. +and the autotag workflow creates the tag. ## Automatic tagging @@ -87,13 +89,40 @@ The workflow: - skips if the tag already exists; - runs `npm run release:preflight` from the aligned `main` commit; - creates an annotated tag at that commit; -- dispatches `release.yml` with the tag as input. +- prints the manual `release.yml` dispatch command. GitHub does not reliably start another workflow from a tag push made with the -default `GITHUB_TOKEN`, so autotag dispatches the release workflow explicitly. -That dispatched workflow uses the existing-tag recovery posture, but the live -issue gates and exact-`main` checks have already run in autotag immediately -before tag creation. +default `GITHUB_TOKEN`. JSR also treats workflows dispatched by +`github-actions[bot]` differently from workflows dispatched by a GitHub user who +is a JSR scope member. For now, autotag stops after tag creation. A maintainer +dispatches the release workflow manually so JSR OIDC publishing runs under a +scope-member actor. + +## Manual registry publication + +After autotag creates `vX.Y.Z`, a maintainer whose GitHub account is a JSR +`@git-stunts` scope member must dispatch the release workflow: + +```bash +gh workflow run release.yml --ref main -f tag=vX.Y.Z +``` + +Watch the run: + +```bash +gh run list --workflow release.yml --limit 5 +gh run watch --exit-status --interval 30 +``` + +Verify both registries: + +```bash +npm view @git-stunts/git-warp@X.Y.Z version --registry=https://registry.npmjs.org +npm view @jsr/git-stunts__git-warp@X.Y.Z version --registry=https://npm.jsr.io +``` + +Do not move the tag if either registry fails. Fix the registry-specific problem +and rerun the release workflow or the failed job. ## Release gates @@ -127,4 +156,5 @@ git push origin vX.Y.Z ``` If one registry publish fails after the tag exists, rerun the `Release` -workflow manually with the existing tag. Do not move the tag. +workflow manually with the existing tag from a maintainer account that satisfies +registry identity requirements. Do not move the tag. diff --git a/.github/workflows/release-autotag.yml b/.github/workflows/release-autotag.yml index 2e3141d7..3d205d77 100644 --- a/.github/workflows/release-autotag.yml +++ b/.github/workflows/release-autotag.yml @@ -5,7 +5,6 @@ on: branches: [main] permissions: - actions: write contents: write issues: read pull-requests: read @@ -77,9 +76,11 @@ jobs: TAG_EXISTS=false fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" + { + echo "version=$VERSION" + echo "tag=$TAG" + echo "tag_exists=$TAG_EXISTS" + } >> "$GITHUB_OUTPUT" - name: Lychee link check if: steps.release_pr.outputs.should_release == 'true' && steps.metadata.outputs.tag_exists != 'true' @@ -121,16 +122,23 @@ jobs: -f ref="refs/tags/$TAG" \ -f sha="$TAG_OBJECT_SHA" >/dev/null - - name: Dispatch release workflow + - name: Manual release dispatch summary if: steps.release_pr.outputs.should_release == 'true' && steps.metadata.outputs.tag_exists != 'true' env: - GH_TOKEN: ${{ github.token }} TAG: ${{ steps.metadata.outputs.tag }} shell: bash run: | set -euo pipefail - gh workflow run release.yml --ref main -f tag="$TAG" - echo "Tagged $TAG and dispatched release.yml." >> "$GITHUB_STEP_SUMMARY" + { + echo "Tagged $TAG." + echo "" + echo "Registry publication is a manual maintainer step for now." + echo "Dispatch release.yml from a GitHub account that is a member of the JSR @git-stunts scope:" + echo "" + echo '```bash' + echo "gh workflow run release.yml --ref main -f tag=$TAG" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" - name: Skip summary if: steps.release_pr.outputs.should_release != 'true' || steps.metadata.outputs.tag_exists == 'true' @@ -142,5 +150,5 @@ jobs: if [ "$SHOULD_RELEASE" != "true" ]; then echo "No release branch merge detected." >> "$GITHUB_STEP_SUMMARY" else - echo "Tag $TAG already exists; no release dispatched." >> "$GITHUB_STEP_SUMMARY" + echo "Tag $TAG already exists; no tag created." >> "$GITHUB_STEP_SUMMARY" fi diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index f9454218..404aaeb2 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -57,9 +57,11 @@ jobs: if [[ "$VERSION" =~ -beta\. ]]; then DIST=beta; fi if [[ "$VERSION" =~ -alpha\. ]]; then DIST=alpha; fi fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "dist=$DIST" >> "$GITHUB_OUTPUT" - echo "pre=$PRE" >> "$GITHUB_OUTPUT" + { + echo "version=$VERSION" + echo "dist=$DIST" + echo "pre=$PRE" + } >> "$GITHUB_OUTPUT" - name: Release prep guard run: npm run release:guard -- --stage prep-pr --tag "v${{ steps.predict.outputs.version }}" @@ -77,7 +79,7 @@ jobs: "- npm pack dry-run: passed", "- jsr publish dry-run: passed", "", - "If this PR is from a `release/*` branch and merges to `main`, Release Autotag will run final preflight, create `v${{ steps.predict.outputs.version }}`, and dispatch the Release workflow." + "If this PR is from a `release/*` branch and merges to `main`, Release Autotag will run final preflight and create `v${{ steps.predict.outputs.version }}`. A maintainer who is a JSR `@git-stunts` scope member must then dispatch the Release workflow manually." ].join("\n"); github.rest.issues.createComment({ ...context.repo, diff --git a/scripts/release-preflight.sh b/scripts/release-preflight.sh index f25c2a13..4ce271d9 100755 --- a/scripts/release-preflight.sh +++ b/scripts/release-preflight.sh @@ -207,9 +207,12 @@ if [ "$EXIT" -eq 0 ]; then if [ "$STAGE" = "prep-pr" ]; then echo "Ready to push the release-prep branch and open a PR." else - echo "Ready for Release Autotag to create and publish:" + echo "Ready for Release Autotag to create:" echo " v${PKG}" echo "" + echo "After the tag exists, manually dispatch registry publication as a JSR @git-stunts scope member:" + echo " gh workflow run release.yml --ref main -f tag=v${PKG}" + echo "" echo "Manual fallback, if autotag cannot run:" echo " git tag -a v${PKG} -m 'release: v${PKG}'" echo " git push origin v${PKG}" From d16ce1860210c491d22e3bc68f7e49c4a1a603f7 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 25 Jun 2026 11:08:04 -0700 Subject: [PATCH 2/2] Fix release preview output handling --- .github/workflows/release-pr.yml | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 404aaeb2..5c9c61fa 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -68,18 +68,34 @@ jobs: - name: PR comment (release preview) uses: actions/github-script@v8 + env: + RELEASE_DIST: ${{ steps.predict.outputs.dist }} + RELEASE_PRE: ${{ steps.predict.outputs.pre }} + RELEASE_VERSION: ${{ steps.predict.outputs.version }} with: script: | + const requireEnv = (name) => { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} is required`); + } + return value; + }; + + const dist = requireEnv("RELEASE_DIST"); + const pre = requireEnv("RELEASE_PRE"); + const version = requireEnv("RELEASE_VERSION"); + const body = [ "## Release Preflight", "", - `- package version: \`${{ steps.predict.outputs.version }}\``, - `- prerelease: \`${{ steps.predict.outputs.pre }}\``, - `- npm dist-tag on release: \`${{ steps.predict.outputs.dist }}\``, + `- package version: \`${version}\``, + `- prerelease: \`${pre}\``, + `- npm dist-tag on release: \`${dist}\``, "- npm pack dry-run: passed", "- jsr publish dry-run: passed", "", - "If this PR is from a `release/*` branch and merges to `main`, Release Autotag will run final preflight and create `v${{ steps.predict.outputs.version }}`. A maintainer who is a JSR `@git-stunts` scope member must then dispatch the Release workflow manually." + `If this PR is from a \`release/*\` branch and merges to \`main\`, Release Autotag will run final preflight and create \`v${version}\`. A maintainer who is a JSR \`@git-stunts\` scope member must then dispatch the Release workflow manually.` ].join("\n"); github.rest.issues.createComment({ ...context.repo,