Npm publish ci pitfalls
Skill narenaryan/agent-skills/skills/github/npm-publish-ci-pitfalls
Byte-sized agent skills for giving advanced knowledge to AI agents
npx -y skills add narenaryan/agent-skills --skill npm-publish-ci-pitfallsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when setting up or debugging npm publishing from CI (GitHub Actions tag-release workflows) — covers EOTP token failures, npm ≥11.6 silently stripping ./-prefixed bin entries, provenance/OIDC rejections on private repos, ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING from shipping raw .ts, and registry 404s right after a successful publish
SKILL.md
4.8 KB, as published. Nobody here has run it
npm Publish from CI: Non-Obvious Pitfalls
Field-tested failure modes of npm publish in GitHub Actions (tag-triggered release workflows). Each was hit live; the diagnostic signature is the log line quoted.
1. npm error code EOTP — token cannot bypass 2FA
CI publish reaches the registry, then dies with This operation requires a one-time password. The NPM_TOKEN is valid but the account's 2FA-for-writes policy demands an interactive OTP.
Fix (operator, npmjs.com): granular access token with "Bypass two-factor authentication" explicitly enabled. Note npm is deprecating bypass-2FA tokens for publishing (gh.io/npm-gat-bypass2fa-deprecation) — prefer trusted publishing (§3) once the package exists.
2. npm ≥11.6 silently strips bin entries with ./ prefix
Log signature: npm warn publish "bin[cmd]" script name src/cli.js was invalid and removed. It's a warning, not an error — the publish proceeds and ships a package with no CLI command at all.
Fix: "bin": { "cmd": "src/cli.js" } — no ./ prefix. Verify before tagging:
npm pkg fix # normalizes; diff should be empty on a clean manifest
npm pack && tar -xOf *.tgz package/package.json | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).bin"
npm pack --dry-run does NOT surface this warning; only the publish path (or npm pkg fix) does.
3. Provenance and OIDC on private repos
npm publish --provenancefrom a private repo fails at the registry:E422 … Unsupported GitHub Actions source repository visibility: "private". Provenance requires a public source repo, full stop.- Trusted publishing (OIDC) itself works from private repos — but it turns provenance ON by default, so a private repo must set
provenance=false(envnpm_config_provenance: falseon the publish step) or it hits the same E422. - Trusted publisher config lives in package settings on npmjs.com → the package must already exist. Bootstrap the first publish with a token, then switch.
- OIDC needs npm ≥ 11.5.1, GitHub-hosted runners, job permission
id-token: write, and setup-node'sregistry-url. Reusableworkflow_callworkflows are unreliable (validation checks the calling workflow's filename).
4. Raw .ts in a package breaks every install (Node type stripping)
A Node ≥22.6/24 project can run .ts files directly in its own checkout, so a package that ships raw TypeScript passes every repo-side check — then the installed CLI dies instantly with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING: Node deliberately disables type stripping for files under node_modules.
Fix: compile at pack time. TS ≥5.7's rewriteRelativeImportExtensions: true permits allowImportingTsExtensions imports with emit and rewrites ./x.ts → ./x.js in the output — dev keeps running raw .ts, the tarball gets lib/*.js (outDir: lib, allowJs to carry plain-JS entry files, bin → lib/cli.js, build in prepack).
Prevention (the real lesson): tarball-content inspection is not enough — npm pack --dry-run looked perfect here. CI must smoke-test the packed artifact the way a user gets it:
npm pack
TMP=$(mktemp -d)
npm install -g --prefix "$TMP" ./<name>-*.tgz
"$TMP/bin/<cmd>" # bare invocation: exercises the full import chain, must exit 0
5. Registry 404 immediately after a successful publish
+ pkg@version in the CI log is authoritative — the publish landed. Yet npm view/npm install can 404 for minutes on a brand-new package: the top-level packument lags and CDNs negative-cache the 404.
Diagnose instead of panicking: the versioned manifest (registry.npmjs.org/<pkg>/<version>) and tarball URL go 200 before the packument does. If those are 200, wait (~1–5 min); repeated polling of the packument can keep re-priming the cached 404, so use a cache-buster query param.
6. Tag-release hygiene
- Guard step: fail if
${GITHUB_REF_NAME#v}≠node -p "require('./package.json').version". - After a failed release run is fixed by new commits, do not re-run the failed workflow run — it rebuilds the old tag commit. Re-point the tag:
git tag -f vX.Y.Z origin/main && git push -f origin vX.Y.Z. - Nothing is consumed on the registry until a publish fully succeeds — a failed version number remains usable.
- Scoped packages:
publishConfig: { "access": "public" }(default is restricted), andrepositoryfield must match the repo when provenance is on.