Skip to content
workflow confidence: high

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.

Published May 19, 2026 · kw: migrate NPM_TOKEN to trusted publishing

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

  1. Friday evening — audit. Run the preflight checklist on your current setup. List everything that fails.
  2. Saturday morning — add OIDC alongside the token. Add permissions: id-token: write, switch to actions/setup-node@v4, set registry-url. Keep NPM_TOKEN available.
  3. Saturday afternoon — register the trusted publisher on npm.
  4. Sunday morning — publish a pre-release. Verify provenance badge appears.
  5. 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: write missing.
  • actions/setup-node@v3 instead of v4.
  • registry-url not set.
  • NPM_TOKEN present in env.
  • --provenance not 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:

  1. Open the package’s SettingsTrusted Publishers.
  2. Add a new trusted publisherGitHub Actions.
  3. Fill in the GitHub owner, repo, workflow filename (publish.yml or whatever you use), and environment if applicable.
  4. 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-node reports it set the registry-url.
  • npm publish log 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:

  1. id-token: write is at workflow or job level.
  2. registry-url is set on setup-node.
  3. The trusted publisher’s repo and workflow filename match exactly.
  4. 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:

  1. Delete the env: NODE_AUTH_TOKEN line from the workflow.
  2. Search the repo for NPM_TOKEN, _authToken, .npmrc — remove any references.
  3. 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_TOKEN secret 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:

  1. Revert the most recent migration commit.
  2. The workflow is back to the previous step.
  3. If you are at step 5 already, restoring the NPM_TOKEN env line gets you back to the dual-mode flow from step 2.
  4. Publish the urgent release through the old path.
  5. Resume the migration with fresh eyes.

Common mistakes

  • Skipping the pre-release. Going straight from v1.0.0 (token) to v1.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 .npmrc files. A repo-level .npmrc with //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.