git checkout for releases that span many repos.
Record a release once. Put the app and every repo it depends on back to exactly that state with one command.
No server. No database. Any language.
Production broke after last Tuesday's 2.1.0 deploy. Your product is four repos. Which commit of each one actually shipped? Without vmn, that is an afternoon of CI logs and container tags. With vmn:
vmn goto -v 2.1.0 my_platform # every repo back at the commit that shippedvmn stores release metadata as readable YAML in annotated Git tags: the application revision, every dependency's revision and remote, the previous version, and release context. There is no vmn server and no external metadata database.
Developed continuously since 2019, vmn is used in daily production workflows by teams at large companies managing multi-repository products. vmn versions its own releases. The repository contains more than 400 tests, including Docker-backed multi-repository, recovery, and compatibility scenarios.
If vmn saves you an afternoon, a ⭐ helps other teams find it.
Quick start · Why vmn · Multi-repository recovery · Release models · Configuration · Operations · Commands · Documentation
| Requirement | What vmn provides |
|---|---|
| Recover a recorded multi-repository source state | vmn goto restores the application and its configured dependencies to their recorded Git revisions. |
| Keep release data inspectable | Annotated tags contain readable YAML and use the namespaced form <app>_<version>. |
| Version mixed technology stacks | vmn operates on Git repositories, not a language-specific package manager or build system. |
| Release services independently | Root apps group independently versioned services under a monotonic composition version. |
| Work without a hosted control plane | A standard Git remote is enough; internal and air-gapped Git servers are supported. |
| Adopt without replacing build tooling | Version backends update npm, Cargo, Poetry, PEP 621, Jinja2, or regex-selected files. |
A version is a handle to recorded source state, not only a string:
| State | Command | Captures |
|---|---|---|
| Release | vmn stamp → vmn goto |
Committed application and dependency revisions |
| Working | vmn snapshot |
Release state plus local commits, tracked changes, and untracked files |
Scope: vmn restores recorded source revisions. It does not rebuild artifacts, capture toolchains or runtime infrastructure, sign tags, or deploy software. Keep those responsibilities in your build, signing, and deployment pipeline.
Requirements: Python 3.8+, Git 2.10+ (2.17+ recommended), and a Git repository with at least one commit and a writable remote.
pipx install vmn # or: uv tool install vmn
vmn --completion-install # optional: bash/zsh/fish/tcsh, auto-detectedInside any Git repository:
vmn stamp -r patch my_app # 0.0.1; initializes the repo and app on first use
vmn show my_app # 0.0.1
# After committing the next change:
vmn stamp -r minor my_app # 0.1.0
# After committing another change:
vmn stamp -r patch --pr rc my_app # 0.1.1-rc.1
vmn release my_app # 0.1.1A stamp creates a version commit and annotated tags, then pushes the branch and
tags. --dry-run previews it. Stamping an already-versioned state is
idempotent. No separate vmn init is needed; init and init-app -v <version>
remain for migrations and non-default starting versions.
Inspect the source of truth directly:
git tag --list 'my_app_*'
git cat-file -p my_app_0.1.0
vmn show --verbose my_appDeclare dependency repositories in .vmn/my_app/conf.yml, keyed by their
directory relative to the app repository:
conf:
deps:
../:
lib_core:
vcs_type: git
service_api:
vcs_type: git
branch: main # optional pin, checked before every stampEvery stamp records each dependency's exact revision and remote. Later, from any revision:
vmn goto -v 1.4.0 my_appgoto checks out every recorded repository and clones any that are missing.
--pull fetches first when the version is not available locally;
--deps-only leaves the application repository unchanged; without -v it
returns to the tip of the current branch.
Dependency keys
| Key | Meaning |
|---|---|
vcs_type |
git |
remote |
Clone URL; auto-detected from the existing checkout when omitted |
branch |
Stamping requires the dep to be on this branch |
tag |
Stamping requires the dep to be at this tag |
hash |
Stamping requires the dep to be at this commit |
A dependency with uncommitted changes blocks stamp. Do not embed credentials
in remote URLs: dependency remotes are part of release metadata. Use SSH, a Git
credential helper, or vmn's per-command push credentials.
vmn wt (alias of vmn worktrees) builds an island: git worktrees for the
application and every dependency, laid out like the originals, plus an
island.json manifest. Each checkout starts on a private
island/<name>/<branch> branch that follows its source branch but cannot be
pushed; stamping is refused on it.
vmn wt create my_app --island-name feat # from the current commits
vmn wt create my_app --island-name feat --carry-changes # ...plus uncommitted work
vmn wt pull # rebase onto the source branches
vmn wt freeze my_app # pin deps to the branches they are on
vmn wt remove featTo share the work, check out a real branch in each repo you changed
(git checkout -b feature/x && git push -u origin feature/x) and run
vmn wt freeze in the application. It pins those dependency branches in the
current branch's conf, so a colleague who checks out feature/x and runs
vmn wt create gets the same dependency state. -fv 2.1.0 builds an island at
a recorded version instead.
1.6.0 release
1.6.0-rc.23 prerelease
1.6.7.4 optional fourth hotfix segment
1.6.0-rc.23+build01 build metadata (vmn add)
1.6.0-dev.a1b2c3d.e4f5g6h recorded working state (a snapshot)
How the release mode is chosen, first match wins:
-r <mode>(strict: always bumps) or--orm <mode>(optional: only advances if no prerelease exists at the target) on the command line.- Conventional Commits since the last version — on by default:
fix:→ patch,feat:→ minor,type!:or aBREAKING CHANGEfooter → major. default_release_modefrom conf.yml.
Modes from 2 and 3 are applied as --orm or -r per release_mode_policy
(optional by default, or strict). During a prerelease sequence, vmn stamp --pr rc my_app needs no mode at all.
For independently deployed services, use a root app:
vmn stamp -r patch platform/auth # auth 0.0.1; platform 1
vmn stamp -r minor platform/billing # billing 0.1.0; platform 2
vmn show --root platform # 2Per-app configuration lives in .vmn/<app>/conf.yml under a top-level conf:
key; root apps have .vmn/<root>/root_conf.yml. Edit it with the TUI
(vmn config my_app, --vim for $EDITOR) or create it non-interactively
with vmn config gen my_app.
conf:
release_mode_policy: optional
changelog:
path: CHANGELOG.md
github_release:
draft: true
policies:
whitelist_release_branches: [main]
version_backends:
pep621:
path: pyproject.tomlAll conf.yml keys
| Key | Default | Meaning |
|---|---|---|
template |
[{major}][.{minor}][.{patch}][.{hotfix}][-{prerelease}][.{rcn}][-dev.{dev_commit}.{dev_diff_hash}][+{buildmetadata}] |
Display format; [...] sections drop out when their field is empty |
hide_zero_hotfix |
true |
Show 1.2.3 rather than 1.2.3.0 |
conventional_commits |
true |
Detect the release mode from commit messages |
release_mode_policy |
optional |
Apply a detected/default mode as --orm (optional) or -r (strict) |
default_release_mode |
unset | major/minor/patch/hotfix fallback when nothing else resolves a mode |
changelog |
unset | {path: CHANGELOG.md}: insert a Conventional-Commits entry below the file's title on each stamp |
github_release |
unset | {draft: true|false}: create a GitHub Release on stamp (body from the changelog entry, else the commits); needs gh and GITHUB_TOKEN/GH_TOKEN; best-effort, warns instead of failing |
policies.whitelist_release_branches |
unset | Branches allowed to stamp non-prerelease versions and to vmn release |
deps |
the app repo only | Dependency repositories (see Dependency keys) |
version_backends |
none | Files to write the version into (below) |
create_snapshots |
false |
Also write a version file per stamp, readable with vmn show --from-file |
extra_info |
false |
Record host/environment information in the stamp metadata |
experiment |
none | vmn-exp settings (storage URI, metrics, alerts); ignored by core vmn |
Root apps: root_conf.yml accepts external_services, recorded in each root
version. Deprecated keys (create_verinfo_files, default_release_mode: optional|strict) are migrated automatically.
Version backends
| Backend | Writes |
|---|---|
npm: {path: package.json} |
version |
cargo: {path: Cargo.toml} |
package.version |
poetry: {path: pyproject.toml} |
tool.poetry.version |
pep621: {path: pyproject.toml} |
project.version |
generic_jinja |
Renders Jinja2 templates to output files |
generic_selectors |
Regex search-and-replace inside existing files |
conf:
version_backends:
generic_jinja:
- input_file_path: version.py.j2
output_file_path: mypkg/_version.py
custom_keys_path: custom.yml # optional extra template values
generic_selectors:
- paths_section:
- input_file_path: chart/Chart.yaml
output_file_path: chart/Chart.yaml
selectors_section:
- regex_selector: '(version: ){{VMN_VERSION_REGEX}}'
regex_sub: '\1{{version}}'In regex_selector, {{VMN_VERSION_REGEX}} expands to a pattern matching any
vmn version; it has capture groups of its own, so in regex_sub refer only to
groups placed before it. Templates see the stamp metadata (version, base_version,
changesets, root_* for root apps, ...), plus release_notes (generated by
the bundled git-cliff, only when the template uses it). vmn gen -t <tmpl> -o <out> my_app renders the same data on demand.
Integration branches can override configuration, typically dep pins, without touching the main conf:
vmn config gen my_app --branch # seeded from the effective conf
vmn config gen my_app --branch --sync-dep-branches # pin deps to their checked-out branches
vmn config my_app --branch # edit interactivelyThe canonical layout is .vmn/<app>/branch_conf/<branch>/conf.yml (slashes in
the branch name become directories; root apps use root_conf.yml). Legacy
<branch>_conf.yml files are still read and are migrated on the next stamp.
--dry-runpreviews a stamp without committing or tagging.- Dirty, detached, outgoing, and dependency states are checked before release.
- A per-repository lock prevents concurrent local vmn operations.
- Release-branch allowlists restrict stable stamps to configured branches.
--pullfetches remote state and retries version conflicts.- vmn rolls back newly created local release state when publication fails.
--git-push-user/--git-push-token(orVMN_GIT_PUSH_USER/_TOKEN) authenticate the push through an ephemeral HTTPS URL; the remote config is never modified.- No internet access is required when an internal or local Git remote is used.
For GitHub Actions, use vmn-action:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: vmn
uses: progovoy/vmn-action@latest
with:
app-name: my_app
do-stamp: true
stamp-mode: patch
env:
GITHUB_TOKEN: ${{ github.token }}
- run: echo "Stamped ${{ steps.vmn.outputs.verstr }}"Elsewhere, fetch complete history and tags, serialize stamps for the same app, and give the job write access to the remote:
pip install vmn
vmn stamp --pull -r patch my_appStart an established migration with --dry-run, then add branch policy before
enabling automatic stamps.
Between releases, save and restore your exact working state — uncommitted changes, local commits, and untracked files, across every dependency — as a named version, without committing:
vmn snapshot create my_app --note "parser refactor" # prints 1.2.0-dev.a1b2c3d.e4f5g6h
vmn snapshot list my_app --last 5
vmn snapshot diff my_app -v @2 # vs your working tree; --to <ref|version>
vmn snapshot restore my_app --latest # your current work is auto-saved firstActions are create (default), list, show, note, delete, restore,
export and diff. The app must be stamped once first. With vmn-exp
installed, --store <uri> keeps snapshots in a shared experiment store and
vmn goto -v <snapshot> restores them. See
docs/snapshots.md.
vmn-exp is a separate, optional product built on vmn: it records training and
evaluation runs as working-state versions with their metrics, params and
artifacts, plus a model registry, sweeps and a web dashboard.
pip install vmn-exp # "vmn-exp[ui]" adds the dashboardSee the vmn-exp documentation.
Coming from vmn 0.10 or earlier: vmn exp/model/ui are now vmn-exp …
(packaging).
vmn skill gives AI coding agents the context they need to use vmn correctly:
vmn skill # print the skill block
vmn skill --install # .claude/skills/vmn/SKILL.md (--force overwrites)
vmn skill --install --target cursor # .cursorrules
vmn skill --install --target agents # AGENTS.mdRe-running --install for cursor/agents updates only vmn's section. Optional,
opinionated development rules for agents (TDD, worktrees, minimal diffs, ...)
live in docs/agent-methodology.md.
| Command | Purpose |
|---|---|
vmn stamp |
Compute, create, and publish a version |
vmn release |
Promote a prerelease to a final release |
vmn show |
Read version, status, or effective configuration |
vmn goto |
Restore recorded application and dependency revisions |
vmn snapshot |
Capture, inspect, compare, export, or restore working state |
vmn worktrees (wt) |
Islands: worktrees of the app and its deps (create, list, pull, freeze, remove) |
vmn add |
Attach build metadata to an existing version |
vmn gen |
Render a file from a Jinja2 template |
vmn config |
List apps, or edit global, app, root-app, and branch configuration |
vmn skill |
Output or install the AI agent skill block |
vmn init / vmn init-app |
Explicit initialization (optional; stamp auto-inits) |
Flags
vmn stamp <app>
| Flag | Meaning |
|---|---|
-r, --release-mode |
major/minor/patch/hotfix; always bumps |
--orm, --optional-release-mode |
Bump only if no prerelease already exists at the target |
--pr, --prerelease <id> |
Prerelease, e.g. rc → 0.0.1-rc.1 |
--ov, --override-version <v> |
Bump from <v> instead of the current version (--ov 1.0.0 -r patch → 1.0.1) |
--orv, --override-root-version <n> |
Bump the root app from <n> |
--pull |
Pull first; retry on a version conflict |
--dry-run |
Preview without committing, tagging or pushing |
-e, --extra-commit-message <s> |
Append to the version commit message (e.g. [ci skip]) |
--dont-check-vmn-version |
Skip the check that this vmn is not older than the one that stamped last |
--git-push-user, --git-push-token |
Push credentials (both required) |
vmn release <app>: tags the prerelease's commit as the final version
and pushes the tag. -v <version> names the prerelease (default: the one at
HEAD); -s, --stamp instead runs the full stamp flow (new commit, backends
updated). Takes --git-push-user/--git-push-token.
vmn show <app>
| Flag | Meaning |
|---|---|
-v <version> |
Show a specific version instead of the current one |
--verbose |
Full stamp metadata as YAML |
--raw |
Version without the template applied |
-t, --template <t> |
Format with a different template |
--root |
Root app version |
--type |
Release type (release or the prerelease id) |
-u, --unique |
Version plus the commit hash |
--dev |
Working-state version of a dirty tree |
--conf |
Effective configuration |
--from-file |
Read from .vmn/ files instead of git (with create_snapshots) |
--ignore-dirty |
Do not report dirty states |
vmn goto <app>: -v <version> (default: tip of the current branch),
--root (-v is a root version), --deps-only, --pull, --force (dev
versions: restore even if oversized untracked files would be lost).
vmn add <app>: --bm, --buildmetadata <s> (required), -v <version>
(default: the version at HEAD), --vmp, --version-metadata-path <yml>,
--vmu, --version-metadata-url <url>.
vmn gen <app>: -t, --template <j2> and -o, --output <file>
(required), -v <version>, -c, --custom-values <yml>, --verify-version
(refuse on a dirty tree or when HEAD is not at the version).
vmn config [gen] [app]: no app lists managed apps; --vim ($EDITOR),
--root (root_conf.yml), --global (.vmn/conf.yml), --branch,
--sync-dep-branches (with gen --branch). gen never overwrites.
vmn worktrees [create|list|pull|freeze|remove] [name]: create (the
default) takes --island-name, -fv, --from-version, -fb, --from-branch,
--base-path (default ../vmn-islands), --shallow-deps, --carry-changes.
vmn init-app <app>: -v <version> (start from, default 0.0.0),
--dry-run, --orm optional|strict (sets release_mode_policy).
vmn snapshot: see docs/snapshots.md.
Global: --version, --debug, --completion [SHELL],
--completion-install [SHELL], --completion-uninstall [SHELL].
Environment variables
| Variable | Effect |
|---|---|
VMN_WORKING_DIR |
Run as if started in this directory |
VMN_LOCK_FILE_PATH |
Lock file path (default .vmn/vmn.lock) |
VMN_GIT_PUSH_USER / VMN_GIT_PUSH_TOKEN |
Fallbacks for --git-push-user / --git-push-token |
GITHUB_TOKEN / GH_TOKEN |
Needed for github_release |
VMN_SNAPSHOT_MAX_FILE_MB / VMN_SNAPSHOT_MAX_TOTAL_MB |
Caps on untracked files captured into a snapshot (default 50 / 200) |
EDITOR |
Editor for vmn config --vim (default vim) |
- Working-state snapshots
- AI agent skill reference
- Packaging and installation
- vmn vs semantic-release
- vmn vs release-please
- vmn vs setuptools-scm
- Migrating from standard-version
- Migrating from bump2version
- vmn-exp (experiment tracking)
vmn is open source under the MIT License. Issues, questions, and pull requests are welcome; see the contributing guide and the issue tracker.
