Migrating from NPM_TOKEN to Trusted Publishing — the 5-Step Migration in a Weekend
A weekend-sized migration plan from long-lived NPM_TOKEN secrets to OIDC trusted publishing on npm. Five steps, each reversible until the last, with the rollback path if a publish fails.
Sources: S-001 S-002 S-005
A migration plan you can execute over a weekend. Five steps. Each step is reversible until the last. If anything fails, you stay on the old NPM_TOKEN flow.
TLDR
- Friday evening — audit. Run the preflight checklist on your current setup. List everything that fails.
- Saturday morning — add OIDC alongside the token. Add
permissions: id-token: write, switch toactions/setup-node@v4, setregistry-url. KeepNPM_TOKENavailable. - Saturday afternoon — register the trusted publisher on npm.
- Sunday morning — publish a pre-release. Verify provenance badge appears.
- Sunday evening — remove
NPM_TOKEN. Publish a second pre-release with the token gone.
If any step fails, stop. Roll back to the previous step’s commit. You are still publishing fine through the old path.
Why this order
The migration is non-destructive on purpose. The dangerous step is removing NPM_TOKEN — until you do that, the worst case is “OIDC silently does not engage and the legacy path keeps working.” After you remove it, the worst case is “publish breaks until you fix the OIDC config.”
Doing the registration on Saturday (step 3) before the publish (step 4) means the trusted publisher exists when OIDC fires for the first time. Otherwise npm has nothing to verify against.
Step 1: Audit (Friday, 30 minutes)
Open the preflight checklist. Paste your current package.json and .github/workflows/publish.yml. Note every fail.
Most repos that have not migrated yet will fail at minimum:
id-token: writemissing.actions/setup-node@v3instead of v4.registry-urlnot set.NPM_TOKENpresent in env.--provenancenot on the publish command.
The “no NPM_TOKEN” row will fail; that is expected at this stage.
Write the failing rows on a sticky note. They become your checklist for Saturday.
Step 2: Add OIDC alongside the token (Saturday morning, 1 hour)
In a feature branch, make these changes. Do not remove NPM_TOKEN yet.
# Before
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# After (token still present as backup)
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # still here, intentionally
NODE_AUTH_TOKEN will dominate the auth flow until step 5. The other changes are wired but not exercised. Merge to main. Do not tag a release yet.
Reversibility: if this breaks anything (e.g., the workflow syntax errors), revert the PR. You are back where you started.
Step 3: Register the trusted publisher on npm (Saturday afternoon, 10 minutes per package)
Sign in to npmjs.com. For each package:
- Open the package’s Settings → Trusted Publishers.
- Add a new trusted publisher → GitHub Actions.
- Fill in the GitHub owner, repo, workflow filename (
publish.ymlor whatever you use), and environment if applicable. - Save.
This is purely additive on the npm side. Existing NPM_TOKEN flow keeps working. The trusted publisher just sits ready.
Reversibility: delete the trusted publisher entry. No side effects.
Step 4: Publish a pre-release (Sunday morning, 30 minutes)
Bump the version to v1.2.3-rc.0 (or v0.5.0-rc.0 for v0 packages). Tag it. Push the tag.
git checkout main
npm version prerelease --preid=rc
git push --follow-tags
gh release create v1.2.3-rc.0 --notes "OIDC migration pre-release"
The workflow runs. Watch the logs. You want to see:
setup-nodereports it set the registry-url.npm publishlog mentions OIDC or trusted publisher (depending on CLI version).- The version page on npmjs.com shows a provenance badge linked to your workflow run.
If the badge appears: OIDC worked. Trusted publishing engaged. Continue to step 5.
If the badge does not appear: OIDC fell back to the legacy token. Check:
id-token: writeis at workflow or job level.registry-urlis set on setup-node.- The trusted publisher’s repo and workflow filename match exactly.
actions/setup-node@v4, not v3.
Re-run, fix, re-tag with -rc.1. Repeat until the badge appears. You are still on a pre-release — no canonical version was created.
Reversibility: the rc release stays on npm forever (unpublish within 72h if you really want), but it does not affect latest. Users only see it if they install @rc.
Step 5: Remove NPM_TOKEN (Sunday evening, 15 minutes)
In a feature branch:
- Delete the
env: NODE_AUTH_TOKENline from the workflow. - Search the repo for
NPM_TOKEN,_authToken,.npmrc— remove any references. - Open a PR. Merge.
Publish a second pre-release: v1.2.3-rc.1.
If the provenance badge appears again, OIDC is the only auth path. The migration is complete.
If the publish fails entirely, the token was load-bearing — it was masking an OIDC config bug. Revert the PR (restoring the token) and debug.
Final cleanup once the next non-rc release is out clean:
- Delete the
NPM_TOKENsecret from GitHub repo settings. - Revoke the npm token on
npmjs.com → Account → Access Tokens.
This is the only irreversible step. The previous four can each be backed out individually.
What you have after the migration
- Every publish is signed with a workflow-scoped, run-scoped, ~10-minute OIDC token.
- The provenance attestation links every release to the exact commit and workflow run.
- No long-lived secret in GitHub or in
.npmrc. - The publish flow is reproducible by reading the workflow file alone.
Concrete rollback plan
If at any point a publish fails and you need to ship urgently:
- Revert the most recent migration commit.
- The workflow is back to the previous step.
- If you are at step 5 already, restoring the
NPM_TOKENenv line gets you back to the dual-mode flow from step 2. - Publish the urgent release through the old path.
- Resume the migration with fresh eyes.
Common mistakes
- Skipping the pre-release. Going straight from
v1.0.0(token) tov1.0.1(OIDC) and discovering the bug after the canonical version is on npm. Use rc tags. - Removing the token in the same PR that adds OIDC. Two changes, two PRs. The migration is in five steps for a reason.
- Forgetting
.npmrcfiles. A repo-level.npmrcwith//registry.npmjs.org/:_authToken=survives the workflow change. Delete the line. - Not actually checking the provenance badge after step 4. The publish succeeded! … via the legacy token. Confirm the badge.
FAQ
How long does the migration take in real time?
The plan above is paced for a weekend, but the actual hands-on work is around 2 hours. Most of the time is waiting for workflow runs.
Can I do this for multiple packages at once?
Yes. The workflow changes are per repo, the trusted publisher registration is per package. If you have a monorepo, register each package’s trusted publisher pointing at the same publish.yml.
What if I publish on a schedule, not on release?
Same plan, just trigger the publish manually via workflow_dispatch for the pre-release. Adjust step 4 accordingly.
Next step
Run the preflight on your current setup to start step 1 right now. For context, see the OIDC vs NPM_TOKEN background and the failure modes you might hit at step 4.