Skip to content
template confidence: high

npm Trusted Publisher GitHub Actions Workflow Template (Copy-Paste, 2026)

A copy-paste GitHub Actions workflow for npm trusted publishing in 2026: id-token, registry-url, --provenance, --access public, and the comments explaining why each line is required.

Published May 15, 2026 · kw: npm trusted publisher github actions workflow

Sources: S-001 S-002 S-003 S-005

A working copy-paste workflow for npm trusted publishing, with every line annotated. Save as .github/workflows/publish.yml. The filename must match the one you registered as a trusted publisher on npmjs.com.

TLDR

name: Publish to npm

on:
  release:
    types: [published]

permissions:
  contents: read
  id-token: write

jobs:
  publish:
    name: npm publish
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: "https://registry.npmjs.org"
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test --if-present

      - name: Build (if applicable)
        run: npm run build --if-present

      - name: Publish to npm
        run: npm publish --provenance --access public

Line-by-line: why each block matters

Trigger

on:
  release:
    types: [published]

Publishing on release.types: [published] is the safest default. The release object is created on the GitHub UI (or via gh release create) and the workflow runs exactly once.

Alternative: on: push: tags: ["v*"] — also acceptable. Avoid on: push to main for publish workflows. The most common “oops we just published v0.0.1-dev” thread in npm/cli (S-005) is a workflow scoped to every main push.

Permissions

permissions:
  contents: read
  id-token: write
  • contents: read — needed because actions/checkout reads the repo.
  • id-token: write — needed because the workflow asks GitHub to mint an OIDC token. Without this, OIDC silently does not engage and npm falls back to legacy auth (GitHub OIDC docsS-002).

If your job creates a tag or release, add contents: write.

Runner

runs-on: ubuntu-latest

GitHub-hosted runners issue OIDC tokens with iss=https://token.actions.githubusercontent.com, which npm trusts. Self-hosted runners may issue tokens npm rejects (npm docsS-001). Use GitHub-hosted runners for publishing. If you need self-hosted for CI, split publishing into a job pinned to ubuntu-latest.

Checkout

- uses: actions/checkout@v4
  with:
    fetch-depth: 0

fetch-depth: 0 is optional but useful if your build inspects git history (changelogs, version tags). For a minimal workflow you can omit it.

setup-node

- uses: actions/setup-node@v4
  with:
    node-version: 20
    registry-url: "https://registry.npmjs.org"
    cache: npm
  • actions/setup-node@v4 — required. v3 does not fully wire the OIDC audience.
  • node-version: 20 — pick an LTS that ships with npm@9.5 or newer (provenance support — npm CLI releases, S-003). Node 20 ships with npm 10.
  • registry-url: "https://registry.npmjs.org" — load-bearing. Without it, the OIDC audience claim does not point at npm and the token is rejected.
  • cache: npm — optional, speeds up CI.

Install + test + build

- run: npm ci
- run: npm test --if-present
- run: npm run build --if-present

npm ci over npm install so your package-lock.json is the source of truth. --if-present lets the workflow apply to repos that have not defined those scripts.

Publish

- run: npm publish --provenance --access public
  • --provenance — generates the Sigstore attestation. Without it, the package publishes but the provenance badge does not appear on npm.
  • --access public — required if your package is scoped (@you/lib). Without it, scoped packages publish privately and npm install fails for everyone else.

There is no NODE_AUTH_TOKEN, no env: NPM_TOKEN. If either of those lines is in your workflow, OIDC is bypassed.

Variant: matrix publish for monorepos

strategy:
  matrix:
    package:
      - packages/core
      - packages/cli
      - packages/plugin-x
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
    working-directory: ${{ matrix.package }}

Each package needs its own trusted publisher row on npm pointed at this same publish.yml. The matrix workflow is one file, one trusted-publisher registration per package.

Variant: pnpm

- uses: pnpm/action-setup@v4
  with:
    version: 9
- uses: actions/setup-node@v4
  with:
    node-version: 20
    registry-url: "https://registry.npmjs.org"
    cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm publish --provenance --access public --no-git-checks

pnpm publish speaks OIDC starting at v9.5. --no-git-checks skips the “working tree dirty” guard that pnpm applies by default — fine in CI where the tree is always clean. yarn classic (v1) does not speak OIDC; if you want trusted publishing on a yarn classic project, switch the publish step to npm publish.

Common mistakes

  • Pasting this template, leaving an existing env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} line below — npm CLI prefers it and bypasses OIDC.
  • Forgetting that permissions must be set at the workflow or job level, not the step level.
  • Renaming the file without updating the trusted publisher on npm. The trusted publisher is exact-match on filename.
  • Running npm publish --dry-run in CI as a “test” — --dry-run still requires auth and can succeed misleadingly.

FAQ

Can I just npm publish without --access public?

If your package is unscoped (super-lib not @you/super-lib), yes. Scoped packages need --access public or publishConfig.access in package.json.

How do I pin the action versions for supply-chain safety?

Replace @v4 with the commit SHA: actions/setup-node@<40-char-sha>. Watch for security advisories on the action repo and bump the SHA when patches land.

What if I need to publish from a Linux self-hosted runner?

Today, use GitHub-hosted for the publish job. Self-hosted for everything else. The runner issuing the OIDC token has to match what npm’s registry trusts.

Next step

Run the preflight checklist against this template and your package.json. If you see green across the board, your next git tag is safe. For the failure shapes this template prevents, read npm publish —provenance failed: 8 reasons and the OIDC migration playbook.