11name : Release
22
3- # Gated on CI. `needs:` only works inside a single workflow, so this waits for
4- # the CI run on main to finish and refuses to publish unless it went green.
3+ # Releases are cut by pushing a tag, never automatically on a merge:
4+ #
5+ # 1. bump "version" in package.json, update CHANGELOG.md, commit, push
6+ # 2. git tag -a v1.2.3 -m "Release v1.2.3" && git push origin v1.2.3
7+ #
8+ # The tag is the trigger and the version in package.json is the source of truth;
9+ # the two must agree or this workflow refuses to run.
510on :
6- workflow_run :
7- workflows : ['CI']
8- types :
9- - completed
10- branches :
11- - main
11+ push :
12+ tags :
13+ - ' v*'
14+ # Manual re-run, for finishing a release that partly failed.
15+ workflow_dispatch :
1216
1317permissions :
1418 contents : write
1519 issues : write
16- pull-requests : write
1720 id-token : write
1821
1922jobs :
2023 release :
21- if : github.event.workflow_run.conclusion == 'success'
2224 runs-on : ubuntu-latest
2325 steps :
2426 - uses : actions/checkout@v4
2527 with :
26- # Full history and tags: the guard below needs to see existing tags,
27- # and the changelog is generated from commit history.
28+ # Full history: release notes are generated from the commit range.
2829 fetch-depth : 0
29- ref : main
3030
3131 - uses : pnpm/action-setup@v4
3232 - uses : actions/setup-node@v4
@@ -36,37 +36,53 @@ jobs:
3636 cache : ' pnpm'
3737 - run : pnpm install --frozen-lockfile
3838
39- # This workflow fires on every successful CI run on main, but the version
40- # only changes when someone bumps it. Without this guard every later merge
41- # would try to republish the same version and fail.
42- - name : Has this version already been released?
39+ # A tag that disagrees with package.json would publish a version nobody
40+ # asked for, so fail before anything reaches the registry.
41+ - name : Check the tag matches package.json
4342 id : check
4443 run : |
44+ set -euo pipefail
45+ NAME="$(node -p "require('./package.json').name")"
4546 VERSION="$(node -p "require('./package.json').version")"
4647 echo "version=$VERSION" >> "$GITHUB_OUTPUT"
47- if git rev-parse "v$VERSION" >/dev/null 2>&1; then
48- echo "released=true" >> "$GITHUB_OUTPUT"
49- echo "v$VERSION is already tagged — nothing to release."
48+
49+ if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
50+ TAG_VERSION="${GITHUB_REF_NAME#v}"
51+ if [ "$TAG_VERSION" != "$VERSION" ]; then
52+ echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $VERSION."
53+ exit 1
54+ fi
55+ echo "Tag $GITHUB_REF_NAME matches package.json."
56+ else
57+ echo "Manual run — releasing version $VERSION from package.json."
58+ fi
59+
60+ # Re-running a tag must not fail on a publish that already succeeded.
61+ if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
62+ echo "published=true" >> "$GITHUB_OUTPUT"
63+ echo "$NAME@$VERSION is already on npm — the publish step will be skipped."
5064 else
51- echo "released=false" >> "$GITHUB_OUTPUT"
52- echo "v$VERSION is not tagged yet — releasing."
65+ echo "published=false" >> "$GITHUB_OUTPUT"
5366 fi
5467
5568 - name : Configure git identity
56- if : steps.check.outputs.released == 'false'
5769 run : |
5870 git config --global user.name "github-actions[bot]"
5971 git config --global user.email "github-actions[bot]@users.noreply.github.com"
6072
61- # `--no-increment` publishes exactly the version in package.json rather
62- # than deriving one from commit messages, so the released version is
63- # whatever the committed manifest says and nothing else.
73+ # release-it neither commits nor tags here — the tag already exists and is
74+ # what triggered this run. It publishes and creates the GitHub release.
6475 #
65- # `prepublishOnly` re- runs lint, typecheck, tests and build before the
66- # tarball is uploaded, so a broken tree cannot reach npm.
76+ # `prepublishOnly` runs lint, typecheck, tests and build before the tarball
77+ # is uploaded, so a broken tree cannot reach npm.
6778 - name : Release v${{ steps.check.outputs.version }}
68- if : steps.check.outputs.released == 'false'
69- run : pnpm release-it --ci --no-increment
79+ run : |
80+ set -euo pipefail
81+ FLAGS="--ci --no-increment"
82+ if [ "${{ steps.check.outputs.published }}" = "true" ]; then
83+ FLAGS="$FLAGS --no-npm.publish"
84+ fi
85+ pnpm release-it $FLAGS
7086 env :
7187 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
7288 NODE_AUTH_TOKEN : ${{ secrets.NPM_TOKEN }}
0 commit comments