Skip to content

Add a release workflow that stages the npm publish - #409

Draft
agarbutt-figma wants to merge 1 commit into
mainfrom
agarbutt/npm-staged-publishing
Draft

Add a release workflow that stages the npm publish#409
agarbutt-figma wants to merge 1 commit into
mainfrom
agarbutt/npm-staged-publishing

Conversation

@agarbutt-figma

@agarbutt-figma agarbutt-figma commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Adds the first CI in this repo, in the form of a Release workflow that replaces the manual npm run publish:npm release step.

Instead of a maintainer running npm publish from a laptop with a long-lived token, CI stages the publish through npm trusted publishing (OIDC — no token anywhere), and a maintainer completes it with npm stage approve, which requires 2FA.

Why this shape

npm stage publish uploads 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:

Authorization

Manual dispatch of a publish workflow needs real gating:

Control Enforced by
workflow_dispatch requires write access — outside contributors cannot trigger it GitHub
The staging job runs in the npm-release environment, whose required reviewers gate it GitHub
That environment's deployment branches are limited to main (the workflow also checks the ref) GitHub
The trusted publisher is pinned to this repo, release.yml and the npm-release environment, and npm validates the environment claim on the OIDC token npm
The publisher is stage-onlynpm publish from CI is rejected outright npm
Package publishing access requires 2FA and disallows tokens, so a leaked classic token cannot route around the above npm
npm stage approve requires 2FA; OIDC tokens cannot approve npm

permissions: {} at the top level; each job opts into the minimum it needs. Only tag-and-release gets contents: write.

There is also an optional RELEASE_DISPATCHERS repo 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

  1. authorize — ref must be main; actor check; resolves the version from cli/package.json (asserted against the optional version input).
  2. verify — version is not already on npm; the CHANGELOG has a non-empty section for it.
  3. testnpm install, typecheck, test:ci, bundle:npm on Node 20.18.1 and 22.22.0.
  4. stagenpm stage publish --access public --provenance behind the npm-release environment.
  5. tag-and-release — pushes vX.Y.Z and 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: true runs everything up to staging.

Notes

  • Node 18 is not tested, despite engines.node: ">=18". undici@^7.19.1 requires >=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: bump engines, or pin undici back.
  • The staging step is deliberately not routed through 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 separate if: always() step.
  • No reusable workflows. npm validates the calling workflow's filename, so workflow_call indirection breaks trusted publishing.

Before this can run

Actions is not yet enabled on this repo, and the npm trusted publisher and npm-release environment need configuring. The prerequisite checklist and the rest of the release process are tracked internally — see the companion PR. Reviewers: please rehearse with dry_run: true before the first real release.

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
agarbutt-figma force-pushed the agarbutt/npm-staged-publishing branch from 58550fd to c1b958e Compare August 20, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant