Add a release workflow that stages the npm publish - #409
Draft
agarbutt-figma wants to merge 1 commit into
Draft
Conversation
Replaces the manual `npm run publish:npm` release step with a dispatchable workflow that publishes through npm trusted publishing (OIDC) and staged publishing, so no long-lived npm token is involved and a human completes the release under 2FA. The workflow only ever stages. Its trusted publisher is configured stage-only, so `npm publish` from CI is rejected by the registry; a maintainer finishes the release with `npm stage approve`, which requires a 2FA challenge and cannot be done with an OIDC token. A compromised CI credential therefore stops at the stage queue. Authorization is layered: dispatch requires write access, the staging job runs in the `npm-release` environment whose required reviewers gate it, and npm validates that environment claim on the OIDC token. This is also the first CI in this repo, so it doubles as a build and test gate on the commit being released. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
agarbutt-figma
force-pushed
the
agarbutt/npm-staged-publishing
branch
from
August 20, 2026 17:11
58550fd to
c1b958e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the first CI in this repo, in the form of a
Releaseworkflow that replaces the manualnpm run publish:npmrelease step.Instead of a maintainer running
npm publishfrom a laptop with a long-lived token, CI stages the publish through npm trusted publishing (OIDC — no token anywhere), and a maintainer completes it withnpm stage approve, which requires 2FA.Why this shape
npm stage publishuploads the tarball to a queue rather than making it installable. Approval is a separate, human, 2FA-gated action — and OIDC tokens cannot approve, by design. So if a CI credential is ever compromised, the attack stops at the stage queue instead of reaching consumers.The workflow has to live in this repo specifically:
org/repo/workflow-filename, so renamingrelease.ymlbreaks publishing.Authorization
Manual dispatch of a publish workflow needs real gating:
workflow_dispatchrequires write access — outside contributors cannot trigger itnpm-releaseenvironment, whose required reviewers gate itmain(the workflow also checks the ref)release.ymland thenpm-releaseenvironment, and npm validates the environment claim on the OIDC tokennpm publishfrom CI is rejected outrightnpm stage approverequires 2FA; OIDC tokens cannot approvepermissions: {}at the top level; each job opts into the minimum it needs. Onlytag-and-releasegetscontents: write.There is also an optional
RELEASE_DISPATCHERSrepo variable (comma-separated usernames, admin-editable, no secret required) so an unauthorized dispatch fails immediately rather than sitting in the reviewer queue. It is a convenience on top of the environment gate, not a substitute for it.What it does
main; actor check; resolves the version fromcli/package.json(asserted against the optionalversioninput).npm install,typecheck,test:ci,bundle:npmon Node 20.18.1 and 22.22.0.npm stage publish --access public --provenancebehind thenpm-releaseenvironment.vX.Y.Zand creates a draft GitHub release from the CHANGELOG section.The release is left as a draft deliberately: it isn't real until the staged npm publish is approved, and that's a separate human action.
dry_run: trueruns everything up to staging.Notes
engines.node: ">=18".undici@^7.19.1requires>=20.18.1, so 18 is already unsupported in practice and a Node 18 job would fail for pre-existing reasons. Worth a follow-up decision: bumpengines, or pinundiciback.npm run publish:npm. That script chains the README restore with;, so its exit code is the restore's, not the publish's — a failed publish would look successful. Here the steps are explicit and the restore is a separateif: always()step.workflow_callindirection breaks trusted publishing.Before this can run
Actions is not yet enabled on this repo, and the npm trusted publisher and
npm-releaseenvironment need configuring. The prerequisite checklist and the rest of the release process are tracked internally — see the companion PR. Reviewers: please rehearse withdry_run: truebefore the first real release.