Publish #9
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
| name: Publish | |
| # Releasing is not a thing anyone does. Merge a version bump to main and the | |
| # package ships: this workflow asks the registry whether package.json's version | |
| # already exists, and publishes it if not. | |
| # | |
| # Why that check rather than a tag trigger: a tag pushed by GITHUB_TOKEN does | |
| # not start another workflow, so "push a tag, let publish.yml notice" silently | |
| # never runs. Asking npm what is published is also idempotent — re-running this, | |
| # or pushing a tag by hand, cannot double-publish or fail confusingly. | |
| on: | |
| push: | |
| branches: [main] | |
| tags: ['v*'] | |
| # The reconciler, and the reason this is reliable. A merge made with | |
| # GITHUB_TOKEN (auto-merge, or any bot) starts no workflow, so the push | |
| # trigger above silently does not fire for exactly the merges that matter. | |
| # This repo has no auto-merge today, which is precisely why the schedule | |
| # belongs here now rather than after it is added and a release goes missing. | |
| # | |
| # The schedule asks the registry the same idempotent question on a timer: | |
| # is package.json's version published? A release missed by any means goes | |
| # out within the hour without anyone noticing it was missed. | |
| schedule: | |
| - cron: '17 * * * *' | |
| workflow_dispatch: | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| # Tagging the released commit, so a version on the registry can always be | |
| # traced back to the tree it was built from. | |
| contents: write | |
| # npm provenance: proves on the registry that this tarball was built by | |
| # this workflow from this commit. | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| # npm >= 11.5.1 is required for trusted publishing (OIDC); Node 24 ships it. | |
| node-version: '24' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Is this version already on the registry? | |
| id: check | |
| run: | | |
| name=$(node -p "require('./package.json').name") | |
| version=$(node -p "require('./package.json').version") | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| if npm view "$name@$version" version >/dev/null 2>&1; then | |
| echo "→ $name@$version is already published; nothing to do." | |
| echo "publish=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "→ $name@$version is not on the registry; releasing it." | |
| echo "publish=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - if: steps.check.outputs.publish == 'true' | |
| run: npm ci --ignore-scripts | |
| # Never publish something that would not have passed CI. | |
| - if: steps.check.outputs.publish == 'true' | |
| run: npm run verify | |
| # Bootstrap auth. Trusted publishing (OIDC) needs no token and is the | |
| # destination; until it is configured on the package, NPM_TOKEN is what | |
| # authenticates. The token expires — token-health.yml warns before it does, | |
| # rather than letting a release be the thing that discovers it. | |
| - if: steps.check.outputs.publish == 'true' | |
| run: npm publish | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Tag the released commit | |
| if: steps.check.outputs.publish == 'true' | |
| env: | |
| TAG: v${{ steps.check.outputs.version }} | |
| run: | | |
| # Tag after a successful publish, so a tag never claims a release that | |
| # did not happen. Skipped silently if it already exists. | |
| if git rev-parse "$TAG" >/dev/null 2>&1; then | |
| echo "→ tag $TAG already exists" | |
| else | |
| git tag "$TAG" | |
| git push origin "$TAG" | |
| fi |