verbump
Version and changelog bumps for repos: only the packages that changed move.
verbump finds the packages in your repo that changed since their own last tag, writes each one's new version into its files, and turns its changelog's ## [Unreleased] notes into the release. One package or fifty, Python, Node, .NET, Java, Rust, Go or anything with a version in a file. One CLI, one verbump.toml, no dependencies.
Try it in one command: uvx --from verbump-cli verbump demo
A PR from feat/checkout changed only apps/web: web gets a minor bump and its release notes, api stays at 1.4.0.
Try it in 60 seconds
With verbump installed, this builds the repo from the recording in a scratch folder and releases it:
verbump demo --dir verbump-try --setup-only && cd verbump-try
verbump status # api and web, both tagged; web has changes
verbump plan --branch feat/checkout # web: 0.3.0 -> 0.4.0 (minor, tag web/v0.4.0)
verbump apply --branch feat/checkout # writes apps/web/package.json and CHANGELOG.md
verbump notes web # the notes for the GitHub release
Or run verbump demo for a narrated replay in a temporary folder.
Why verbump
- Only what changed moves. Each package is released from its own last tag, so a fix in one service doesn't bump the other nine.
- Release notes you already wrote. The
Unreleasednotes become the version's section and the GitHub release body; with no notes, the commit subjects that touched the package. - Any stack. It reads and writes versions in
package.json,pyproject.toml,Cargo.toml,.csproj,pom.xml, Gradle, Helm charts, Dockerfiles and more, or any file you give a pattern. Go-style tags work too. - You choose the bump. A flag, the PR's branch name, or trunk-semver's outputs. A
release/<package>/X.Y.Zbranch cuts an exact version. - Safe to re-run. Tags are created once and never moved; a second run finds nothing to release.
- Nothing to run. Python 3.11+ and the standard library; no server, no plugins per language.
What it's for
| Use | What you get |
|---|---|
| A repo with several services or libraries | Per-package versions, tags (api/v1.5.0) and changelogs, from one config. Example |
| A single package | tag_format = "v{version}" and one package at .; verbump releases itself this way. Its config |
| GitHub Actions | verbump-release releases on merge: bumps, commits, tags, pushes and publishes a GitHub release per package. verbump-check previews a PR's release and fails early. setup-verbump just installs the CLI. |
| Any other CI | Four commands: apply, check, tag, notes. Recipe |
Quick start
Install it. The PyPI package is verbump-cli, because the name verbump belongs to an unrelated project. The command it installs is verbump.
pipx install verbump-cli # or: uv tool install verbump-cli, pip install verbump-cli
verbump --version
Declare your packages in verbump.toml at the repo root. verbump init writes a starter file. A minimal one looks like this:
tag_format = "{name}/v{version}"
[packages.api]
path = "services/api"
files = ["pyproject.toml", "src/api/__init__.py"]
[packages.web]
path = "apps/web"
files = ["package.json"]
See what would release. Here a commit changed services/api since its last tag api/v1.4.0, and nothing changed in apps/web:
$ verbump plan --bump minor
api: 1.4.0 -> 1.5.0 (minor, tag api/v1.5.0)
Write the new versions and changelogs. apply changes files only. It does not commit or tag.
$ verbump apply --bump minor --plan-out plan.json
api: 1.4.0 -> 1.5.0 (minor, tag api/v1.5.0)
wrote services/api/pyproject.toml
wrote services/api/src/api/__init__.py
wrote services/api/CHANGELOG.md
The wrote lines go to stderr. Commit the changes, then verbump tag --plan plan.json creates the tags. For a full CI recipe, see Using it in CI.
How it works
merge PR feat/orders ─► trunk-semver: v0.4.0 → v0.5.0 ─► bump = minor
│
verbump: which packages changed since their last tag?
services/api (api/v1.4.0..HEAD touches it) → 1.5.0
apps/web (no changes) → stays 0.3.0
libs/shared (watched by api) → counted for api
│
writes pyproject.toml, __init__.py, services/api/CHANGELOG.md
commit "chore(release): api 1.5.0" → tag api/v1.5.0 → GitHub release "api 1.5.0"
The bump can come from trunk-semver, as drawn, or from --bump or a branch name. The commit, tag and release steps are your CI's job; verbump plans them and writes the files.
- What counts as a change. A package changed if any file under its
pathor itswatchpaths changed since the package's last tag, other than files that matchexclude. A package with no tag yet counts every tracked file under those paths. - Version source. Each package's version files are the source of truth. If the files and the last tag disagree, verbump bumps from the higher of the two. If a package's version files disagree with each other, verbump stops with an error.
- First release. A package with no tag yet is released at the version its files already have. A package with no version files starts from
initial_version(0.0.0) and is bumped. - Changelog. The notes under
## [Unreleased]become## [X.Y.Z] - <date>. If nobody wrote notes, the section lists the subjects of the commits that touched the package since its last tag, leaving outchore(release)and merge commits. A missingCHANGELOG.mdis created. A section for that version that already exists is left alone. - Exact versions. A PR from
release/<package>/X.Y.Zreleases that package at exactly X.Y.Z, changed or not. In a single-package repo,release/X.Y.Zis enough. Use this for a new major, which bumps can't reach. An exact version must be newer than the package's last release. - Safe to re-run. Tags point at the release commit, so a second run finds nothing new to release. If the planned tag already exists, verbump stops instead of moving it. Only the optional major tag (
vX) is moved.
Configuration
verbump.toml at the repo root. Use -c <file> for another path.
tag_format = "{name}/v{version}" # default for every package
changelog = "CHANGELOG.md" # relative to the package path; "" for none
[packages.api]
path = "services/api"
files = ["pyproject.toml", "src/api/__init__.py"]
watch = ["libs/shared"] # other repo paths whose changes release this package
exclude = ["*.md", "docs/*"] # changes only here don't release it
[packages.web]
path = "apps/web"
files = ["package.json"]
Package names use letters, digits, ., _ and -. verbump rejects unknown keys.
Keys
tag_format, changelog and major_tag can be set at the top level as defaults for every package, and again per package.
| Key | Where | Default | Meaning |
|---|---|---|---|
tag_format |
top level, package | {name}/v{version} |
Tag name. Uses {name}, {path} and {version}; {version} appears exactly once. {path} can't be used at the repo root. Two packages can't share a tag. |
changelog |
top level, package | CHANGELOG.md |
Changelog file, relative to the package path. "" turns it off. |
major_tag |
top level, package | false |
Also move a major tag, such as api/v1, on each release. |
commit_message |
top level | chore(release): {releases} |
Commit message in the plan. {releases} becomes api 1.5.0, web 0.3.1. |
path |
package | . |
The package folder, relative to the repo root. It must stay inside the repo. |
files |
package | none | Files that carry the version, relative to path. See Supported files. |
watch |
package | none | Other repo paths whose changes also release this package. |
exclude |
package | none | Glob patterns. Changes only to matching files don't release the package. Patterns match the repo path or the path relative to the package. |
initial_version |
package | 0.0.0 |
Where a package with no tag and no version files starts. |
For a single-package repo, use tag_format = "v{version}" and one package at path = ".". This repo's own verbump.toml is an example. examples/monorepo/verbump.toml is a monorepo with Python, Node, .NET, Maven, Go and Helm packages.
Files as tables
A file can be a table, to pick a kind for any file name or to give your own pattern:
files = [
{ path = "chart/Chart.yaml", kind = "helm" }, # a kind for any file name
{ path = "deploy/values.yaml", # or your own pattern
pattern = 'tag:\s*"(?P<version>\d+\.\d+\.\d+)"',
after = "image:", # search only after this text
replace_all = true }, # rewrite every match, not just the first
]
| Key | Meaning |
|---|---|
path |
The file, relative to the package path. Required. |
kind |
A built-in matcher: json, toml, yaml, helm, plain, python, msbuild, assemblyinfo, maven, properties, gradle, ruby, elixir or dockerfile. |
pattern |
Your own regex. It needs a (?P<version>...) group. It wins over kind. |
after |
Search only after the first occurrence of this text. |
replace_all |
Rewrite every match. By default only the first match is rewritten, and when there are several matches the first is read. |
Supported files
verbump picks the kind of a file from its name, so most configs only list paths. Any other file needs a kind or a pattern.
| Stack | Files | Matches |
|---|---|---|
| Node, PHP, Deno | package.json, composer.json, deno.json, jsr.json, manifest.json |
"version": "X.Y.Z" |
| Python | pyproject.toml |
version = "X.Y.Z" at the start of a line |
| Python | setup.cfg |
version = X.Y.Z |
| Python | *.py |
__version__ = "X.Y.Z" |
| Rust | Cargo.toml |
version = "X.Y.Z" at the start of a line |
| .NET | *.csproj, *.fsproj, *.vbproj, *.props |
<Version> or <VersionPrefix> |
| .NET | AssemblyInfo.cs |
every Assembly[File|Informational]Version("X.Y.Z...") |
| Java, Kotlin | pom.xml |
the project's <version>, after </parent> when there is one |
| Java, Kotlin | build.gradle, build.gradle.kts |
version = "X.Y.Z" or version 'X.Y.Z' |
| Java, Kotlin | gradle.properties |
version=X.Y.Z |
| Ruby | version.rb, *.gemspec |
VERSION = "X.Y.Z", .version = "X.Y.Z" |
| Elixir | mix.exs |
version: "X.Y.Z" |
| Dart, Flutter | pubspec.yaml |
version: X.Y.Z (a +build suffix is kept) |
| Helm | Chart.yaml |
version: X.Y.Z. Set kind = "helm" to move appVersion too. |
| Docker | Dockerfile |
LABEL ... version="X.Y.Z" |
| Any | VERSION, VERSION.txt, version.txt, .version |
the whole file |
| Go, Swift, ... | no file | the version lives only in the tags; use tag_format = "{path}/v{version}" for Go modules |
A v prefix is read in Chart.yaml with kind = "helm" and in plain version files. Versions are always X.Y.Z.
Commands
verbump [-C ROOT] [-c CONFIG] <command>
| Command | What it does |
|---|---|
status [--json] |
Lists each package with its file version, last tag and number of changed files. |
plan [options] |
Shows what would release. Changes nothing. |
apply [options] [--date YYYY-MM-DD] |
Writes versions and changelogs, and prints the plan. Doesn't commit or tag. --date defaults to today, UTC. |
tag --plan FILE |
Creates the annotated tags in a saved plan, and moves its major tags. Doesn't push. |
notes <package> [<version>] |
Prints a version's changelog section, for a release body. Without a version, it uses the package's file version. |
set <package> <version> [--date D] |
Writes an exact version into one package's files and changelog. |
check |
Prints each package's file version. Exits 1 if a package's version files disagree. |
init |
Writes a starter verbump.toml. It won't overwrite an existing one. |
demo [--dir D] [--setup-only] |
A narrated release in a throwaway repo with a Python and a Node package. --dir keeps the repo; --setup-only only builds it and prints its path. |
Global options: -C <repo root> (default .) and -c <config file> (default <root>/verbump.toml).
Choosing the bump
plan and apply take the bump from one of these sources:
--bump major|minor|patch.--previousand--version-from, which are trunk-semver'sprevious-versionandversionoutputs. verbump works out the bump from the two. They go together.--branch <name>, which applies trunk-semver's branch rule without trunk-semver.- Nothing, which means a patch.
--bump and --branch can't be combined. If you pass several sources, --bump wins, then --previous with --version-from, then --branch.
Other options for plan and apply
| Option | Meaning |
|---|---|
--exact NAME=X.Y.Z |
Release a package at exactly this version, changed or not. Repeatable. |
--release-branch <branch> |
If the branch is release/<package>/X.Y.Z (or release/X.Y.Z with one package), it sets --exact. Other branches are ignored. |
--only NAME |
Limit the run to this package. Repeatable. |
--json |
Print the plan as JSON instead of text. |
--plan-out FILE |
Also write the plan as JSON to this file. |
Output
plan and apply print one line per release, as name: previous -> new (reason, tag T). The previous version is - for a package with no tag. With nothing to release they print nothing to release. The reason is the bump, first release or exact.
The plan JSON has the shape {"bump", "commit_message", "releases": [{"name", "path", "previous", "version", "tag", "major_tag", "changelog", "reason", "changed"}]}.
Using it in CI
In GitHub Actions there are three actions, each in its own repo:
| Action | Use it to |
|---|---|
TheKathan/verbump-release@v1 |
release on merge: apply the bump, commit, tag, push, and publish a GitHub release per package |
TheKathan/verbump-check@v1 |
preview on a PR what merging it will release, with the release notes, and fail early if that release would fail |
TheKathan/setup-verbump@v1 |
put verbump on the PATH for your own steps, without touching the job's Python |
# on push to main
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: TheKathan/verbump-release@v1
The release action takes the bump as an input from any source: trunk-semver's outputs, the merged PR's branch name (the default), or a fixed value. It doesn't run trunk-semver itself.
Anywhere else, the CLI does the same work. This recipe takes the bump from the merged PR's branch name:
verbump apply --branch "$PR_BRANCH" --release-branch "$PR_BRANCH" --plan-out plan.json
verbump check
git commit -am "$(jq -r .commit_message plan.json)"
verbump tag --plan plan.json
git push origin HEAD:main --follow-tags
--follow-tags pushes the annotated tags. Major tags are lightweight and moved, so push them yourself, for example with git push --force origin <major_tag>. To create a GitHub release for each package, loop over the plan and use verbump notes <name> <version> as the release body.
This repo releases itself with .github/workflows/release.yml, running verbump from its own source. It runs in this order:
- Run trunk-semver against a local mirror, so its repo-wide tag never reaches GitHub, and take the bump from its
previous-versionandversion. When it gives no previous version, use the PR's branch name. - Run
verbump applyandverbump check. - Commit, tag and push.
- Publish the GitHub release.
- Build the sdist and the wheel.
- Upload them to PyPI, when the
PUBLISH_PYPIvariable istrue.
Contribute
Bugs, questions and ideas go to issues, and pull requests are welcome: see CONTRIBUTING and the code of conduct. Issues about one of the GitHub Actions go to its repo: verbump-release, verbump-check or setup-verbump. If verbump saves you a release chore, a star helps others find it.
CHANGELOG · SECURITY · Publishing · LICENSE (MIT)
Metadata
Release files for verbump-cli 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| verbump_cli-1.0.2.tar.gz | 31.6 kB | With provenance | Details for verbump_cli-1.0.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| verbump_cli-1.0.2-py3-none-any.whl | Python 3 | none | any | Details for verbump_cli-1.0.2-py3-none-any.whl |
Total release size: 55.1 kB
Release files / verbump_cli-1.0.2.tar.gz
| Download URL | verbump_cli-1.0.2.tar.gz |
|---|---|
| Size | 31.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
95e4ae0800c728c3d1b6122000f260d222ecde662c9c6a70cf59cfc6a71bb261
|
|
BLAKE2b-256 checksum How to use checksums |
47ef6674e78ec9c40626ebeaab98d5e371927c1169b336f364d702dd0ceb6706
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logRelease files / verbump_cli-1.0.2-py3-none-any.whl
| Download URL | verbump_cli-1.0.2-py3-none-any.whl |
|---|---|
| Size | 23.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d9bf35232c1c99cdabdb735dfa9ff04401a4e86cff2365b9f4daf141a819abf6
|
|
BLAKE2b-256 checksum How to use checksums |
bde07e1f414f1a984519e975a82d8f85156a1b49c7eb107c0f1e65302a40d860
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logSecurity for verbump-cli 1.0.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for verbump_cli-1.0.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.