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
52 changes: 41 additions & 11 deletions .github/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@ 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.
3. Open a normal PR to `main`; do not open a draft PR for release prep.
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.
Expand Down Expand Up @@ -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

Expand All @@ -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 <RUN_ID> --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

Expand Down Expand Up @@ -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.
26 changes: 17 additions & 9 deletions .github/workflows/release-autotag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
branches: [main]

permissions:
actions: write
contents: write
issues: read
pull-requests: read
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -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
32 changes: 25 additions & 7 deletions .github/workflows/release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,27 +57,45 @@ 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 }}"

- 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, 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${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,
Expand Down
5 changes: 4 additions & 1 deletion scripts/release-preflight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Loading