Automate releases from your
CHANGELOG.md. Graduate the[Unreleased]block into a real release — bump the version, rewrite the changelog, commit, tag, and push — with one command.
pubv is a tiny, zero-config release automation CLI for projects that keep a Keep a Changelog-style CHANGELOG.md. It picks a sensible semver bump from your unreleased notes, then commits, git-tags, and pushes the release for you — on GitHub, GitLab, or Bitbucket. No config files, one runtime dependency.
pubv v1.0.0
── preflight ─────────────────────────────
✓ on main
✓ working tree clean
✓ origin up-to-date
── plan ──────────────────────────────────
last 1.2.0
major 2.0.0
minor 1.3.0 ← default
patch 1.2.1
tag fmt v1.3.0 matches existing tags
version? [1.3.0] › _
Under the hood: it reads CHANGELOG.md, figures out a sensible default for the next version, rewrites the changelog, commits, tags, and pushes — with a single push using --follow-tags.
# one-shot
bunx @runwisp/pubv # or: npx @runwisp/pubv
# global (recommended — puts `pubv` on your PATH)
bun add -g @runwisp/pubv # or: npm install -g @runwisp/pubvRequires Node 20+ (or Bun).
pubv # interactive
pubv 1.3.0 # explicit version
pubv minor # shorthand: bump minor based on last release
pubv --dry-run # print the plan; don't change anything
pubv --yes 1.3.0 # CI mode: no prompts, exit non-zero on any issue- Preflight — verifies
CHANGELOG.mdexists, you're on the default branch, the tree is clean, andoriginis reachable and not ahead of you. - Plan — parses
CHANGELOG.md, finds your last released version, suggests a default bump based on what's in[Unreleased](see Heuristic), and detects the tag prefix (v1.2.3vs1.2.3) from your existing tags. - Rewrite — moves the
[Unreleased]body into a new## [<version>] - <date>section, and rewrites the trailing[Unreleased]/[<version>]link refs with compare-URLs for your forge (GitHub / GitLab / Bitbucket auto-detected). - Commit + tag + push —
git commit -m v<version>,git tag -a <prefix><version>,git push --follow-tags origin <branch>.
If anything fails or you say no at any prompt, CHANGELOG.md is left untouched.
pubv inspects the [Unreleased] section:
Trigger in [Unreleased] body |
Default bump |
|---|---|
BC, BREAKING CHANGE, or Breaking (capitalized) |
major |
Added or Changed |
minor |
otherwise (incl. only Fixed / Security / Removed) |
patch |
You can always override on the prompt or via pubv major | minor | patch | pre | 1.2.3.
Removedalone doesn't force major — many changelogs use it for deprecations and dead-code removal that aren't breaking. Mark genuinely-breaking entries with**BC**:orBREAKING CHANGE:.
pubv [version] [flags]
--dry-run Show the plan; don't change anything.
-y, --yes Skip all confirmations (suitable for CI).
--no-push Don't push to the remote.
--no-tag Don't create a tag.
--merge-request,--mr Protected branch: commit on a release/<version> branch,
push it, and print a "create merge request" URL (no tag).
--tag-release Post-merge step: tag the latest changelog release on HEAD
and push the tag (pairs with --merge-request).
--tag-prefix=v|none Override tag-prefix auto-detection.
--changelog=PATH Path to the changelog file (default: CHANGELOG.md).
--remote=NAME Remote name (default: origin).
--date=YYYY-MM-DD Override today's date in the new heading.
-h, --help Show this help.
-v, --version Print pubv's version.
pubv respects NO_COLOR and CI (no spinners, no ANSI colors).
If your default branch is protected and only accepts changes via merge/pull
requests, you can't push the release commit directly. pubv splits the release
into two steps:
pubv --merge-request # off the default branch:
# creates release/<version>, commits the changelog there,
# pushes the branch, and prints a ready-to-open MR URL.
# No tag is created yet — your local default stays clean.
# … review and merge the MR. Then, on the updated default branch:
git switch main && git pull
pubv --tag-release # reads the latest released version from CHANGELOG.md and
# tags the real merge commit, then pushes just the tag.The tag is deferred to --tag-release on purpose: until the MR is merged the
release commit isn't on the protected branch, and a squash/rebase merge would
leave a tag pointing at a commit that never lands there. --tag-release tags
HEAD of the merged default branch so the tag is always correct.
Code layout:
src/
core/ pure logic — parse, transform, bump, host, prefix, orchestrator
ports/ narrow TS interfaces for git, fs, prompt, logger
adapters/ the only files that touch node:fs / node:child_process / node:readline
cli/ hand-rolled arg parser + help + composition root
bin.ts entry point
Tests:
- Unit — every core module has its own test file. Changelog transforms are driven by plain
.md/.jsonfixtures intests/fixtures/— input + expected output + metadata. Adding a new scenario is a new folder, no test code change. - E2E —
tests/e2e/publish.test.tsspawns the built CLI inside a temp git repo against a local bare remote, verifying the full path including the push.
pubv deliberately does one thing: turn a Keep a Changelog CHANGELOG.md into a tagged, pushed release. If you were looking at heavier release tools — standard-version, release-it, semantic-release, changesets, np, or commit-and-tag-version — and wanted something smaller that trusts the changelog you already write (no commit-message conventions, no config file, one dependency), that's the gap pubv fills.
bun install
bun run lint
bun run typecheck
bun test # fast: ~50 unit tests + 7 e2e
bun run build