Release #38
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Runs on a v* tag push, or via workflow_dispatch ON A TAG REF (bump.yml uses the | |
| # dispatch path because pushes made with the default GITHUB_TOKEN never trigger | |
| # other workflows). Flow: test -> assert tag == package.json version -> publish to | |
| # public npm (with provenance) -> cut a GitHub Release. | |
| # | |
| # npm publish is SKIPPED with a warning — not failed — when the NPM_TOKEN secret is | |
| # missing, so the GitHub Release is still created. Add an npm "Automation" token as | |
| # the NPM_TOKEN repo secret to enable publishing. See docs/RELEASING.md. | |
| name: Release | |
| on: | |
| push: | |
| tags: ["v*"] | |
| workflow_dispatch: # dispatched by bump.yml with --ref vX.Y.Z; must target a tag | |
| # A tag can arrive twice for the same version — the push (tags: v*) AND bump.yml's | |
| # explicit `gh workflow run release.yml --ref vX.Y.Z` dispatch. Serialize per-ref so | |
| # the two never race a double publish; the loser queues (never cancels a mid-publish). | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Publish only after the FULL quality gate passes — not just `npm test` (P0-11). | |
| gate: | |
| uses: ./.github/workflows/reusable-quality-gate.yml | |
| release: | |
| needs: gate | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # create the GitHub Release | |
| id-token: write # npm provenance (supply-chain attestation) | |
| env: | |
| # secrets are not directly usable in step `if:`, so surface presence here. | |
| HAS_NPM_TOKEN: ${{ secrets.NPM_TOKEN != '' }} | |
| steps: | |
| - name: Assert this run targets a v* tag | |
| if: github.ref_type != 'tag' | |
| run: | | |
| echo "::error::Release must run on a v* tag ref (got '${{ github.ref }}'). Use the 'Bump version' workflow to cut one." | |
| exit 1 | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 # full history so release notes can diff from the last tag | |
| - uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: 22 | |
| registry-url: "https://registry.npmjs.org" | |
| cache: npm | |
| - run: npm ci | |
| - name: Assert tag matches package.json version | |
| env: | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| PKG="v$(node -p "require('./package.json').version")" | |
| if [ "$TAG" != "$PKG" ]; then | |
| echo "::error::Tag $TAG does not match package.json version $PKG. Re-run the 'Bump version' workflow instead of tagging by hand." | |
| exit 1 | |
| fi | |
| - name: Publish to npm (with provenance) | |
| if: env.HAS_NPM_TOKEN == 'true' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # Idempotent: skip when the version is already on the registry, so re-running a | |
| # wedged release (or the serialized second tag event) never dies on "cannot | |
| # publish over the previously published version". | |
| run: | | |
| PKG=$(node -p "require('./package.json').name") | |
| VER=$(node -p "require('./package.json').version") | |
| if npm view "$PKG@$VER" version >/dev/null 2>&1; then | |
| echo "::notice::$PKG@$VER is already on npm — skipping publish (idempotent re-run)." | |
| else | |
| npm publish --provenance --access public | |
| fi | |
| - name: Skip npm publish (NPM_TOKEN not set) | |
| if: env.HAS_NPM_TOKEN != 'true' | |
| run: | | |
| echo "::warning::NPM_TOKEN secret is not set — SKIPPING npm publish. The GitHub Release is still created. To publish, add an npm Automation token as the NPM_TOKEN repo secret (Settings -> Secrets and variables -> Actions) and re-run this workflow. See docs/RELEASING.md." | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.ref_name }} | |
| # --generate-notes builds notes from merged PRs/commits since the previous tag, | |
| # so it works with lightweight OR annotated tags (unlike --notes-from-tag). | |
| # Idempotent: skip if the Release already exists (re-run recovery). | |
| run: | | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "::notice::GitHub Release $TAG already exists — skipping creation." | |
| else | |
| gh release create "$TAG" --title "$TAG" --generate-notes --verify-tag | |
| fi | |
| - name: Verify the release loop closed (fail loudly on a wedge) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ github.ref_name }} | |
| # H1 closed-loop check: a tag must produce BOTH a GitHub Release and (when | |
| # publishing is enabled) an npm version. Historically release.yml could wedge | |
| # after the tag landed and nothing alerted — orphan tags v0.22.2/v0.23.2/v0.24.0. | |
| # This step turns that silent wedge into a red, notified job failure; re-running | |
| # the workflow (steps above are idempotent) then completes whatever is missing. | |
| run: | | |
| fail=0 | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "GitHub Release $TAG: present." | |
| else | |
| echo "::error::No GitHub Release exists for $TAG after the release job." | |
| fail=1 | |
| fi | |
| if [ "$HAS_NPM_TOKEN" = "true" ]; then | |
| PKG=$(node -p "require('./package.json').name") | |
| VER=$(node -p "require('./package.json').version") | |
| ok=0 | |
| for _ in 1 2 3 4 5 6; do | |
| if npm view "$PKG@$VER" version >/dev/null 2>&1; then ok=1; break; fi | |
| sleep 10 # let the registry reflect a just-published version | |
| done | |
| if [ "$ok" = "1" ]; then | |
| echo "npm $PKG@$VER: present." | |
| else | |
| echo "::error::npm does not serve $PKG@$VER after publish (registry wedge?)." | |
| fail=1 | |
| fi | |
| else | |
| echo "::warning::NPM_TOKEN not set — verified the GitHub Release only, not npm." | |
| fi | |
| if [ "$fail" != "0" ]; then | |
| echo "::error::Release loop did NOT close for $TAG — do not treat it as shipped. Re-run this workflow to complete the missing artifact." | |
| exit 1 | |
| fi | |
| echo "Release loop verified for $TAG." |