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