From 7a59dc3b27e1958829d0534578553820d75f91d1 Mon Sep 17 00:00:00 2001 From: Ali Ibrahim Jr <48456829+IBJunior@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:04:34 +0200 Subject: [PATCH] ci: stage npm releases for 2FA approval npm trusted publishers only allow staged publishing unless "Allow npm publish" is ticked, so the v0.9.0 run failed with 403 OIDC permission denied. Rather than enable direct publishing, which npm recommends against, the workflow now runs npm stage publish: CI uploads the version and a maintainer approves it on npmjs.com with 2FA before it goes live. This CLI runs on users' machines via npx, so a release that a compromised workflow could ship on its own would execute immediately. AGENTS.md documents the approval step and why the publisher is stage-only. --- .github/workflows/publish.yml | 9 ++++++--- AGENTS.md | 20 +++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 16c49ad..0df5e46 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -49,7 +49,7 @@ jobs: cache: 'npm' registry-url: 'https://registry.npmjs.org' - # Trusted publishing needs npm >= 11.5.1; Node 22 ships npm 10. + # Staged publishing needs npm >= 11.15.0; Node 22 ships npm 10. - name: Upgrade npm run: npm install -g npm@latest @@ -59,5 +59,8 @@ jobs: - name: Build run: npm run build - - name: Publish to npm - run: npm publish --access public + # Staged, not direct: the version is uploaded but stays private until a + # maintainer approves it on npmjs.com with 2FA. A compromised workflow can + # stage a release, but cannot ship one on its own. + - name: Stage publish to npm + run: npm stage publish --access public diff --git a/AGENTS.md b/AGENTS.md index f283cf2..246c376 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -231,16 +231,26 @@ Releases are published by CI, not from a laptop. The flow: 2. After it merges, create a GitHub release tagged `vX.Y.Z` on `main` (`gh release create vX.Y.Z --target main --notes-file ...`). 3. `.github/workflows/publish.yml` runs on the published release, checks the tag - matches `package.json`, builds, and runs `npm publish`. + matches `package.json`, builds, and runs `npm stage publish`. +4. **Approve the staged version.** Nothing is live until a maintainer approves it + on npmjs.com (package → Staged Packages → Approve) or with + `npm stage approve ` (`npm stage list` shows the id). Approval needs + 2FA. A release isn't done until this step is. **Authentication is npm trusted publishing (OIDC), not a token.** The package's npm settings name this repo and `publish.yml` as its trusted publisher, and the workflow's `id-token: write` permission lets npm verify the run. There is no `NPM_TOKEN` to rotate — that secret expired once and silently broke a release. -Two things keep it working: - -- **npm must be ≥ 11.5.1.** Node 22 ships npm 10, so the workflow upgrades npm - before publishing. Don't remove that step. +Things that keep it working: + +- **The trusted publisher is stage-only, deliberately.** "Allow npm publish" is + left unchecked on npmjs.com, so CI can stage a version but never ship one on + its own. This CLI runs on users' machines via `npx`, so a compromised release + would execute immediately; the human approval is the guard. Switching the + workflow back to plain `npm publish` fails with `403 OIDC permission denied + for this action` — that is this setting working, not a bug. +- **npm must be ≥ 11.15.0** (staged publishing). Node 22 ships npm 10, so the + workflow upgrades npm before publishing. Don't remove that step. - **Renaming `publish.yml` breaks publishing** until the trusted publisher on npmjs.com is updated to the new filename.