This section covers the release engineering side of dual-format npm packages: automating builds and publishes with GitHub Actions, generating cryptographic Sigstore provenance for every tarball, replacing long-lived tokens with OIDC trusted publishing, and validating a package’s shape before it ever reaches the registry. It is written for maintainers and platform engineers who need a release pipeline that is reproducible, auditable, and resistant to supply-chain tampering.


The Automated npm Publish Pipeline A five-stage horizontal flow: a tagged commit triggers a build that emits both ESM and CJS artifacts, which are validated by publint and attw, then published with a Sigstore provenance attestation, and finally land on the npm registry alongside their transparency-log entry. Source to Registry Source tagged commit Build dual ESM + CJS Validate publint + attw Publish --provenance Registry + transparency log

Quick-Reference: Key Terms

Term Definition Reference
Provenance A signed statement linking a published tarball to the exact source commit and CI workflow that produced it npm Provenance & Sigstore Attestation
Sigstore The open, keyless signing infrastructure (Fulcio + Rekor) npm uses to generate and store provenance attestations npm Provenance & Sigstore Attestation
OIDC trusted publishing A publish flow where npm exchanges a CI-issued OIDC token for a short-lived credential, removing the need for a stored NPM_TOKEN Configuring OIDC Trusted Publishing for npm
dist-tag A named pointer (latest, next, beta) that maps to a specific published version, controlling what plain npm install resolves to Managing prerelease and dist-tags on npm
Attestation The signed, verifiable record (build provenance or publish attestation) stored alongside a package version in the registry Publishing with npm Provenance in GitHub Actions
publint A static linter that checks a package’s exports, main, and types fields resolve to files that actually exist in the published tarball Using publint to catch exports errors
attw (are the types wrong) A CLI that simulates how TypeScript resolves a package’s types under every moduleResolution mode and reports mismatches Checking types with are-the-types-wrong

Core Concepts

The automated release pipeline

A dual-format package cannot be published safely by running npm publish from a developer’s laptop. The build step must run in a clean, reproducible environment so the ESM and CJS artifacts in the tarball match exactly what CI validated — a laptop with stale dist/ output or an uncommitted local patch produces a tarball nobody can audit. The baseline pipeline triggers on a version tag, installs with a locked lockfile, builds both formats, validates the result, and only then publishes:

name: Release
on:
  push:
    tags:
      - 'v*'

permissions:
  contents: read
  id-token: write   # required for --provenance and OIDC trusted publishing

jobs:
  release:
    runs-on: ubuntu-latest
    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 run build
      - run: npx publint --strict
      - run: npx attw --pack .
      - run: npm publish --provenance --access public

Every step is ordered deliberately: npm ci (not npm install) enforces the committed lockfile, the build runs before validation so publint inspects real output, and npm publish is the last step so a validation failure never reaches the registry.

How provenance attestation works

npm publish --provenance asks npm to generate a Sigstore attestation during the publish step instead of, or in addition to, signing with a maintainer’s personal key. The CI job proves its identity to Sigstore’s Fulcio certificate authority using the workflow’s short-lived OIDC token — no long-lived signing key is stored anywhere. Fulcio issues a certificate valid for minutes, npm’s CLI signs the package manifest and tarball digest with it, and the resulting attestation is written to Sigstore’s Rekor transparency log, a public, append-only ledger.

# Inside the CI job, after the build:
npm publish --provenance --access public

# npm CLI (>= 9.5) performs, transparently:
# 1. Requests a short-lived OIDC token from the CI provider (GitHub Actions)
# 2. Exchanges it for a Fulcio-issued signing certificate
# 3. Signs the tarball digest + build metadata
# 4. Uploads the attestation to Rekor and links it to the published version

The result is a verifiable answer to “which commit and which workflow run produced this exact tarball” — without requiring consumers to trust the maintainer’s personal machine or a shared secret.

Token auth vs OIDC trusted publishing

Classic npm automation relies on a long-lived NPM_TOKEN stored as a repository secret. It works, but the token grants publish rights indefinitely until manually rotated or revoked, and a leaked token compromises every package it can publish. OIDC trusted publishing removes the stored secret entirely: the maintainer configures npmjs.com to trust a specific GitHub repository and workflow file, and at publish time npm exchanges the workflow’s ephemeral OIDC token for a publish credential that expires within minutes.

# No NPM_TOKEN secret required at all — trust is configured on npmjs.com
permissions:
  id-token: write
  contents: read

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      registry-url: 'https://registry.npmjs.org'
  - run: npm ci && npm run build
  - run: npm publish --provenance

If the workflow file, branch, or repository does not exactly match what was registered as a trusted publisher, the exchange fails closed — there is no fallback to a stored token unless one is explicitly configured as a backup.

Hazard and Failure-Mode Inventory

HAZARD PREVENTION

Symptom: npm ERR! code ENEEDAUTH during npm publish in CI, even though a token secret is configured.

Root cause: The NPM_TOKEN environment variable was not exported to the npm publish step, the token expired, or registry-url was omitted from actions/setup-node, leaving .npmrc unconfigured.

Fix: Pass registry-url: 'https://registry.npmjs.org' to setup-node, export NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} on the publish step, or switch to OIDC trusted publishing to remove the token dependency entirely.

HAZARD PREVENTION

Symptom: npm publish --provenance fails with an error mentioning the OIDC token could not be obtained, or provenance silently does not appear on the published version.

Root cause: The workflow’s permissions block does not grant id-token: write. Without it, the runner never issues the short-lived OIDC token that Sigstore’s Fulcio authority needs to sign the attestation.

Fix: Add permissions: { id-token: write, contents: read } at the job or workflow level in your release workflow.

HAZARD PREVENTION

Symptom: npm publish --provenance succeeds for a public package but fails, or is rejected on the registry, for a scoped package that has never been published before.

Root cause: Scoped packages (@scope/name) default to private on first publish. Provenance attestations are only supported for packages published with public visibility; omitting --access public on a scoped package’s first publish causes a registry-side rejection unrelated to the attestation itself.

Fix: Always pass --access public alongside --provenance the first time a scoped package is published, as shown in publishing with npm provenance in GitHub Actions.

HAZARD PREVENTION

Symptom: A published version is missing files a consumer expects, or ships a stale dist/ build from a previous run — even though the workflow appears green.

Root cause: The build step was skipped or cached incorrectly, and npm publish ran against a leftover dist/ directory from an earlier job, a local checkout, or a partially restored cache.

Fix: Run the build step unconditionally before publish in the same job (never a separate, independently-cacheable job), and add a pre-publish validation step that fails the pipeline if publint finds paths that do not resolve.

Decision Guide: Token, OIDC, or Both?

Which authentication method you use to publish depends on two questions: can your CI provider present an OIDC identity token, and can you pin a single trusted workflow and branch on npmjs.com? The tree below maps those answers to the three outcomes.

Choosing an npm Publish Authentication Method A branching decision tree. First: does your CI support OIDC trusted publishing? If no, use a granular access token. If yes, can you register a fixed workflow and branch as a trusted publisher? If yes, use OIDC trusted publishing with no stored token; if no, fall back to a scoped automation token. CI supports OIDC? GitHub Actions, GitLab CI Yes No Fixed workflow + branch? registrable on npmjs.com Granular Access Token scoped + rotated Yes No OIDC trusted publish no stored token Scoped automation token fallback for dynamic workflows
  • OIDC trusted publishing is the default when your CI can present an OIDC token and you can register a fixed workflow file and branch as a trusted publisher on npmjs.com — no long-lived secret is stored anywhere.
  • A scoped automation token is the fallback when workflows are dynamic (matrix publishes or generated workflow files) so a single trusted-publisher record cannot cover them. Use a Granular Access Token scoped to the one package.
  • A Granular Access Token is the only option for self-hosted runners or CI providers that cannot issue OIDC tokens; rotate it on a schedule.

Topic Index

npm Provenance & Sigstore Attestation

How npm publish --provenance produces a Sigstore-backed attestation, how GitHub Actions permissions feed the signing flow, and how consumers verify the resulting supply-chain link before installing. Read guide →

Includes: Publishing with npm Provenance in GitHub Actions, Verifying Package Provenance with npm audit signatures


Automating npm Releases with GitHub Actions

A complete release workflow for dual-format packages: matrix builds, registry authentication, build-before-publish ordering, and tag-triggered releases that stay reproducible across runs. Read guide →

Includes: Configuring OIDC Trusted Publishing for npm, semantic-release for Dual-Format Packages


Validating Packages Before Publish

The pre-publish checklist that catches broken exports maps, incorrect TypeScript resolution, and missing files before they ship — using publint, are-the-types-wrong, and npm pack --dry-run. Read guide →

Includes: Using publint to catch exports errors, Checking types with are-the-types-wrong


Versioning & Changelog Automation

Automating semantic version bumps, changelog generation, and dist-tag management so prereleases, betas, and stable releases never collide on the registry. Read guide →

Includes: Automating Changelogs with Changesets, Managing Prerelease and dist-tags on npm

Frequently Asked Questions

Does npm publish --provenance work outside GitHub Actions?

Provenance generation requires a CI environment that issues an OIDC token npm’s CLI can exchange with Sigstore’s Fulcio authority. GitHub Actions and GitLab CI both support this natively as of npm 9.5+. Publishing from a local machine or an unsupported CI provider cannot generate provenance — the flag is silently unavailable outside a recognized OIDC-issuing environment.

Is OIDC trusted publishing the same thing as provenance?

No — they solve adjacent but distinct problems. Provenance proves what built the tarball (the workflow, commit, and build environment). OIDC trusted publishing removes the credential used to authenticate the publish call itself, replacing a stored NPM_TOKEN with a short-lived exchange. Most release pipelines use both together.

Can I build once and publish the same artifact from a promotion pipeline instead of rebuilding per stage?

Yes, and it is the safer pattern: build once, upload the tarball as a workflow artifact, run validation against that exact tarball, then publish it unchanged. Rebuilding at the publish step risks a non-reproducible build producing a different tarball than what was validated.

Do I need both publint and are-the-types-wrong in CI?

They check different failure modes. publint validates that exports/main/files fields point at real files with correct condition ordering. attw simulates how TypeScript actually resolves your types under node10, node16, and bundler resolution — a package can pass publint and still resolve to the wrong .d.ts file under a specific moduleResolution. Run both, detailed in validating packages before publish.

What happens if I forget to bump the version before publishing?

npm publish rejects a version that already exists on the registry with npm ERR! 403 Forbidden - cannot publish over previously published version. Automating version bumps with tools like semantic-release or Changesets removes this class of human error entirely by deriving the version from commit history.


← Back to home