From 3cb77fd755d0f3cea5fc1dd4a46ff10b12ff2774 Mon Sep 17 00:00:00 2001 From: fjmorant Date: Sat, 26 Sep 2026 20:39:28 +0200 Subject: [PATCH] fix(ci): declare the production environment so the token reaches the job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NPM_TOKEN was added as a secret of the production environment rather than as a repository secret. An environment's secrets are only visible to a job that declares that environment, and the publish job declared none, so secrets.NPM_TOKEN resolved to empty. The rehearsal could not catch this. npm only warns about missing authentication under --dry-run, so the manual run went green while a real release would have failed at npm publish with a 403 — which reads like a problem with the npm account rather than a secret that never arrived. Declaring the environment is the better fix than moving the secret. Environment secrets bring protection rules with them: required reviewers, wait timers, and a restriction on which branches and tags may deploy. Limiting the latter to v* makes it impossible to publish from anything but a release tag, which is stronger than the tag check this workflow does for itself. Also adds a guard for the case in general, next to the tag check and before the gate rather than after it. An empty token fails a release immediately with a message naming the cause, and warns without failing on a rehearsal, where there is nothing to authenticate against anyway. Co-Authored-By: Claude Opus 5 --- .github/workflows/publish.yml | 23 +++++++++++++++++++++++ README.md | 7 +++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ead5039..4353c94 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,6 +22,12 @@ jobs: publish: name: publish runs-on: ubuntu-latest + # NPM_TOKEN is an environment secret, and an environment's secrets are only + # visible to a job that declares it. Without this line secrets.NPM_TOKEN is + # empty and npm refuses the publish. Declaring it also means the + # environment's protection rules — reviewers, wait timers, which branches + # and tags may deploy — apply to releases. + environment: production steps: - uses: actions/checkout@v7 @@ -51,6 +57,23 @@ jobs: fi echo "publishing ${pkg}" + # A missing token otherwise surfaces as an npm 403 after the whole gate has + # run, which reads like a permissions problem with the account rather than + # a secret that is not reaching the job. + - name: Check the npm token is present + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -n "$NODE_AUTH_TOKEN" ]; then + echo "NPM_TOKEN is reaching this job" + exit 0 + fi + if [ "${{ github.event_name }}" = "release" ]; then + echo "::error::NPM_TOKEN is empty. It must be a secret of the environment this job declares, or a repository secret." + exit 1 + fi + echo "::warning::NPM_TOKEN is empty, so this rehearsal cannot check authentication." + # CI already ran these on the merge commit, but a publish cannot be undone # — npm only allows unpublishing within 72 hours, and never a republish of # the same version. Cheap insurance against releasing from a tag that was diff --git a/README.md b/README.md index bf08b34..33f8630 100644 --- a/README.md +++ b/README.md @@ -273,8 +273,11 @@ is the mistake that otherwise ships a version under the wrong release. Running passes `--dry-run`, so it can never publish; provenance is left to real releases, since a dry run has nothing to attest. -Publishing needs an `NPM_TOKEN` repository secret — an npm **automation** token, -since a classic token fails against an account that requires 2FA for publishing. +Publishing needs an `NPM_TOKEN` secret on the **`production`** environment — an +npm **automation** token, since a classic token fails against an account that +requires 2FA for publishing. The job declares that environment, so its +protection rules apply: restricting *Deployment branches and tags* to `v*` means +only a release tag can ever publish. ### Trying a local build in an app