I published 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.
The Old Way
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 never expires, has write access to your packages, and lives in plain text 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 set a 90-day expiration. But you still have to rotate them manually.
Trusted Publishing
npm now supports trusted publishing with OIDC. 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.
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
I spent a while debugging my workflow before realizing trusted publishing only works from the second release onward. Once the package exists on npmjs.com, go to its settings and add a trusted publisher. From that point, the workflow handles everything.
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.
In the workflow: add id-token: write permission and an environment that matches what you configured on npm.
name: Release
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24.x
registry-url: https://registry.npmjs.org
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
- run: npm publish --provenance --access public
Provenance attestation is automatic with trusted publishing. The --provenance flag is redundant but makes the intent explicit.
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. The OIDC token exchange succeeded — I could see the signed provenance statement in Rekor's transparency log. Everything worked except the actual publish.
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.
- uses: actions/setup-node@v4
with:
node-version: 24.x
If you're stuck on an older Node version, upgrade npm explicitly:
- run: npm install -g npm@latest
With npm 11.5.1+, the same workflow publishes successfully. No tokens needed.
The Environment Mismatch
The same 404 shows up when the environment name on npmjs.com doesn't match the environment field in your workflow job. If your workflow says environment: release but npm has the environment field blank (or vice versa), the OIDC claims don't match and npm rejects the publish — with a 404, not a meaningful error.
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.
Top comments (0)