Skip to content
Nicolas Chiong· 4 min read

An npm trusted publishing migration playbook

Move npm package releases from long-lived tokens to OIDC trusted publishing with a small, reviewable migration path for JavaScript teams.

I would treat npm trusted publishing as a release hardening migration, not a weekend security cleanup. The feature is mature enough for normal JavaScript packages now, but the risky part is still workflow ownership: which repository, which workflow file, which environment, and which human approval path is allowed to put a tarball in the registry.

The shape of the migration

The old model is simple: put an npm token in CI, run npm publish, rotate the token when somebody remembers, and hope it never appears in logs or a forked workflow. It works until it does not.

Trusted publishing changes the release credential from a stored secret into a short-lived identity exchange. npm trusts a specific CI workload through OpenID Connect. The workflow asks its identity provider for a token, npm verifies that identity against the package's trusted publisher configuration, and the publish proceeds without an npm write token.

That is a better default because the release permission moves closer to the build that produced the package. It also means the migration should be reviewed like release infrastructure. I want the trusted publisher to point at the exact repository and workflow that produces the artifact, not a generic release workflow that every package quietly shares.

This pairs well with my existing habit of auditing dependency roles before production builds, which I wrote about in the dependency role audit for JavaScript production builds. One checks what goes into the install graph. The other checks who is allowed to ship the package graph.

Step 1: make the current release boring

Before touching npm settings, I would make the existing release path boring enough to compare against. The package should already have a deterministic build, a lockfile-backed install, and a CI job that creates the same package contents a maintainer would publish locally.

For a small package, that usually means:

  • npm ci, not an opportunistic install
  • a test or typecheck gate before publish
  • npm pack --dry-run in CI so reviewers can see what will ship
  • one release workflow file with a clear trigger
  • no publish step in pull request workflows

This is intentionally plain. Trusted publishing does not fix a sloppy artifact. It only removes a weak credential model from the release path.

Step 2: add the smallest OIDC surface

The workflow needs permission to request an OIDC token. In GitHub Actions, that is the id-token: write permission on the job that publishes. I keep it on the publish job, not at a broad workflow level, because it makes the privilege obvious in review.

permissions:
  contents: read
  id-token: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm publish

npm's documentation currently calls out npm CLI 11.5.1 or newer and Node 22.14.0 or newer for trusted publishing. I would still choose Node 24 in the workflow unless the package has a real compatibility reason not to. It is the boring path because the toolchain already includes a modern npm.

The npm side is package configuration: add the trusted publisher and bind it to the provider, repository, workflow file, and environment when used. This is the part I would screenshot or record in the release PR because it lives outside the repository.

Step 3: preserve proof of presence where it matters

A direct OIDC publish is not always the right endpoint. For packages with many consumers, I prefer npm staged publishing when the team can tolerate the extra approval step. The CI job uploads the tarball into a stage queue, then a maintainer approves it with proof of presence before it becomes installable.

That is a better fit for packages where the risk is not only token theft, but also an overpowered release workflow. The workflow can build and stage. A human still approves the final registry mutation.

The tradeoff is operational: approvals can delay patches. For a private internal package or a low-risk package with fast rollback, direct trusted publishing may be enough. For a public SDK, CLI, auth library, payment helper, or build tool, I would rather spend the extra minute.

Step 4: check provenance like a consumer

Trusted publishing from supported public CI setups can produce npm provenance automatically. That changes the maintainer checklist. After the first release, I want to verify the package the same way a consumer can verify it.

Run npm audit signatures after installing the published version from the registry. Then open the package page and confirm the provenance points back to the expected repository, workflow file, commit, and build environment.

This catches a different class of mistakes than CI logs. The release can pass, but the attestation can still be missing because the source repository is private, the runner is unsupported, or the package was published through an old token path. I do not want to discover that gap during an incident.

This is the same reason I still like runtime guardrails such as the Node.js permission model. Security controls are most useful when they are simple enough to verify from the outside.

Step 5: retire the token deliberately

The lazy mistake is to add trusted publishing and leave the old automation token alive forever. That gives the team two publish paths: the reviewed one and the forgotten one.

After one successful trusted publish, I would remove the npm token from the CI environment, revoke the old token in npm, and tighten package publishing access. npm's docs describe a stricter setting that requires two-factor authentication and disallows token publishing while keeping trusted publishers working.

Do this after the first successful OIDC release, not before. A broken release path is still a production risk.

What I would not automate yet

I would not start by bulk-applying trusted publisher settings across every package in a monorepo. npm has CLI support for managing trust relationships, but the first migration should prove the workflow shape on one package with real consumers and a clear rollback path.

Once that package ships cleanly, the repeatable part is small: same Node version, same publish job permission, same dry-run pack check, same provenance verification, and the same post-release token cleanup. The upgrade path is not a new release platform. It is deleting a long-lived secret and making the remaining publish path boring enough to review.

npmtrusted-publishingsupply-chain-securitygithub-actions

References

  1. docs.npmjs.comnpm Docs
  2. github.blogGitHub Changelog
  3. docs.github.comGitHub Docs
  4. github.blogGitHub Changelog
  5. repos.openssf.orgOpenSSF Securing Software Repositories WG

Related writing

← PreviousClaude Opus 5 is here: what developers should test before upgrading

Let's make something useful.

Start a conversation