Staged npm publishing is production change control
I think we have been lying to ourselves about npm publish.
We call it a release step, which makes it sound small and administrative: tests pass, CI uploads a tarball, someone updates a changelog, and we move on. But publishing a library is closer to a production deployment than it is to uploading a build artifact. The audience just happens to be every application, every developer laptop, and every downstream CI pipeline that trusts that package name.
That is a pretty impressive blast radius for a job step that is often protected by one long-lived secret named something like NPM_TOKEN.
The good news is that npm’s publishing model has finally caught up with that reality. Trusted publishing, staged releases, malware scanning, and independent workflow identities let us build a release lane with actual controls. The important part is not ticking a supply-chain-security box. It is deciding that making a package public is a production change and operating it that way.
A registry publish is a deployment with strangers on the other side
When I deploy a service, my team normally has some guardrails: an environment, a deployment record, observable health signals, a rollback plan, and a person who can decide whether to continue. A library publish often has less ceremony despite affecting systems we do not own and cannot observe.
Once a new version is tagged latest, consumers may pull it into fresh CI runs within minutes. A compromised publish workflow can ship credential theft. A packaging mistake can break installs across a wide ecosystem. A post-install surprise can land in a build environment before the consumer has had a chance to understand what changed.
This is not a reason to turn every patch release into a meeting. It is a reason to put the decision at the correct boundary. Producing a package and making it available to the public are different actions. They should not share the same authorization by default.
The release is now a state machine
The useful mental model is:
build → stage → scan → approve → public
That sequence is a meaningful improvement over “CI has a secret, therefore CI may publish.”
In the current npm model, a trusted publisher authenticates with an OpenID Connect (OIDC) identity from its CI provider rather than presenting a reusable registry token. npm checks that identity against a configured repository, workflow, and environment. It also creates provenance automatically for supported trusted-publishing flows. npm’s trusted-publishing documentation is worth reading closely because the details are the policy: the identity is not merely “GitHub Actions,” but a particular workflow running in a particular context.
Then comes the part I like most: staging keeps the version out of the public registry until a human approves it. npm scans published packages before they are available for install; a scan can allow, hold, or block a package. Staged packages cannot be approved until that scan is complete. That means the registry itself now acknowledges that availability is a lifecycle, not an instantaneous side effect of a successful command. GitHub’s npm update describes the staged history maintainers can review, while the earlier malware-scanning announcement spells out the possible delay and review outcomes.
This is not bureaucracy for its own sake. It buys time for the two questions that matter:
- Did the intended release workflow create this exact artifact?
- Should this exact artifact become public now?
Those questions are related, but they are not the same question.
OIDC proposes; approval promotes
OIDC is excellent at constraining who may propose a release. A trusted-publishing configuration can say, in effect, “only this workflow in this repository, under this environment, may obtain publish authority.” It removes a copied secret from the pipeline and makes an unexpected publishing attempt easier to reason about.
OIDC does not decide whether the release is sensible. It cannot notice that someone selected the wrong latest tag, that a generated file is missing, or that a perfectly legitimate change needs a careful rollout. It also cannot make a human owner accountable for the public-release decision.
Staging covers that second boundary: who may make it public. It is the change-control gate.
That distinction keeps the design honest. Provenance can establish where an artifact came from; it does not prove that the code is safe or that the release is appropriate. The OpenSSF attestation UX guide makes the same caution in broader terms: attestation is evidence, not a safety verdict. Treat it as useful forensic material and a release-review input, not a green shield beside a dependency name.
An operating model that does not depend on heroic maintainers
The September npm update made multiple trusted-publishing configurations available for a package. That is more than a convenience feature. It lets a team stop pretending all release paths are equivalent.
I would set up three lanes.
Stable releases
Use a dedicated release workflow, ideally protected by the repository’s normal review and release-environment rules. It stages a version and produces a release record with the commit, version, changelog, test result, package contents, and provenance link.
Promotion to public should require a named package maintainer with 2FA. For a team, make it a deliberate approval responsibility rather than an ambient permission held by anyone who happens to be on call. The approver checks the staged package, confirms the dist-tag, and records the decision in the normal release trail.
That is not a four-eyes ritual for every typo. For small teams, one accountable maintainer may be entirely appropriate. The control is that the same compromised CI context cannot silently decide to publish broadly.
Prereleases
Use a separate trusted-publishing configuration and workflow for versions such as 2.4.0-rc.1 and tags such as next or beta. Keep its OIDC conditions distinct from the stable-release workflow.
This makes experimentation cheaper without weakening the stable lane. A prerelease can still be staged and scanned, while its distribution tag makes the intended audience explicit. More importantly, a release engineer does not have to retain a broad token simply because the prerelease workflow has slightly different triggers.
Emergency releases
An emergency path should be faster, not invisible. Use another narrowly scoped workflow identity, require a short incident note, and preserve the same staging and human-promotion boundary. A designated incident commander or package owner can approve it, but the exception must leave evidence.
Do not confuse “we need to fix this now” with “we should bypass every control.” The fastest reliable emergency process is the one people have rehearsed: publish a patched version, verify its provenance, move consumers to it, and deprecate a bad version if that is the appropriate registry response. A package version is hard to take back once consumers have cached or mirrored it, which is exactly why the release record matters.
Make the approval observable
There is a failure mode here too: teams add a human button and call it governance, while the button says nothing useful. Then the process becomes an unreviewed rubber stamp with extra latency.
An approver should be able to answer a short set of concrete questions without spelunking through CI logs:
- What commit and source repository produced this package?
- Which release workflow and environment requested the stage?
- What changed from the previous public version?
- Did the expected tests and package-content checks pass?
- Has registry scanning completed, and what is the state?
- Which tag will consumers receive?
Put that information in the release description or a small generated summary next to the approval action. If the answer depends on a maintainer remembering which run URL was the good one, the process is not yet operational.
Keep the evidence after promotion too. GitHub documents how to verify artifact attestations offline, which is useful when investigating a suspicious release or preserving evidence outside a web UI. You do not need to perform offline verification on every ordinary release; you do need a tested procedure for doing it when trust is in question. See GitHub’s offline-attestation verification guide.
A Monday-morning migration list
If your current process is npm publish from CI with a long-lived token, I would do this in order:
- Inventory publishing authority. Find every publish token, every workflow that can use one, and every person or service account that can change package settings.
- Install trusted publishing. Bind separate OIDC configurations to stable, prerelease, and emergency workflows. Keep their repository, workflow, and environment criteria narrow.
- Stage by default. npm supports staging by default per trusted-publishing configuration; make direct publishing an exceptional, explicit choice rather than the convenient default.
- Name the approvers. Decide who may promote stable versions, who covers incidents, and what evidence they must review. “Any maintainer” is an answer, but usually a lazy one.
- Exercise a bad release. Rehearse how you would stop recommending a bad version, release a fix, communicate to consumers, and collect provenance and workflow evidence.
- Verify provenance when it matters. Document the commands and access needed to verify an attestation independently during an incident.
There is some migration pressure here, and it is welcome. npm has announced that granular access tokens configured to bypass 2FA will lose the ability to publish directly around January 2027; the remaining automated use is staging, with a human promotion step. The announcement is a good reason to remove those old publish tokens before they become an outage disguised as a security migration.
Accountable beats magical
None of this makes an npm dependency safe. Malware scanning catches some things, not everything. Provenance can show that a release came from the expected workflow even when that workflow faithfully built a bad change. A human approver can still make the wrong call.
But that is not an argument for keeping a reusable secret in CI and hoping for the best. It is an argument for a release path that can answer basic questions after the fact: who proposed the package, what produced it, what checks ran, who made it public, and what happened next.
That is production change control. It happens to be good supply-chain security too.
References
- Trusted publishing for npm packages
- Multiple trusted publishing configurations for npm
- npm publish-time malware scanning and dual-use metadata
- npm install-time security and GAT bypass-2FA deprecation
- OpenSSF attestation UI/UX guide
- Verifying artifact attestations offline
To test my projects, I use Railway. If you want $20 USD to get started, use this link.