publish #8
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
| # Publish the release to npm. | |
| # | |
| # moshcode's own install path is `curl … install.sh | sh`, and npm is a second | |
| # channel rather than the primary one — which is exactly why it needs to be | |
| # automatic. A channel that only updates when someone remembers is a channel | |
| # that silently serves an old version forever. | |
| # | |
| # Runs on a published GitHub release, so the release itself is the trigger and | |
| # there is no separate step to forget. `workflow_dispatch` is for re-running one | |
| # that failed on something transient; it is safe because a version already on | |
| # the registry is skipped rather than attempted. | |
| # | |
| # Authenticates by trusted publishing (OIDC) rather than a stored token: npm | |
| # trades the short-lived token GitHub mints for this specific workflow run for | |
| # permission to publish, so there is no long-lived credential in the repository | |
| # to leak, rotate or forget. | |
| # | |
| # The other half of that trust lives on npmjs.com, under the package's Trusted | |
| # Publisher settings, and it is pinned to the *filename* of this workflow. | |
| # Renaming this file silently breaks publishing — npm will refuse the exchange | |
| # because the run no longer matches what was configured. | |
| # | |
| # When it goes wrong the error names neither OIDC nor trusted publishing: | |
| # | |
| # npm error code E404 | |
| # npm error 404 Not Found - PUT https://registry.npmjs.org/moshcode | |
| # npm error 404 ... could not be found or you do not have permission | |
| # | |
| # That is an unauthenticated PUT, reported as though the package did not exist. | |
| # It has two quite different causes, and it took v0.24.3 to tell them apart: | |
| # | |
| # 1. anything that leaves an empty auth token in an .npmrc, which stops npm | |
| # attempting the exchange at all — see the setup-node note below; or | |
| # 2. no trusted publisher registered for this package, or one registered | |
| # against a different workflow filename. | |
| # | |
| # Check 1 first. It is in this file, and it is the one that looks like 2. | |
| name: publish | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| # The whole basis of the exchange: this is what lets the run mint the OIDC | |
| # token npm authenticates against. Without it there is no credential at all | |
| # and publishing fails outright. | |
| id-token: write | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # pnpm version is read from the "packageManager" field in package.json. | |
| # Do not pin a version here — it conflicts with packageManager and fails | |
| # with ERR_PNPM_BAD_PM_VERSION. | |
| - uses: pnpm/action-setup@v4 | |
| # Deliberately no `registry-url`. With it, setup-node always writes | |
| # | |
| # //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} | |
| # | |
| # into an .npmrc — correct for token auth, and quietly fatal here. Under | |
| # trusted publishing there is no NODE_AUTH_TOKEN, so that line resolves to | |
| # an empty token, and npm stops before ever attempting the OIDC exchange: | |
| # it believes it already has credentials. The registry then answers the | |
| # unauthenticated PUT with E404, which names nothing to do with OIDC and | |
| # is what sent us looking at the npmjs.com config instead of at this file. | |
| # | |
| # actions/setup-node#1551. Without registry-url no .npmrc is written and | |
| # npm defaults to registry.npmjs.org anyway, which is where we publish. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| # Node 24 bundles npm 11, but pin the floor anyway rather than depend on | |
| # what a runner image happens to ship: below 11.5.1 there is no trusted | |
| # publishing, and the failure would again look like a credential problem. | |
| - name: Install an npm that understands trusted publishing | |
| run: | | |
| npm install -g npm@latest | |
| VERSION="$(npm --version)" | |
| MINIMUM=11.5.1 | |
| echo "npm $VERSION" | |
| # Lowest of the two must be the minimum, or this npm is older than it. | |
| if [ "$(printf '%s\n%s\n' "$MINIMUM" "$VERSION" | sort -V | head -n1)" != "$MINIMUM" ]; then | |
| echo "::error::npm $VERSION cannot use trusted publishing — $MINIMUM or later is required" | |
| exit 1 | |
| fi | |
| - run: pnpm install --frozen-lockfile | |
| # Publishing is the one action here that cannot be taken back — npm will | |
| # not let a version be replaced — so the tests run first, on the exact | |
| # tree about to be packed. | |
| - run: pnpm run --if-present test | |
| env: | |
| CI: true | |
| - name: Read the version being published | |
| id: version | |
| run: | | |
| VERSION="$(node -p "require('./package.json').version")" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "moshcode@$VERSION" | |
| # A release tagged v0.25.0 carrying package.json 0.24.0 would publish the | |
| # wrong tree under a version nobody can reuse. Cheap to check, impossible | |
| # to undo. | |
| - name: Check the tag matches package.json | |
| if: github.event_name == 'release' | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| if [ "$TAG" != "v$VERSION" ]; then | |
| echo "::error::release tag $TAG does not match package.json version $VERSION" | |
| exit 1 | |
| fi | |
| # Makes a re-run harmless. Without it, dispatching the workflow twice | |
| # fails the second time on an E403 that reads like something broke. | |
| - name: Skip if this version is already on npm | |
| id: published | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| if npm view "moshcode@$VERSION" version >/dev/null 2>&1; then | |
| echo "already=true" >> "$GITHUB_OUTPUT" | |
| echo "moshcode@$VERSION is already published — nothing to do" | |
| else | |
| echo "already=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # No token. `--provenance` is passed even though npm documents it as | |
| # automatic under trusted publishing: reports differ on whether it really | |
| # is, and asking for it explicitly costs nothing and cannot produce a | |
| # weaker result. v0.24.2 published with an attestation using this flag. | |
| - name: Publish | |
| if: steps.published.outputs.already == 'false' | |
| run: npm publish --access public --provenance | |
| - name: Confirm the registry has it | |
| if: steps.published.outputs.already == 'false' | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| # The registry is read-through cached, so a fresh publish can 404 for | |
| # a moment. Retry rather than report a good publish as a failure. | |
| for _ in 1 2 3 4 5; do | |
| if [ "$(npm view "moshcode@$VERSION" version 2>/dev/null)" = "$VERSION" ]; then | |
| echo "moshcode@$VERSION is on the registry" | |
| exit 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "::error::published, but the registry does not report moshcode@$VERSION yet" | |
| exit 1 |