npm Trusted Publishing Setup for First-Time Maintainers (with the GitHub Actions YAML That Works)
A first-publish guide for npm trusted publishing (OIDC) on GitHub Actions. The exact permissions block, the registry-url that matters, and the three lines that bypass your trusted publisher silently.
Sources: S-001 S-002 S-003 S-005
You wired up an npm publish workflow, switched it from a long-lived NPM_TOKEN to OIDC trusted publishing, ran it, and the registry still asks for an auth token. This is the exact configuration that works in 2026, and the three things that quietly break it.
TLDR
To publish from GitHub Actions with npm trusted publishing:
- Configure the trusted publisher on
npmjs.comfor your package, pointing at your repo + workflow. - Add
permissions: { id-token: write, contents: read }to the publish job. - Use
actions/setup-node@v4withregistry-url: "https://registry.npmjs.org". - Run
npm publish --provenance --access public. - Remove
NPM_TOKENfrom the workflow env — its presence makes npm prefer it over OIDC.
A copy-paste workflow is at the bottom.
Why the first publish breaks for most maintainers
Trusted publishing is two systems handshaking:
- GitHub Actions mints an OIDC ID token claiming “this run is
your-org/your-repoon branchmain” — but only if you ask for it withpermissions.id-token: write(see GitHub OIDC docs for npm —S-002). - npm registry verifies that token, looks up the trusted publisher you configured at
npmjs.com → your package → Settings → Trusted Publishers, and either accepts the publish or falls back to legacy auth (npm docs —S-001).
Either side missing kills the handshake.
The recurring failure shape in npm/cli issues (S-005) is the workflow runs, the auth step “succeeds” because NPM_TOKEN is still in the env, and the package publishes — just without provenance, and without using the trusted publisher you set up.
Step-by-step fix
1. Configure the trusted publisher on npmjs.com
Sign in to https://www.npmjs.com. Open your package’s Settings → Trusted Publishers → Add a new trusted publisher. Pick GitHub Actions. Fill:
- Organization or user: your GitHub owner.
- Repository: your repo name.
- Workflow filename:
publish.yml(or whatever you named it — it must match exactly). - Environment name: optional. Leave blank unless you actually configured a GitHub Environment.
Save.
2. Set the workflow permissions
The job that publishes must grant id-token: write. Without this, GitHub never mints the OIDC token.
permissions:
id-token: write
contents: read
If you only set this at the workflow level, fine. If you set it at the job level, make sure the publish job has it.
3. Use setup-node v4 with the registry URL
actions/setup-node@v3 and earlier do not fully wire the OIDC audience for npm. Use v4.
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
The registry-url line is load-bearing. Without it, npm’s CLI does not know which audience to request the OIDC token for, and the token it gets back is rejected by the registry.
4. Publish with --provenance
- run: npm publish --provenance --access public
--provenance generates the Sigstore attestation that proves “this tarball was built by this workflow on this commit” (Sigstore docs — S-004). Without it your package shows no provenance badge on npm, even though the publish itself works. Provenance shipped in npm@9.5 (npm CLI release notes — S-003); confirm node-version resolves to an npm version at or above that.
--access public matters only if your package is scoped (@you/lib). Scoped packages default to restricted access and a missing --access public is the most common reason a first publish appears to work but no one can npm install it.
5. Remove NPM_TOKEN
If NPM_TOKEN is set as an env or secrets.NPM_TOKEN is referenced anywhere in the job, npm’s CLI uses it instead of OIDC. The publish succeeds, but the trusted publisher you configured is bypassed and there is no provenance. Delete the secret reference. If you keep NPM_TOKEN around for migration reasons, scope it to a separate job that you do not run.
The workflow that works
name: Publish to npm
on:
release:
types: [published]
jobs:
publish:
name: npm publish
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm test --if-present
- run: npm publish --provenance --access public
Save as .github/workflows/publish.yml. The filename must match what you registered as the trusted publisher on npm.
Common mistakes (sourced)
permissions: id-token: writeset at the wrong scope. If the publish step is in a reusable workflow called from another workflow, the inner one needs the permission too. (GitHub Actions OIDC docs —S-002)registry-urlmissing. Without it the OIDC audience is wrong and the token is rejected by npm. (npm docs —S-001)- Stale
package.json#repository.url. The repo URL in yourpackage.jsonmust match the GitHub repo running the workflow. If you forked and changed owner, update the URL. (npm/cli issues —S-005) - Scoped package, no
--access public. Default is restricted; a first publish completes with no error but the package is private to your org.
FAQ
Do I need to delete NPM_TOKEN from npm before this works?
No — you can leave the npm token alive on npmjs.com as a fallback, but you must remove it from the workflow env, or npm CLI will prefer it.
Does this work for pnpm or yarn?
pnpm publish speaks OIDC starting at v9.5. yarn classic (1.x) does not. yarn modern (Berry) supports it through the npm publish plumbing. If you want trusted publishing, use npm publish or pnpm publish.
What if my repo has a monorepo with multiple packages?
You configure a trusted publisher per package on npm. The workflow can publish them all in one job; npm verifies the OIDC token per package.
Next step
Run the npm Trusted Publishing Preflight on your own package.json and workflow — it flags the four most common failure shapes above. For a deeper dive on the failures, read npm publish OIDC vs NPM_TOKEN and npm publish —provenance failed: 8 reasons.