
Jurij Tokarski
· Updated
npm Trusted Publishing from GitHub Actions
Run npm publish from GitHub Actions over OIDC, with no npm token to store. Miss one requirement and npm returns a misleading 404.
October 2026 update. The 404 fix below still holds: the latest Node 22 still bundles npm 10, and Node 24 bundles npm 11. What changed is the setup. Since September, a new trusted publisher can only run npm stage publish until you also allow npm publish. Since October, it expires unless its first publish succeeds within 2 days.
npm is also targeting January 2027 to end direct publishing with a token. I updated the steps, and the workflow below now follows npm's and setup-node's current examples. My own repo still runs the April version. I also corrected what I had wrong, including token expiry, calling npm's requirement undocumented, and what the Rekor entry proved.
I shipped a new release of an npm package last week — markdown-repository, a Firestore-style query builder for markdown files. The code worked. The tests passed. The release pipeline took longer to get right than the package itself.
npm Publish with a Token
The standard npm publishing workflow uses a long-lived access token. You generate it on npmjs.com, store it as a GitHub Actions secret, and reference it in your workflow:
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
It works, but the token has write access to your packages and sits in your CI secrets. If it leaks — through a copied workflow file or a careless log — anyone can publish under your name.
npm's granular tokens improved this slightly. You can scope them to specific packages, and a token with write access lasts 90 days at most. But you still have to rotate them manually. Classic tokens were revoked in December 2025, so granular tokens are the only kind left.
npm now plans to close that route. A token set to bypass 2FA can still publish directly today, but npm is targeting January 2027 to remove direct publishing with a token. Stage-only tokens are the bridge npm offers: CI runs npm stage publish, and a maintainer approves each version with 2FA.
Trusted Publishing
npm has supported trusted publishing with OIDC since July 2025. Instead of a stored token, your GitHub Actions workflow proves its identity to npm using a short-lived OpenID Connect credential. npm verifies the credential against the workflow you've authorized, and accepts the publish.
No token to store. No token to rotate. No token to leak.
Trusted publishing also works from GitLab CI/CD and, since April 2026, CircleCI. All three need hosted runners: npm doesn't support self-hosted ones yet.
First Publish Is Manual
Before you can configure trusted publishing, the package must already exist on the registry. npm has no "pending publisher" feature — you can't set up OIDC for a package that doesn't exist yet.
For the very first version, publish from your machine:
npm login
npm publish --access public
My package dates from 2022, so this rule never bit me. It will for a new package. Once the package exists on npmjs.com, go to its settings and add a trusted publisher. From that point, the workflow handles everything.
There is now a second route for that first version. npm stage publish (npm 11.15.0 or later) can create a new package with a local login or a granular token, and the version waits until a maintainer approves it with 2FA. Either way, a person signs off the first release. One catch: a new trusted publisher expires unless its first publish succeeds within 2 days, so add it when a release is ready to run.
Setting Up the Workflow
The setup has two parts.
On npmjs.com: go to your package settings, add a trusted publisher. Specify the GitHub org/user, repository, workflow filename, and optionally an environment name. A new trusted publisher can only run npm stage publish by default, so turn on npm publish under its allowed actions. npm trust github with --allow-publish does the same from the terminal, on npm 11.15.0 or later.
In the workflow: add id-token: write permission, and if you set an environment on npm, the same environment on the job.
name: Release
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
environment: release # only if you set this environment on npm
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.x
registry-url: https://registry.npmjs.org
package-manager-cache: false
- run: npm ci
- run: npm test
- run: npm run build
- run: npm publish --provenance --access public
Provenance attestation is automatic with trusted publishing when both the repository and the package are public. The --provenance flag is redundant but makes the intent explicit. Keep caching off in a publish workflow. setup-node's docs warn that a poisoned cache can expose credentials there, the OIDC token included.
The Misleading 404
My first three releases failed with this error:
npm error 404 Not Found - PUT https://registry.npmjs.org/markdown-repository
npm error 404 'markdown-repository@1.1.0' is not in this registry.
The package existed. The version was correct. Provenance signing worked — I could see the signed provenance statement in Rekor's transparency log. Everything worked except the actual publish.
That Rekor entry proved less than I thought. npm 10 asks GitHub for an OIDC token only to sign provenance with Sigstore. It never asks for the token npm's registry accepts, so it never trades one for publish rights.
The problem: Node 22 ships with npm 10.x. Trusted publishing requires npm 11.5.1 or later.
npm's documentation mentions this requirement. The error message doesn't. A 404 on PUT looks like a registry problem or a package name conflict. Nothing points you toward an npm version mismatch.
The Fix
Use Node 24.x in your workflow. On GitHub Actions, node-version: 24.x resolves to a recent patch that includes npm 11.5.1+ — markdown-repository publishes this way without an explicit npm upgrade. Every Node 24 from 24.5.0 on bundles npm 11.5.1 or later. Every Node 26 release bundles npm 11.12.1 or later, so 26.x meets the requirement too. Node 26 is due to become LTS on October 28, 2026.
- uses: actions/setup-node@v7
with:
node-version: 24.x
The other route is to stay on Node 22 and upgrade npm in the workflow. It's the one route I didn't get to work.
My run on Node 22 with npm install -g npm@latest failed after 9 seconds, and its logs have since expired. A likely cause, unconfirmed: on Node 22.22.2, the latest Node 22 at the time, npm fails to upgrade itself with Cannot find module 'promise-retry' (npm/cli#9151). If you try this route, avoid 22.22.2 and pin npm 11:
- run: npm install -g npm@11
Pin the major version. npm@latest is now npm 12, which won't install on Node 20 or on Node 22 below 22.22.2. Where it does install, it blocks dependencies' install scripts unless your package's allowScripts policy allows them. npm's docs also name a Node floor for trusted publishing: 22.14.0 or later.
With npm 11.5.1+, the same workflow publishes successfully. No tokens needed.
The Environment Mismatch
A 404 also shows up when the environment name on npmjs.com doesn't match the environment field in your workflow job. If npm has an environment set and your job runs without it, or under another name, the OIDC claims don't match and the publish can fail with a 404, not a meaningful error. On the npm versions that support trusted publishing, the second line of that error reads The requested resource '<package>@<version>' could not be found or you do not have permission to access it.
The environment isn't the only field that must match exactly. npm's docs add the workflow filename with its .yml or .yaml extension and repository.url in package.json, and every field is case-sensitive. npm doesn't check any of them when you save the trusted publisher, so a typo surfaces only at publish time.
What the Pipeline Looks Like Now
The full workflow for markdown-repository runs lint, tests, and build on every commit. On a GitHub release, it publishes to npm with provenance — no secrets configured anywhere in the repository.
If your CI/CD has shapes like this hiding in it — broken pipelines, secret sprawl, opaque failures — that's the kind of thing a DevOps audit is for.
Subscribe to the newsletter:
About Jurij Tokarski
I run Varstatt and create software. Usually, I'm deep in work shipping for clients or building for myself. Sometimes, I share bits I don't want to forget.
x.comlinkedin.commedium.comdev.tohashnode.devjurij@varstatt.comRSS