From afd574dc6a26a32280e1b177ad51c0e9033b68cc Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 5 Aug 2026 09:54:19 +0000 Subject: [PATCH] ci: publish to npm by trusted publishing instead of a stored token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the NPM_TOKEN requirement. npm now trades the short-lived OIDC 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 — and none had to be minted, since the secret was never added. Two things that would have failed quietly otherwise: Node 22 bundles npm 10, which predates trusted publishing and falls back to looking for a token that no longer exists — an auth failure that reads as a credential problem rather than a version one. So npm is upgraded first, and the 11.5.1 floor is checked rather than assumed. The check compares with `sort -V`: lexically, 11.16.0 is lower than 11.5.1. `--provenance` is gone because trusted publishing generates and attaches the attestation itself. The other half of the trust lives on npmjs.com under the package's Trusted Publisher settings, and it is pinned to this file's *name*. Renaming publish.yml breaks publishing with no other symptom, which the header now says. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/publish.yml | 51 +++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6b999f3..118c362 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,8 +10,15 @@ # that failed on something transient; it is safe because a version already on # the registry is skipped rather than attempted. # -# Requires a repository secret `NPM_TOKEN` — an npm automation token, which is -# the kind that publishes without a 2FA prompt. +# 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. name: publish on: @@ -21,9 +28,9 @@ on: permissions: contents: read - # For npm provenance: the attestation is signed with a short-lived OIDC token - # rather than anything stored here, and it is what lets npm show which build - # this tarball actually came from. + # 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: @@ -44,6 +51,22 @@ jobs: cache: pnpm registry-url: https://registry.npmjs.org + # Node 22 bundles npm 10, which predates trusted publishing and would fall + # back to looking for a token that no longer exists — an auth failure that + # reads as a credential problem rather than a version one. 11.5.1 is the + # floor; the check below says so plainly if that ever regresses. + - 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 @@ -88,23 +111,11 @@ jobs: echo "already=false" >> "$GITHUB_OUTPUT" fi - # Said plainly here, rather than as the 401 npm would otherwise return - # partway through a release. - - name: Require an npm token - if: steps.published.outputs.already == 'false' - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - if [ -z "$NPM_TOKEN" ]; then - echo "::error::NPM_TOKEN is not set — add an npm automation token as a repository secret named NPM_TOKEN" - exit 1 - fi - + # No token, and no `--provenance` either: publishing through trusted + # publishing generates and attaches the attestation on its own. - name: Publish if: steps.published.outputs.already == 'false' - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public - name: Confirm the registry has it if: steps.published.outputs.already == 'false'