CI/CD, Publishing & npm Provenance
Automate npm publishing with GitHub Actions, OIDC trusted publishing, Sigstore provenance attestation, and pre-publish validation for dual ESM/CJS packages.
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.
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 ENEEDAUTHduringnpm publishin CI, even though a token secret is configured.Root cause: The
NPM_TOKENenvironment variable was not exported to thenpm publishstep, the token expired, orregistry-urlwas omitted fromactions/setup-node, leaving.npmrcunconfigured.Fix: Pass
registry-url: 'https://registry.npmjs.org'tosetup-node, exportNODE_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 --provenancefails 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
permissionsblock does not grantid-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 --provenancesucceeds 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 publicon a scoped package’s first publish causes a registry-side rejection unrelated to the attestation itself.Fix: Always pass
--access publicalongside--provenancethe 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
buildstep was skipped or cached incorrectly, andnpm publishran against a leftoverdist/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
publintfinds 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.
- 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.
Related
- Module System Fundamentals & Dual-Package Resolution — the
exportsfield and dual-format output this pipeline builds and validates before every publish. - Tree-Shaking & Bundle Optimization — bundle-size regressions are worth catching in the same CI run that validates and publishes a release.
- TypeScript Configuration & Build Tooling — the
tsconfig.jsonand build-tool settings that produce the declaration filesattwchecks in this pipeline. - Mastering the package.json Exports Field — the condition map that
publintandattwboth validate against during pre-publish checks.