npm Publish
Guardrails for publishing npm packages — OIDC setup, version sync, release workflows, and common failure modes.
Before Publishing, Ask
- Has this package been published before? First publish requires a traditional token or
npm login. OIDC can only be configured after the package exists on npm. - Is the version in package.json the one you intend to publish? Check against
npm view <pkg> versions --jsonand the current git tag. They must all agree. - Are you shipping built artifacts? Verify
filesin package.json points to the right directory and the build step runs before publish.
OIDC Trusted Publishing
MANDATORY — READ references/oidc-setup.md when setting up or troubleshooting OIDC workflows.
Key constraints:
- OIDC requires
id-token: writepermission — that is the only workflow requirement - Do NOT set
registry-urlinactions/setup-node— it injectsGITHUB_TOKENasNODE_AUTH_TOKEN, which npm uses instead of OIDC, causing 404 - Do NOT set
environment:on the job unless the exact same environment name is configured in the trusted publisher on npmjs.com — a mismatch causes silent 404 - Node 24 is required (ships with npm 11) — Node 22 ships with npm 10 which has no OIDC support;
npm install -g npm@latestalso crashes on npm 10 withMODULE_NOT_FOUND - No
NPM_TOKENsecret needed — remove it - Workflow filename on npmjs.com settings must match exactly (case-sensitive, including
.yml) - First publish cannot use OIDC — package must exist on npm first
- The "Require OIDC" radio button on npmjs.com is critical and easy to miss
Version Synchronization
The #1 source of publishing failures: package.json version ≠ git tag ≠ GitHub release version.
MANDATORY — READ references/pitfalls.md when troubleshooting publish failures.
The invariant: one source of truth for version. Pick one:
- Git tag is truth: Derive
package.jsonversion from$GITHUB_REF_NAMEin CI (npm version "$TAG_VERSION" --no-git-tag-version) - package.json is truth: Use
npm version <patch|minor|major>which updates package.json, commits, and tags atomically - Changesets/Lerna is truth: Let the tool manage it — don't manually edit versions
Validate before publish:
PKG_VERSION=$(jq -r .version package.json)
TAG_VERSION="${GITHUB_REF_NAME#v}" # strip leading 'v' from tag
[ "$PKG_VERSION" != "$TAG_VERSION" ] && echo "VERSION MISMATCH" && exit 1
Pre-Publish Checklist
Run these before every publish attempt:
npm publish --dry-run # See what will be shipped
npm pack && tar -tzf *.tgz # Inspect tarball contents
NEVER
-
NEVER publish without
--dry-runfirst Instead: Runnpm publish --dry-runand inspect the output. Why: Catches wrong files field, missing build, accidental node_modules inclusion. -
NEVER try to republish the same version Instead: Always bump version. Check
npm view <pkg> versions. Why: npm versions are immutable. Even after unpublish, the version is tainted for 24h, blocked after 72h. -
NEVER trigger publish on push to main Instead: Use
on: release: types: [published]oron: workflow_dispatch. Why: Every push would publish. Accidental commits ship broken packages. -
NEVER forget
--access publicfor scoped packages Instead: Always include--access publicin the publish command or set it once withnpm config set access public. Why: First publish of@scope/pkgdefaults to restricted (private). Users can't install it. -
NEVER store npm tokens long-term in CI secrets when OIDC is available Instead: Configure OIDC trusted publishing. Why: Tokens are long-lived, can leak via logs or config, require manual rotation. OIDC uses short-lived, workflow-scoped credentials.
-
NEVER skip the build step in CI Instead: Always run
npm run build(or equivalent) beforenpm publish. Why:npm publishships whatever is on disk. Ifdist/isn't built, you ship nothing. -
NEVER edit
package.jsonversion and tag separately Instead: Usenpm version <semver>which updates package.json, creates commit, and tags atomically. Why: Manual edits create version/tag drift — the root cause of most publish failures.