DEV Community

Jurij Tokarski
Jurij Tokarski

Posted on • Originally published at varstatt.com on

npm Publish Without Tokens

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 }}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you're stuck on an older Node version, upgrade npm explicitly:

- run: npm install -g npm@latest
Enter fullscreen mode Exit fullscreen mode

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)