release-patch
v1.0.9
Published
Bump the patch version, sync master, commit, push and publish an npm package with a single command.
Downloads
3,570
Maintainers
Readme
release-patch
Bump the patch version, sync master, commit, tag, push and publish an npm package — all with a single command.
release-patch wraps the repetitive steps of cutting a patch release so any package can adopt the same flow just by installing it.
Git tags are the source of truth for versioning. The next version is derived from the latest annotated vX.Y.Z tag, never from the version field in package.json.
What it does
When run from a package's root directory, a normal patch release:
- Verifies the working tree is clean (
git status --porcelain) and aborts on uncommitted changes, before touching any branch, so stray edits can never leak into the release commit and syncingmastercan never clobber uncommitted work. - Syncs
masterwithorigin/master(git checkout master,git fetch, then a fast-forward-onlygit merge --ff-only origin/master). If localmasterhas diverged fromorigin, the sync fails loudly instead of inventing a merge commit that would then be tagged and published as a release. - Reads and validates
package.jsonfrom the syncedmastercheckout (never stale feature-branch metadata): it must declare anamethat npm itself accepts (validated withvalidate-npm-package-name, so scoped names pass and bad characters, capitals, length or reserved names fail) and must not be marked"private": true. A missing, invalid or private name fails here, before any release mutation. - Logs in to npm if you are not already authenticated (
npm login). - Fetches tags (
git fetch origin --tags) and enumerates them withgit for-each-ref, keeping only annotatedvX.Y.Ztag objects. Lightweight tags, pre-release/build tags and malformed tags are ignored — an annotatedv1.0.0wins over a lightweightv9.9.9. The latest annotated tag is the current released baseline. If no valid annotated release tag exists, it fails clearly with separate instructions for a brand-new package or an already-published no-tag package instead of guessing. - Confirms the latest annotated tag is actually published on npm. A normal release never skips an unpublished latest tag: if the baseline tag is not on npm, it blocks with instructions to inspect it and, if it was tagged but never published, finish it with
release-patch --resume. - Derives the next version by incrementing the patch component of the published latest tag, and checks the registry for that exact
<package>@<version>(npm view <package>@<version> version), passing the package name as a process argument, never through a shell. If that exact version already exists, it fails as a duplicate. Only an unambiguous npmE404code means the version is available; any other outcome — a network/auth/registry error, or anE404mixed with another error code — is treated as blocking uncertainty, and the release aborts before any mutation rather than risk an overwrite or a race. - Installs dependencies (
npm install, ornpm install --no-package-lockwhen neitherpackage-lock.jsonnornpm-shrinkwrap.jsonexists). - Writes the exact derived version without creating a git tag (
npm version <version> --no-git-tag-version). - Runs
npm run buildonly if the package defines abuildscript and itsversion/postversionlifecycle scripts do not already runbuild. - Runs a publish dry-run (
npm publish --dry-run) as a gate — before creating the release commit or tag — so a bad tarball or packaging error surfaces while everything is still local. The dry-run pushes no Git refs and publishes no package, though its lifecycle scripts (prepublishOnly,prepack,prepare) may still have external side effects. Running it before the commit and tag means a failed dry-run leaves no local tag behind to poison the next version derivation. - Commits exactly
package.json,package-lock.jsonandnpm-shrinkwrap.jsonwhen present (chore: bump patch version), each staged by name — nevergit add -A. This commit is the exact release commit. - Refuses to continue if any tracked or untracked, non-ignored change remains in the working tree after that commit. A build or dry-run lifecycle script that emits generated files or secrets outside the intended commit blocks the release with an actionable error rather than being swept into the tag and push.
- Creates an annotated tag on that release commit (
git tag -a v<version> -m v<version>). - Pushes the release commit and its exact tag to
originatomically in a single non-force push (git push --atomic origin master v<version>), somasteris never published without its matching release tag. - Publishes to npm (
npm publish). If the publish fails after the atomic push, the commit and tag are already public, so it surfaces precise recovery instructions to finish withrelease-patch --resumeand preserves the non-zero exit. - Verifies the published version is live on the registry (
npm view <package>@<version> version). Because npm's read path can lag a successful publish, verification makes up to 10 attempts across a bounded 150-second window (waiting 1, 2, 4, 8, 15, then 30 seconds between checks) and prints progress before each wait. Only an unambiguousE404is retried; network, authentication, mixed-code, and other ambiguous registry errors fail closed immediately. If the version remains unavailable after the bounded window, the command exits non-zero and directs recovery throughrelease-patch --resume; it never publishes again as a fallback. The package name comes frompackage.jsonand is passed as a process argument, never through a shell, so hostile metadata cannot inject commands.
The manifest is read and validated from the fast-forwarded master, and the baseline-published and duplicate preflights run, before any release mutation — so validation and duplicate detection reflect exactly what will be published, and no version bump, commit, tag, push, dry-run, or publish happens until the version is confirmed available. The clean-tree check and the master sync are the only Git side effects that precede validation. The install step runs before the version write so version lifecycle scripts and the pre-push build gate use dependencies from the synced package. The version is derived from tags before the install so the release fails fast when no valid tag exists.
Because the annotated tag is created on the release commit itself (rather than relying on npm version's implicit commit/tag), the tag always points at exactly the commit that records the version bump. Only package.json, package-lock.json and npm-shrinkwrap.json (when present) are committed; the npm run build step is a pre-push gate whose output is intentionally not committed (and, if it produces non-ignored files, is caught by the stray-change check). npm publish produces the published artifact from a fresh build via its own lifecycle scripts.
npm publish still runs npm lifecycle scripts such as prepublishOnly, prepack, and prepare. Those hooks run after release-patch pushes the version commit, so they do not replace the pre-push build gate.
Resuming an interrupted release
A release can be interrupted after the commit and tag are pushed but before the publish succeeds — for example a flaky network or a registry hiccup on npm publish. Because the tag now exists but the version is not on npm, a normal release would refuse to continue (it will not skip an unpublished latest tag). Finish the exact tagged version with:
release-patch --resume--resume publishes the existing latest annotated tag without bumping, committing or creating another tag. It syncs master, requires the tag to be an ancestor of current master history, and requires current master's package name/version to match the tagged release identity. It then binds the exact synced master commit and annotated tag object, creates an isolated detached Git worktree at that tag's commit, validates the tagged manifest and re-runs the dependency, build and dry-run gates there. Immediately before the atomic push and again before publication, it requires both worktree HEAD and the clean tree to remain at that exact tagged commit. The non-force atomic push uses the validated object IDs as sources for refs/heads/master and the release tag, so concurrent local ref movement cannot change what reaches origin and conflicting remote movement is still rejected. This keeps later master changes out of the package while publishing and verifying the exact tagged version. The temporary worktree is removed and the caller's original branch or detached checkout is restored after every success or failure. SIGINT and SIGTERM are deferred through that cleanup and then re-delivered to preserve signal termination semantics; an accompanying release or cleanup error is still reported. If the version is already published, --resume is a verified no-op. Resume cannot be combined with another release mode.
Bootstrapping an already-published no-tag package
Use this one-time mode when a package is already published on npm but its repository has never had a semver release tag:
release-patch --bootstrap-published 0.0.16 --expected-git-head 407db4fab09a941984614167a1377f773531949cBoth values are mandatory. The version must be exact stable X.Y.Z; the expected Git head must be a
full 40-character lowercase SHA independently reviewed from authoritative package history. Run the
command from a clean master that can fast-forward to and then exactly equals origin/master.
Before changing release state, the helper fetches tags without pruning local state and fails unless
there are no local or remote stable semver tags. It then reads version and gitHead together from
npm for the exact package version and requires the registry values to match the supplied values. The
commit must exist locally, be an ancestor of the verified current master commit, and contain a
package.json whose name and version exactly match the current package and requested release.
After all checks pass, release-patch creates annotated v<version> at the authenticated historical
commit, pushes only that immutable tag object with a non-force exact refspec, verifies both the remote
tag object and its peeled commit, and creates a public, non-prerelease GitHub release with the exact
tag title. It never advances master or runs npm publish in this mode. Tagging current HEAD is wrong
when npm's immutable gitHead names an older commit: it would make the Git release claim different
source than the published artifact.
Recovery is rerunning the exact same command. If a tag push has an uncertain outcome, the exact local tag is preserved. If the tag reached origin but GitHub release creation failed, the retry verifies the tag and finishes the release. The one exact annotated target tag is accepted as resumable state only when it still points to the authenticated commit; any other semver tag, moved/lightweight target tag, or mismatched existing GitHub release blocks. A retry never republishes npm, rewrites or force-pushes a tag, or edits/overwrites a release.
Reconciling an untagged published baseline
Use this guarded package-owned mode when npm already contains the patch immediately after the latest annotated release tag, but that published patch has no tag:
release-patch --reconcile-published 0.5.10 --expected-git-head 161a8291ffd9c66f5bdb4cd13f18e97d37e0648eBoth arguments are mandatory: the version must be exact X.Y.Z, and the expected Git commit must be its full 40-character lowercase SHA reviewed from the package's authoritative history. npm's gitHead is publisher-supplied metadata, not an independent proof of provenance. The operator-supplied SHA is therefore the trust anchor: the command reads version and gitHead together from npm for that exact package version and requires the registry value to match the expected SHA byte-for-byte before trusting it. The SHA authenticates only the one version named in that invocation; it is never reused or inferred for another published version.
The command then fails closed unless all of the following are true:
- the requested version is exactly one patch after the latest annotated tag (or is the exact tag from a partially completed reconciliation);
- the preceding annotated tag is already published on npm, preserving the normal release invariant;
- npm returns that exact version and one full 40-character lowercase hexadecimal
gitHead; - npm's
gitHeadexactly matches the operator-supplied--expected-git-head; - the commit exists after syncing
masterand fetching origin, and is an ancestor of authoritativeorigin/master; package.jsonat that commit has the same package name as current syncedmasterand exactly the requested version;- the baseline tag is absent, or is already the same annotated tag on that exact commit after an interrupted attempt; and
- the registry can unambiguously determine whether the following patch is published.
Only after every check succeeds does the command create and non-force-push the missing annotated baseline tag. If the following patch is definitely unpublished, it then runs the ordinary release transaction unchanged, deriving and publishing that patch. If the following patch is already published, the invocation is a historical-gap step: it returns successfully after pushing only the verified baseline tag, without installing dependencies, bumping a version, building, committing, creating a next tag or publishing. An ambiguous network, authentication or registry failure while checking the following patch blocks before the baseline tag is created or pushed.
For several consecutive published gaps, invoke the helper sequentially from oldest to newest. Each invocation must name that version's independently reviewed SHA:
release-patch --reconcile-published 0.5.10 --expected-git-head 161a8291ffd9c66f5bdb4cd13f18e97d37e0648e
release-patch --reconcile-published 0.5.11 --expected-git-head 2b6c9c887eab9480d485d46f67e536c96c42717a
release-patch --reconcile-published 0.5.12 --expected-git-head 3f15b81af404ef0fdcb60bca7682353c89438869If 0.5.11 and 0.5.12 are already published but 0.5.13 is not, the first two commands each push only one historical baseline tag. The third pushes v0.5.12 and then releases 0.5.13. Do not manually create missing tags or reuse one version's expected SHA for another version.
Recovery is rerunning the exact same helper invocation. If baseline tag creation fails, nothing was pushed and no release commit was made. If its push fails, an exact local tag may remain; the retry re-fetches authoritative tags and recreates or accepts only the verified exact tag. After the baseline tag is pushed, any failure during dependency installation, versioning, build, dry-run, commit, or release-tag creation automatically restores the clean pre-release master, removes the unpushed release tag and non-ignored files created by the attempt, and reports that the same invocation is safe to retry. Lifecycle scripts may still have external side effects that Git cannot undo. An atomic-push error has an ambiguous remote outcome, so the exact clean release commit/tag are preserved and recovery uses the printed release-patch --resume instruction instead of bumping again. Publishing or verification failures after a successful push use that same resume path.
Bootstrapping a brand-new package
The next version is always derived from the latest published annotated tag, so a brand-new package needs one published tag to start from. Create an annotated tag that matches package.json and the current HEAD, then publish that initial version with --resume:
git tag -a v0.0.0 -m v0.0.0 # package.json version must be 0.0.0 and HEAD must be the tagged commit
release-patch --resume # pushes the tag and publishes v0.0.0From then on, ordinary release-patch runs derive each next patch from the published tag.
Install
npm install --save-dev release-patchUsage
Add a script to your package.json:
{
"scripts": {
"release:patch": "release-patch"
}
}Then cut a release:
npm run release:patchYou can also run it ad hoc without adding a script:
npx release-patchRequirements
- The package is a git repository with a
masterbranch and anoriginremote, and a clean working tree (no uncommitted changes) when you run the release. package.jsondeclares anamethat npm accepts (validated withvalidate-npm-package-name) and is not marked"private": true.- At least one annotated release tag in
vX.Y.Zform exists (for examplev1.0.0), and that latest tag is published on npm. Lightweight tags are ignored. This published tag is the source of truth for the current released version. To bootstrap a brand-new package, see Bootstrapping a brand-new package. - An already-published package with no release tags instead uses the guarded published-baseline bootstrap, which also requires authenticated GitHub CLI access to create the matching GitHub release.
- You have publish rights to the package on npm.
License
ISC
