Publish safely
Run the release gate
Make tests, build, consumer checks, metadata, tarball inspection, and version state block publishing.
A release should be the output of repeatable checks, not a manual command you run from an uncertain working tree.
I wire those checks into one script called release:check:
{
"scripts": {
"release:check": "npm ci && npm test && npm run build && npm pack --dry-run && node scripts/consumer-smoke.mjs"
}
}
If any step fails, publishing stops. No partial fixes after the fact.
What the gate should catch
Dirty git tree: npm version refuses to tag when uncommitted files exist. That protects you from releasing code that is not in git.
Stale build output: delete dist/, run the gate, and confirm npm run build recreates every file referenced in exports.
Already used version: npm publish fails if @acme/[email protected] already exists on the registry. Bump the version before you retry.
Bad tarball contents: npm pack --dry-run should list only supported files. Add a grep step in CI that fails if .env or fixtures/secrets appear.
Broken consumer path: scripts/consumer-smoke.mjs packs the package, installs the tarball in a temp dir, and imports the public API.
Prove the gate works
Break it on purpose once. Leave a stale file in dist/ or skip npm run build, then run npm run release:check. The script should fail before any network write.
Fix the tree, rerun, and only then tag:
npm version patch
git push --follow-tags
My advice: never publish from a laptop without running the same gate CI runs. If the gate is too slow locally, split a fast prepublishOnly hook from a slower nightly job, but do not drop checks entirely.
Log the gate output in CI artifacts. When a release fails at 2 a.m., the saved npm pack --dry-run listing tells you whether the tarball or the tests broke first.
Lesson completed