One command. Any language. Versions that live in git — not in your way.
pip install vmn
vmn stamp -r minor my_app # => 0.1.0
vmn stamp -r patch --pr rc my_app # => 0.1.1-rc.1
vmn release my_app # => 0.1.1
vmn goto -v 0.1.0 my_app # entire repo + deps restored to 0.1.0That last line? No other versioning tool can do that.
Why vmn? · Only in vmn · Commands · Auto-Embedding · Configuration · CI · Migration · Contributing
- You version projects in Python, Rust, Go, C++, Java — or any language (not just JavaScript)
- You manage microservices and need coordinated versions across services
- You work across multiple repos and need reproducible cross-repo snapshots
- You're tired of configuring plugins for semantic-release or release-please
- You want versions stored in pure git tags — zero lock-in, zero databases, zero SaaS
- You need to work offline, in air-gapped environments, or without CI
No separate vmn init required — vmn stamp auto-initializes on first run. Works with shallow clones (fetch-depth: 1).
Try it locally (playground)
pip install vmn
mkdir remote && cd remote && git init --bare && cd ..
git clone ./remote ./local && cd local
echo a >> ./a.txt && git add ./a.txt && git commit -m "first commit" && git push origin master
vmn stamp -r patch my_app # => 0.0.1
echo b >> ./a.txt && git add ./a.txt && git commit -m "feat: add b" && git push origin master
vmn stamp -r patch my_app # => 0.0.2vmn does everything semantic-release and release-please do — plus 6 things nothing else can.
| Capability | vmn | semantic-release | release-please | changesets |
|---|---|---|---|---|
| Language-agnostic | ✅ | JS-centric | JS-centric | JS-only |
| Git-tag source of truth | ✅ | ✅ | ✅ | ❌ |
| Conventional commits + changelog | ✅ | ✅ | ✅ | ❌ / ✅ |
| GitHub Release creation | ✅ | ✅ | ✅ | ❌ |
| Auto-embed version (npm, Cargo, pyproject, any file) | ✅ | per-plugin | ❌ | JS only |
| Multi-repo dependency tracking | ✅ | ❌ | ❌ | ❌ |
State recovery (vmn goto) |
✅ | ❌ | ❌ | ❌ |
| Microservice / root app topology | ✅ | ❌ | ❌ | monorepo only |
| 4-segment hotfix versioning | ✅ | ❌ | ❌ | ❌ |
| Zero-config start (auto-init) | ✅ | ❌ | ❌ | ❌ |
| Offline / local file backend | ✅ | ❌ | ❌ | ❌ |
| Zero lock-in (pure git tags) | ✅ | ❌ | ❌ | ❌ |
Bold rows = only vmn. That's the moat.
Detailed comparisons & migration guides
Your QA team reports a bug in v1.2.3. Instead of digging through git log, you run one command:
vmn goto -v 1.2.3 my_appYour entire repository — plus every tracked dependency — is now at exactly the state when 1.2.3 shipped. Reproduce the bug in seconds, not hours. No other versioning tool offers this.
If your product spans multiple git repos, vmn records the exact commit hash of every dependency at stamp time. vmn goto restores all of them in parallel:
# stamp records: my_app @ abc123, lib_core @ def456, lib_utils @ 789fed
vmn stamp -r minor my_app
# six months later — restore everything to that exact state
vmn goto -v 0.1.0 my_app # all 3 repos checked out to their recorded commitsVersion multiple services under one root app. Each service has its own semver; the root app gets an auto-incrementing integer on every service stamp:
vmn stamp -r patch my_platform/auth # auth => 0.0.1, root => 1
vmn stamp -r minor my_platform/billing # billing => 0.1.0, root => 2
vmn stamp -r patch my_platform/auth # auth => 0.0.2, root => 3
vmn show --root my_platform # => 3 (latest root version)All version state lives in annotated git tag messages. Uninstall vmn tomorrow and your tags still make perfect sense. No config files, databases, or SaaS dependencies hold your versions hostage.
Standard Semver 2.0 plus an optional 4th hotfix segment for when you need it:
1.6.0 · 1.6.0-rc.23 · 1.6.7.4 · 1.6.0-rc.23+build01.Info
init-appandstampboth support--dry-runfor safe testing.
| Command | What it does | Example |
|---|---|---|
vmn stamp |
Create a new version | vmn stamp -r patch my_app |
vmn release |
Promote prerelease to final | vmn release my_app |
vmn show |
Display version info | vmn show my_app |
vmn goto |
Checkout repo at a version | vmn goto -v 1.2.3 my_app |
vmn gen |
Generate file from template | vmn gen -t ver.j2 -o ver.txt my_app |
vmn add |
Attach build metadata | vmn add -v 1.0.0 --bm build42 my_app |
vmn config |
Edit app config (TUI) | vmn config my_app |
vmn init |
Initialize repo/app | vmn stamp auto-inits — rarely needed |
vmn stamp -r patch my_app # => 0.0.1
vmn stamp -r minor my_app # => 0.1.0
vmn stamp -r patch --pr rc my_app # => 0.0.2-rc.1 (prerelease)
vmn stamp my_app # => 0.0.3 (with conventional_commits — no -r needed)
vmn stamp --dry-run -r patch my_app # preview without committing
vmn stamp --pull -r patch my_app # pull before stamping (retries on conflict)Idempotent (won't re-stamp if current commit already matches). Auto-initializes repo/app on first run.
Stamping without -r, -r vs --orm, and all flags
Without -r: works only during a prerelease sequence — continues the existing prerelease. If the current version is a release, omitting -r errors out. Exception: with conventional_commits enabled, -r can always be omitted.
vmn stamp -r patch --pr rc my_app # 0.0.2-rc.1
vmn stamp --pr rc my_app # 0.0.2-rc.2 (continues)
vmn stamp my_app # 0.0.2-rc.3 (still continues)-r vs --orm:
| Flag | Behavior |
|---|---|
-r patch |
Strict — always advances. 0.0.1 -> 0.0.2. 0.0.2-rc.3 -> 0.0.3. |
--orm patch |
Optional — advances only if no prerelease exists at target. 0.0.2-rc.1 exists -> 0.0.2-rc.2. |
--orm is useful in CI to bump on new work but continue an existing prerelease series.
All flags:
| Flag | Description |
|---|---|
-r, --release-mode |
major / minor / patch / hotfix (also micro for hotfix) |
--orm, --optional-release-mode |
Like -r but only advances if no prerelease at target |
--pr, --prerelease |
Prerelease identifier (e.g. alpha, rc, beta.1) |
--dry-run |
Preview without committing or pushing |
--pull |
Pull remote first; retries on push conflict (3 retries, 1-5s delay) |
-e, --extra-commit-message |
Append text to commit message (e.g. [skip ci]) |
--ov, --override-version |
Force a specific version string |
--orv, --override-root-version |
Force root app version to a specific integer |
--dont-check-vmn-version |
Skip vmn binary version check |
vmn init-app flags:
| Flag | Description |
|---|---|
-v, --version |
Version to start from (default: 0.0.0) |
--dry-run |
Preview without committing |
--orm, --default-release-mode |
Set default_release_mode in app config: optional (default) or strict |
Enable conventional_commits and never type -r again — vmn reads your commit messages (fix: → patch, feat: → minor, BREAKING CHANGE / ! → major) and picks the release mode automatically:
# with conventional_commits enabled:
git commit -m "feat: add search endpoint"
vmn stamp my_app # => 0.2.0 (minor, auto-detected from "feat:")Configure per-app in .vmn/<app-name>/conf.yml:
conf:
conventional_commits: true # auto-detect release mode — no -r flag needed
default_release_mode: optional # "optional" (--orm behavior) or "strict" (-r behavior)
changelog: # generate CHANGELOG.md on each stamp (requires conventional_commits)
path: "CHANGELOG.md"
github_release: # create GitHub Release after push (requires gh CLI + GITHUB_TOKEN)
draft: falsePromote a prerelease to its final version. Three modes:
vmn release -v 0.0.1-rc.1 my_app # explicit version — tag-only, works from anywhere
vmn release my_app # auto-detect from current commit (must be on version commit)
vmn release --stamp my_app # full stamp flow — new commit + tag + pushIdempotent. -v and --stamp are mutually exclusive.
vmn show my_app # current version from HEAD
vmn show -v 0.0.1 my_app # specific version info
vmn show --verbose my_app # full YAML metadata dump
vmn show --raw my_app # without template formatting
vmn show --root my_root_app # root app version (integer)
vmn show --type my_app # release / prerelease / metadata
vmn show -u my_app # unique ID (version+commit_hash)
vmn show -t '[{major}]' my_app # override display template
vmn show --conf my_app # show app configuration
vmn show --ignore-dirty my_app # ignore dirty working tree
vmn show --from-file my_app # read from local verinfo file (requires create_verinfo_files)Checkout the repo (and all tracked dependencies) to the exact state at a version:
vmn goto -v 1.2.3 my_app # repo + deps restored to version 1.2.3
vmn goto my_app # latest version on current branch
vmn goto -v 1.2.3 --deps-only my_app # only checkout dependencies
vmn goto -v 5 --root my_root_app # checkout to root app version
vmn goto --pull -v 1.2.3 my_app # pull remote before checking outDependencies are auto-cloned if missing (up to 10 in parallel).
Generate a version file from a Jinja2 template:
vmn gen -t version.j2 -o version.txt my_app # current HEAD version
vmn gen -t version.j2 -o version.txt -v 1.0.0 my_app # specific version
vmn gen -t version.j2 -o version.txt -c custom.yml my_app # with custom template values
vmn gen -t version.j2 -o version.txt --verify-version my_app # verify version existsSee Template Variables for available Jinja2 variables.
Attach build metadata to an existing version:
vmn add -v 0.0.1 --bm build42 my_app # => 0.0.1+build42
vmn add -v 0.0.1 --bm build42 --vmp meta.yml my_app # attach metadata file
vmn add -v 0.0.1 --bm build42 --vmu https://ci/42 my_app # attach metadata URLvmn config # list all managed apps in the repo
vmn config my_app # interactive TUI editor
vmn config my_app --vim # open in $EDITOR
vmn config --branch my_app # edit/create branch-specific config for current branch
vmn config --root my_app # edit root app config (root_conf.yml)
vmn config --global # repo-level .vmn/conf.ymlUsually not needed — vmn stamp auto-initializes. Use for explicit control:
vmn init-app my_app # starts from 0.0.0
vmn init-app -v 1.6.8 my_app # start from a specific versionfrom contextlib import redirect_stdout, redirect_stderr
import io, version_stamp.vmn as vmn
out, err = io.StringIO(), io.StringIO()
with redirect_stdout(out), redirect_stderr(err):
ret, vmn_ctx = vmn.vmn_run(["show", "vmn"])| Variable | Description |
|---|---|
VMN_WORKING_DIR |
Override working directory |
VMN_LOCK_FILE_PATH |
Custom lock file path (default: per-repo) |
GITHUB_TOKEN / GH_TOKEN |
Required for GitHub Releases |
Iterate on prereleases, then promote:
vmn stamp -r major --pr alpha my_app # 2.0.0-alpha.1
vmn stamp --pr alpha my_app # 2.0.0-alpha.2
vmn stamp --pr mybeta my_app # 2.0.0-mybeta.1
vmn release my_app # 2.0.0Template variables for vmn gen
{
"version": "0.0.1",
"base_version": "0.0.1",
"name": "test_app2/s1",
"release_mode": "patch",
"prerelease": "release",
"previous_version": "0.0.0",
"stamped_on_branch": "main",
"release_notes": "",
"changesets": {".": {"hash": "d637...", "remote": "...", "vcs_type": "git"}},
"root_name": "test_app2",
"root_version": 1,
"root_services": {"test_app2/s1": "0.0.1"}
}Example template (version.j2):
VERSION: {{version}}
NAME: {{name}}
BRANCH: {{stamped_on_branch}}
{% for k,v in changesets.items() %}
REPO: {{k}} | HASH: {{v.hash}} | REMOTE: {{v.remote}}
{% endfor %}
vmn stamp can automatically write the version into your project files:
| Backend | File | What it updates |
|---|---|---|
| npm | package.json |
version field |
| Cargo | Cargo.toml |
version field |
| Poetry | pyproject.toml |
[tool.poetry].version |
| PEP 621 | pyproject.toml |
[project].version |
Hatchling / uv — dynamic versioning from git tags
Use hatch-vcs to read vmn's tags directly (zero file injection). Works with multiple apps per repo.
uv add --dev hatch-vcs # or: pip install hatch-vcs[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "my-package"
dynamic = ["version"]
[tool.hatch.version]
source = "vcs"
tag-pattern = "my_app_(?P<version>.*)" # replace my_app with your app name
# For root apps: "my_root_app-service1_(?P<version>.*)" (/ becomes -)Or use the pep621 backend to write a static version into [project].version on each stamp instead.
Generic backends — regex or Jinja2 for any file format
generic_selectors — regex find-and-replace:
version_backends:
generic_selectors:
- paths_section:
- input_file_path: in.txt
output_file_path: in.txt
selectors_section:
- regex_selector: '(version: )(\d+\.\d+\.\d+)'
regex_sub: \1{{version}}Use {{VMN_VERSION_REGEX}} to match any vmn version string (playground).
generic_jinja — Jinja2 template rendering:
version_backends:
generic_jinja:
- input_file_path: f1.jinja2
output_file_path: jinja_out.txt
custom_keys_path: custom.yml # optionalSame variables as vmn gen.
vmn auto-generates .vmn/<app-name>/conf.yml per app. Full example with inline docs:
conf:
template: '[{major}][.{minor}]' # display format ({major}, {minor}, {patch}, {hotfix}, {prerelease}, {rcn}, {buildmetadata})
hide_zero_hotfix: true # hide 4th segment when 0 (default: true)
conventional_commits: true # auto-detect release mode from commits
default_release_mode: optional # "optional" (--orm) or "strict" (-r) for auto-detected mode
changelog:
path: "CHANGELOG.md" # generate changelog on stamp (presence enables it)
github_release:
draft: false # create GitHub Release on stamp (presence enables it)
deps: # multi-repo dependencies — tracked on stamp, restored on goto
../:
<repo dir name>:
vcs_type: git
version_backends: # auto-embed version into project files
npm:
path: "relative_path/to/package.json"
policies:
whitelist_release_branches: ["main"] # restrict which branches can stamp/release
extra_info: false # include host/environment metadata in stamp
create_verinfo_files: false # enable vmn show --from-fileUse vmn config <app-name> for a TUI editor, or edit YAML directly.
vmn supports branch-specific config overrides. Place a file named <branch>_conf.yml next to the default conf.yml in .vmn/<app-name>/:
.vmn/my_app/
conf.yml # fallback when no branch-specific file exists
feature-x_conf.yml # used when on branch "feature-x"
When vmn loads configuration it checks for <active_branch>_conf.yml first; if that file exists it is used instead of conf.yml. The same applies to root app configs (<branch>_root_conf.yml vs root_conf.yml).
During vmn stamp, branch-specific config files from other branches are automatically cleaned up so they don't accumulate in the repository.
Use the official GitHub Action — progovoy/vmn-action:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # recommended; vmn handles shallow clones but full is faster
- uses: progovoy/vmn-action@latest
with:
app-name: my_app
do-stamp: true
stamp-mode: patch
env:
GITHUB_TOKEN: ${{ github.token }}Migration takes 5 minutes:
- Migrating from semantic-release
- Migrating from release-please
- Migrating from setuptools-scm
- Migrating from standard-version
- Migrating from bump2version
pip install vmnStar the repo if vmn saved you time. File an issue if it didn't — we'll fix it.
Add the badge to your project:
[](https://github.com/progovoy/vmn)