From 3d4a22dee617429c52abf8298c06b6a65841a7e3 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 9 Nov 2026 16:14:35 -0600 Subject: [PATCH] ci(publish): stage releases on npm and approve them with 2FA The package's trusted publisher on npmjs.com only allows staged publishing, so CI's plain npm publish has been rejected since 3.49.25 (E403 OIDC permission denied for this action). Keep that restriction: CI now runs npm stage publish, and a version goes live only after a maintainer approves it with 2FA. A compromised workflow or release permission can no longer ship a build to users on its own. Drop the NPM_TOKEN fallback from the publish step; trusted publishing needs no long-lived token. --- .github/workflows/publish.yml | 34 +++++++++++++++++++++++----------- CONTRIBUTING.md | 13 +++++++++++-- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 52fad0b5..041af224 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,15 +39,27 @@ jobs: echo "already_published=false" >> "$GITHUB_OUTPUT" fi - # npm trusted publishing (OIDC): with this package's Trusted Publisher set - # to this repo + publish.yml on npmjs.com, npm >= 11.5.1 exchanges the - # job's id-token for a one-off publish credential, so no long-lived - # NPM_TOKEN is needed. The token stays below as a fallback until every - # package is switched over. - - name: Upgrade npm for trusted publishing - run: npm install -g npm@^11.5.1 - - name: Publish to npm + # npm trusted publishing (OIDC) with staged publishing. The package's + # Trusted Publisher on npmjs.com (BlockRunAI/Franklin + publish.yml) is + # allowed to *stage* only, never to publish. This job uploads the version + # in a staged state that nobody can install; it goes live only when a + # maintainer runs `npm stage approve ` (or approves on + # npmjs.com), which requires 2FA. A compromised workflow or a stolen + # release permission therefore cannot ship a Franklin build to users. + # No long-lived npm token is used: npm exchanges the job's id-token. + - name: Upgrade npm for staged trusted publishing + run: npm install -g npm@^11.20.0 + - name: Stage for publishing if: steps.check.outputs.already_published != 'true' - run: npm publish --provenance --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm stage publish --provenance --access public + { + echo "### Staged, not yet live" + echo "" + echo "Approve with 2FA to publish:" + echo "" + echo '```' + echo "npx -y npm@11 stage list @blockrun/franklin" + echo "npx -y npm@11 stage approve " + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e447c79..99e70f9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,8 +77,17 @@ and the ADRs under `docs/adr/`. Patches land on `main` and ship via `chore(release): X.Y.Z — …` commits that bump `package.json` and prepend a `CHANGELOG.md` entry. A matching narrative goes under `docs/release-notes/YYYY-MM-DD-*.md`. -The `npm publish` step is currently driven manually by the maintainer; -the GitHub Release tag is the canonical record. +Publishing a GitHub Release (tag `vX.Y.Z`) runs `.github/workflows/publish.yml`, +which **stages** the version on npm through trusted publishing. A staged +version is not installable until a maintainer approves it with 2FA: + +```bash +npx -y npm@11 stage list @blockrun/franklin +npx -y npm@11 stage approve +``` + +Check the workflow run before approving: the tag must point at the commit you +meant to ship. The GitHub Release tag is the canonical record. ## License