Why provenance
Publishing from GitHub Actions with --provenance takes one extra flag and one permission line. In return, every version on npm links back to the exact commit and workflow run that built it.
Without provenance, a package on npm is just a tarball someone uploaded. Nothing proves it matches the source on GitHub. A provenance statement is a signed attestation, logged in the public Sigstore transparency log. It records which repo, commit and workflow produced the tarball. npmjs.com then shows a green "Built and signed on GitHub Actions" badge.
I set this up for my small utility package color-is-dark. Below is the whole setup, including the mistake I made first.
The package setup
color-is-dark is a TypeScript package built with tsup and managed with pnpm. It ships CommonJS, ESM and type declarations from a single src/index.ts.
tsup.config.ts:
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
splitting: false,
sourcemap: true,
clean: true,
dts: true,
format: ['cjs', 'esm'],
});
The parts of package.json that matter for publishing:
{
"name": "color-is-dark",
"repository": {
"type": "git",
"url": "git+https://github.com/ts-web/color-is-dark.git"
},
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "tsup",
"prepack": "pnpm run build"
},
"files": ["dist"]
}
Two of these fields are essential:
-
repository.urlmust match the GitHub repo the workflow runs in. npm rejects a provenance publish when they differ. -
prepackruns the build automatically duringnpm publish. The workflow never needs a separate build step, anddist/stays out of git.
The workflow
Publishing a GitHub release triggers the workflow, which runs npm publish --provenance. Here is .github/workflows/publish.yaml in full:
name: Publish Package to npm
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v3
with:
version: 9
run_install: true
- run: pnpm i
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
What each piece does:
-
id-token: writelets the job request a GitHub OIDC token. npm uses this token to sign the provenance statement. Without it,--provenancefails. -
registry-urlinsetup-nodewrites an.npmrcthat reads the auth token fromNODE_AUTH_TOKEN. -
pnpm/action-setupinstalls pnpm, so theprepackscript (pnpm run build) can run. -
npm publish --provenance --access publicpacks the tarball, which runs the build. It then generates and signs the attestation and uploads both. -
NPM_TOKENis an npm automation (or granular) access token, stored as a repository secret.
One-time setup:
- On npmjs.com, create an access token with publish rights to the package.
- In the GitHub repo, go to Settings → Secrets and variables → Actions and add it as
NPM_TOKEN. - Bump the version, commit, push, and publish a GitHub release for the tag.
Optional: drop the token with trusted publishing
npm now supports trusted publishing, which replaces the long-lived NPM_TOKEN. On the package settings page on npmjs.com, add the GitHub repo and workflow file as a trusted publisher. The job then authenticates with the same OIDC token, and npm attaches provenance automatically. This requires a recent npm CLI (11.5 or later), so check the npm trusted publishers docs for the current requirements. I haven't switched color-is-dark over yet.
Verifying it worked
The check is on the package page: scroll to the bottom of color-is-dark on npmjs.com. A Provenance section there links to the source commit, the workflow file and the run that built the version.
From the command line, any project that depends on the package can verify the signatures and attestations:
npm audit signatures
The output counts the packages with verified registry signatures and verified attestations. An attestation that fails to verify is reported as an error.
Gotchas
-
Specify the pnpm version. My first run failed because
pnpm/action-setuphad noversion:. The fix was one commit: "GH actions fix -- specify pnpm version". Alternatively, set apackageManagerfield inpackage.json. - Provenance only applies from CI. Version 1.0.0 was published from my laptop, so it has no provenance. I republished as 1.0.1 with no code changes, just to get a signed version. The changelog says so.
-
repository.urlmust match the repo. If you fork or rename the repo, update the field, or the publish is rejected. -
The workflow installs twice.
run_install: truealready runspnpm install, so the separatepnpm istep is redundant. It's harmless, but you can drop one of them. -
Use
npm publish, notpnpm publish, for provenance. The--provenanceflag belongs to the npm CLI. Running it through npm while using pnpm for installs works fine.
Wrapping up
The whole setup is one workflow file and one secret, and every release after that carries a verifiable link to its source. For small packages that others pull into their dependency trees, that's worth the ten minutes.
Links:
Top comments (0)