Automatic semantic versioning powered by git tags. Zero lock-in.
Language-agnostic CLI for versioning, multi-repo state recovery, and local-first experiment tracking -- versions live in git tags, experiments live on disk (or S3), no server required.
pip install vmn
vmn stamp -r patch my_app # => 0.0.1 (auto-initializes repo + app)
vmn stamp -r minor my_app # => 0.1.0
vmn stamp -r patch --pr rc my_app # => 0.1.1-rc.1 (prerelease)
vmn release my_app # => 0.1.1
vmn goto -v 0.1.0 my_app # repo restored to exact 0.1.0 stateVersions live in git annotated tags. Uninstall vmn and the tags still make sense. No databases, no SaaS, no ecosystem buy-in.
Requirements Β· Quick Start Β· Why vmn? Β· Only in vmn Β· Experiments Β· Web UI Β· Snapshots Β· Commands Β· Auto-Embedding Β· Configuration Β· CI Β· Troubleshooting Β· Migration
| Any language β Python, Rust, Go, C++, Java, JS, or anything with a git repo | Microservices β independent versions per service, one root counter |
| Multi-repo β reproducible state recovery across repositories | Zero config β no plugins, no pipelines, no ecosystem buy-in |
| Offline / air-gapped β works without network access | Zero lock-in β versions live in plain git tags |
| ML experiments β reproducible snapshots with metrics, no tracking server |
No separate vmn init required -- vmn stamp auto-initializes on first run. Works in CI (handles shallow clones automatically).
git commit β vmn stamp β git tag β git push
vmn stores all version state in git annotated tag messages as plain YAML. When you vmn stamp, it computes the next version, writes it into a tag (e.g., my_app_1.2.0), and optionally pushes. When you vmn show, it reads that tag. There is no database, no config service, no proprietary format -- just git tags you can inspect with git tag -n1.
Try it locally (30 seconds):
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.2
git tag -n1 my_app_0.0.2 # version metadata right there in the tag- Python 3.8+
- Git 2.10+ (for push options support; 2.17+ recommended)
- Platforms: Linux, macOS, Windows (including WSL). No platform-specific configuration needed -- vmn uses GitPython for cross-platform git operations.
pip install vmn
cd your-project # any git repo
vmn stamp -r patch my_app # => 0.0.1 (auto-initializes)
vmn stamp -r minor my_app # => 0.1.0
vmn show my_app # => 0.1.0
vmn stamp -r patch --pr rc my_app # => 0.1.1-rc.1 (prerelease)
vmn release my_app # => 0.1.1# One command: capture code state, run training, record metrics + duration + exit code.
# Your script appends "key=value" lines to $VMN_METRICS_FILE and vmn ingests them.
vmn exp run my_model --note "baseline CNN" -- python train.py
# => 0.1.0-dev.a1b2c3d.e4f5g6h
# Edit the model, run again β a fresh experiment, even on the same commit
vmn exp run my_model --note "with dropout" -- python train.py
# => 0.1.0-dev.f7a2b1c.d3e4f5g
# Leaderboard, best loss first
vmn exp list my_model --sort loss
# See exactly what changed AND what happened β metric delta + real code diff
vmn exp diff my_model
# Comparing 0.1.0-dev.a1b2c3d.e4f5g6h -> 0.1.0-dev.f7a2b1c.d3e4f5g
#
# metrics: loss 0.45 -> 0.34 acc 0.85 -> 0.91
#
# diff --git a/.../model.py b/.../model.py
# - return lr * 0.5 # baseline
# + return lr * 0.3 # with dropout
# Winner! Restore that exact state (any dirty work is auto-saved first)
vmn exp restore my_model --latestBoth workflows store everything locally under .vmn/ (git-ignored -- never committed or pushed) -- no servers, no lock-in. Add --backend s3 if you want to share experiments across a team; see Storage below.
vmn does everything semantic-release and release-please do -- plus 9 things nothing else does.
| 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 / air-gapped | β | β | β | β * |
| Zero lock-in (pure git tags) | β | β | β | β |
| Dev snapshots (uncommitted state capture) | β | β | β | β |
| ML experiment tracking | β | β | β | β |
Bold rows = only vmn.
* changesets works offline for authoring, but requires GitHub/npm for publishing.
Detailed comparisons & migration guides
See Already using another tool? for step-by-step migration paths from semantic-release, release-please, setuptools-scm, standard-version, and bump2version.
Most experiment trackers require a server, a cloud account, or both. vmn tracks experiments the same way it tracks versions -- in git and local files.
vmn exp run my_model --note "baseline ResNet run" -- python train.py
# => 0.2.0-dev.c3d4e5f.a1b2c3d
vmn exp list my_model --sort loss --top 3
# [1] 0.2.0-dev.c3d4e5f.a1b2c3d (5m ago) loss=0.12 accuracy=0.94 - baseline ResNet run
# [2] 0.1.0-dev.f7a2b1c.d3e4f5g (2d ago) loss=0.34 accuracy=0.91 - with dropout
# [3] 0.1.0-dev.a1b2c3d.e4f5g6h (3d ago) loss=0.45 accuracy=0.85 - baseline CNN
vmn exp restore my_model --latest # checkout exact code state (dirty work auto-saved)| Capability | vmn | MLflow | W&B | DVC | Neptune |
|---|---|---|---|---|---|
| No server required | β | β * | β | β | β |
| No cloud account | β | β (self-hosted) | β | β | β |
| Free & open source | β | β | Free tier | β | Free tier |
| Metrics tracking | β | β | β | β | β |
| Live training curves | β | β | β | β | β |
| Experiment comparison | CLI + Web UI | Web UI | Web UI | CLI | Web UI |
| Web UI | β
(vmn ui, one command) |
server + DB | cloud | β | cloud |
| Stamp-tree / version DAG view | β | β | β | β | β |
| Full code state capture | β | β | β | partial ** | β |
| Uncommitted changes captured | β | β | β | β | β |
| One-command state restore | β | β | β | β | β |
| Built-in version management | β | β | β | β | β |
| Artifact tracking | β (SHA256) | β | β | β | β |
| Works offline / air-gapped | β | self-hosted only | β | partially | β |
| Install complexity | pip install vmn |
server + DB | account + pip | pip + git config | account + pip |
| Storage | local files / S3 (git-anchored) | database | cloud | git/S3 | cloud |
| Lock-in | zero (git tags + files) | MLflow format | W&B cloud | DVC format | Neptune cloud |
Bold rows = only vmn. Capture your exact working state -- dirty files, local commits, everything -- and restore it with one command.
* MLflow Tracking can log to local files without a server, but the comparison UI requires
mlflow server. ** DVC tracks data/model files via git, but does not capture uncommitted code changes or local-only commits.
vmn gives you the CLI-first, local-first workflow and a web UI when you want one. vmn ui (pip install vmn[ui]) serves a dashboard over the files you already have -- experiment leaderboards, live training curves, side-by-side run comparison with a real code diff, an artifact browser, and a stamp-tree version-DAG view that no experiment tracker has -- with no server to stand up and no cloud account. See Web UI below.
Use vmn when:
- You want a CLI-first workflow with no context switching
- You work offline or in air-gapped environments
- You need version management and experiment tracking in one tool
- You want zero infrastructure -- no servers, no databases, no accounts
- You prefer local-first storage with no vendor lock-in
Use MLflow / W&B when:
- You need cloud-hosted dashboards, team collaboration, reports, or sweeps
- You are already invested in their ecosystem and integrations
Subcommands cover the full experiment lifecycle:
| Command | What it does |
|---|---|
vmn exp run |
Capture code state, run a command, record metrics + exit code + duration |
vmn exp create |
Capture a snapshot with metrics, parameters, and notes (no command) |
vmn exp add |
Log additional metrics, notes, or artifacts to an experiment |
vmn exp list |
List experiments with filtering and sorting by any metric |
vmn exp show |
Display full experiment details including log history |
vmn exp diff |
Metric delta + real source diff between two experiments |
vmn exp compare |
Side-by-side metric table across N experiments |
vmn exp restore |
Restore the exact code state -- dirty work is auto-saved first |
vmn exp export |
Export experiment as a directory or tarball |
vmn exp prune |
Clean up old experiments (keep N or older than duration) |
Most actions default to the latest experiment when you omit -v. You can address
experiments by full version string, a unique prefix, --latest, or @N (the N-th
row shown by vmn exp list).
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. No digging through CI logs, no guessing which commit broke prod. Reproduce bugs in seconds, not hours.
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:
vmn stamp -r minor my_app
# records: my_app @ abc123, lib_core @ def456, lib_utils @ 789fed
# six months later
vmn goto -v 0.1.0 my_app # all 3 repos restored to recorded commitsDependencies are declared in .vmn/my_app/conf.yml and auto-cloned if missing -- up to 10 in parallel. One command, full reproducibility.
Version multiple services under one root app. Each service has its own semver; the root gets an auto-incrementing integer that ticks on every child 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 # => 3Deploy auth and billing independently while the root version gives you a single monotonic counter for "what changed last." Perfect for Kubernetes manifests and release notes that span services.
All version state lives in annotated git tag messages as plain YAML. There are no proprietary databases, no SaaS dashboards, no JSON files you have to keep in sync. Uninstall vmn tomorrow and your tags still make perfect sense:
git tag -l 'my_app_*' # every version, right there
git tag -n1 my_app_1.2.3 # full stamp metadata in the tag messageSwitch to a different tool, read the tags with a shell script, or parse them in CI -- the data is yours.
Standard Semver 2.0 plus an optional 4th hotfix segment and dev snapshots for every workflow:
1.6.0 # stable release
1.6.0-rc.23 # prerelease
1.6.7.4 # hotfix (4th segment)
1.6.0-rc.23+build01.Info # build metadata
1.6.0-dev.a1b2c3d.e4f5g6h # dev snapshot
The hotfix segment lets you ship emergency fixes without bumping patch, keeping your release train on schedule while production stays safe.
vmn experiment (alias: vmn exp) adds local-first experiment tracking to any
versioned app. No servers, no databases -- experiments are plain files under
.vmn/{app}/experiments/ (git-ignored, never committed or pushed), anchored to
your version tags via the base commit. Every experiment ties back to an exact
version and commit, so reproducing results is a vmn exp restore away.
No training script required. An experiment is just a snapshot of your tree plus a metrics log -- ML training is one use case, but config sweeps, benchmarks, and performance tests work the same way. The examples below use
python train.py; for the no-command, config-tweaking workflow and a complete reference, see the full experiments guide.
# Run a training command; vmn captures code state + records metrics/exit/duration
vmn exp run my_model --note "baseline CNN" -- python train.py
# ...or capture the current dirty state without running anything
vmn exp create my_model --note "baseline CNN" --metrics loss=0.45 acc=0.85
# Append metrics or artifacts to the latest experiment
vmn exp add my_model --metrics loss=0.31 acc=0.92 --attach weights.pt
# Metric delta + real code diff between the last two experiments
vmn exp diff my_model
# Restore the best run (dirty work auto-saved first)
vmn exp restore my_model --latestEvery version-taking action defaults to the latest experiment when -v is omitted,
and accepts a full version, a unique prefix, --latest, or @N.
Create an experiment, run a command, and record its outcome. The child inherits your
terminal (output streams live) and these env vars: VMN_EXPERIMENT_ID, VMN_APP_NAME,
VMN_METRICS_FILE. Any key=value lines the process appends to VMN_METRICS_FILE
become a metrics entry. vmn exp run returns the command's own exit code.
vmn exp run my_model --note "dropout 0.3" -- python train.py --lr 0.01Minimal train.py that reports back to vmn:
import os
metrics_file = os.environ["VMN_METRICS_FILE"]
def log_metric(key, value, step=None):
with open(metrics_file, "a") as f:
prefix = f"step={step} " if step is not None else ""
f.write(f"{prefix}{key}={value}\n")
def train():
for epoch in range(10):
loss = run_one_epoch() # your training code
log_metric("loss", loss, step=epoch) # per-step series -> training curve
log_metric("accuracy", 0.91) # final scalar
log_metric("loss", loss)
if __name__ == "__main__":
train()Each line is one metrics record: key=value [key=value β¦], optionally prefixed
with step=N to build a per-step series (vmn tails the file during the run, so
metrics appear in the experiment log live while training). Numeric values are
parsed as floats; anything else is kept as a string. Your script's own exit code
becomes vmn exp run's exit code, so CI can tell a failed training run from a
successful one.
Works on a clean or dirty tree, and cold-starts a fresh repo (auto-init + baseline stamp).
Capture the current state as an experiment without running a command. Re-running over
the identical code state starts a new run (.r2, .r3, β¦) instead of overwriting β
so "same code, different seed" never clobbers a previous run. On a clean tree the diff
hash is zeroed (...-dev.<commit>.0000000).
vmn exp create my_model --note "dropout 0.3" --metrics loss=0.45 acc=0.85
vmn exp create my_model -f params.yml --attach initial_weights.pt--metrics records measurements; -f params.yml records inputs (params:,
hypothesis:, tags:) β they no longer overwrite each other.
Append metrics, notes, or artifacts to an experiment (default: the latest).
vmn exp add my_model --metrics val_loss=0.29 val_acc=0.93
vmn exp add my_model -v @2 --attach checkpoint_epoch10.pt --note "after LR warmup"List experiments, optionally sorted by a metric.
vmn exp list my_model # all experiments
vmn exp list my_model --sort loss --top 5 # best 5 by loss
vmn exp list my_model --last 10 # most recent 10Display full details for a single experiment.
vmn exp show my_model # latest
vmn exp show my_model -v @1 # the [1] row from listMetric/param delta plus a real source diff between two experiments (default: the latest two).
vmn exp diff my_model # latest two
vmn exp diff my_model -v @1 -v @3 # specific runs by index
vmn exp diff my_model --tool delta # external diff toolSide-by-side metric table across N experiments (no code diff β use exp diff for that).
vmn exp compare my_model -v 1.1.0-dev.aaa.bbb -v 1.2.0-dev.ccc.ddd
vmn exp compare my_model --last 3Check out the exact code state and retrieve artifacts. If the working tree is dirty, that work is auto-snapshotted first (and the recovery command is printed) β you never lose uncommitted changes.
vmn exp restore my_model --latest
vmn exp restore my_model -v @2Package an experiment (metadata, metrics, artifacts) into a tarball.
vmn exp export my_model # latest -> <verstr>.tar.gz
vmn exp export my_model --latest -o best_run.tar.gzClean up old experiments by count or age.
vmn exp prune my_model --keep 10 # keep the 10 most recent
vmn exp prune my_model --older-than 30d # remove experiments older than 30 daysPass a YAML file with -f to attach structured metadata to any experiment:
# params.yml
hypothesis: "larger batch size improves convergence"
params:
lr: 0.001
batch_size: 64
epochs: 50
tags: [baseline, transformer-v2]vmn exp create my_model -f params.yml --metrics loss=0.38Declare each metric's goal and a primary metric in .vmn/{app_name}/conf.yml so
list --sort and the leaderboard know which direction is better. goal: min means
lower is better (best-first ascending); goal: max means higher is better:
experiment:
metrics:
loss: {goal: min, primary: true}
val_loss: {goal: min}
acc: {goal: max}Experiments are stored locally by default under .vmn/. For team-wide sharing,
point to an S3-compatible backend:
S3 / MinIO storage flags
All subcommands accept these flags:
vmn exp create my_model --backend s3 --bucket my-experiments \
--endpoint-url http://minio:9000 --prefix team/ml| Flag | Default | Description |
|---|---|---|
--backend |
local |
local or s3 |
--bucket |
-- | S3 bucket name |
--endpoint-url |
-- | Custom endpoint (MinIO, LocalStack, etc.) |
--prefix |
vmn-experiments |
Key prefix inside the bucket |
A dashboard over the data you already have -- experiments, versions, snapshots -- with no server infrastructure and no cloud account. It reads git tags and .vmn/ files (or S3) directly; the whole SPA ships inside the wheel.
pip install "vmn[ui]"
cd your-project
vmn ui # http://127.0.0.1:8265, opens your browserWhat it does:
- Experiment leaderboard -- sortable metric columns (goal-aware best-first),
@Nindices matching the CLI, filter by note/metadata. - Run detail -- metadata, params vs. metrics, live training curves (from
step=series), the full log timeline, and copy-paste reproduce commands. - Compare -- pick two runs for a metric-delta table plus a real color-coded code diff.
- Stamp tree -- the version history as a DAG (nodes colored by release mode, edges from
previous_version), root-app β services topology, and cross-repo dependency pins. No experiment tracker has this -- it's unique to vmn's git-tag model. - Actions -- run
vmn stamp/restore/goto/release/prunefrom the browser. Each runs as a realvmnsubprocess (so it takes the repo lock correctly), streams its log live, and shows the equivalent CLI command. Restores surface the dirty-work safety net.
Open the URL and pick a workspace (top-left) -- locally there's just the one repo you started it in. Click into a workspace to see its apps, then click an app for the experiment leaderboard: one row per vmn exp run / exp create, columns are whatever metrics you logged, click a header to sort. The tab bar switches between experiments, snapshots, stamp tree, and actions for that app.
Click any leaderboard row for the run detail page (metadata, metrics, training curves, full log, reproduce commands). Check two rows to reveal Compare selected -- a metric-delta table plus the real code diff between them.
To get training curves, have your script log step=-tagged lines to $VMN_METRICS_FILE (see the train.py example above) -- exp run tails the file live, so the curve updates in the UI during training, not just after.
The stamp tree tab renders your version history as a colored graph (release mode = color); click a node for its details. Root apps also show a service-topology table. The actions tab has a form to run vmn stamp (with a dry-run preview) as a real subprocess, streaming its log back to you; restore/goto actions live on a run's detail page and auto-save any dirty work first.
An editable install picks up Python changes immediately; only the SPA needs an explicit rebuild:
pip install -e ".[ui]"
vmn ui --no-browser # leave running
# edit Python under version_stamp/ui/ -> just refresh the browser
# edit webui/src/... -> rebuild, then refresh (no restart needed)
cd webui && npm run buildnpm run build writes straight into version_stamp/ui/static/, which the running server reads from disk on every request.
Run it on a shared host and point a browser at it:
vmn ui --host 0.0.0.0 --port 8265 \
--token "$VMN_UI_TOKEN" \
--data-dir /srv/vmn-ui- Workspaces -- the server hosts many isolated checkouts. Add them by cloning a remote or attaching a path; several can be clones of the same repo (one per branch/user), so a stamp in one never touches another. S3 buckets register as read-only experiment sources -- no local repo required (
vmn ui --s3-bucket my-experiments). - Auth -- a shared bearer token (
--token/VMN_UI_TOKEN); put TLS and user management behind a reverse proxy. --read-onlydisables all mutation endpoints for cautious deployments.
The whole /api/v1/... surface is documented at /api/docs (OpenAPI/Swagger) for scripting without the UI. See docs/ui.md for deployment details.
Snapshots capture your exact working state -- uncommitted changes, local commits, untracked files -- into a deterministic dev version you can restore later. No WIP commits, no stash juggling. The newer vmn experiment command builds on this primitive for structured experiment tracking.
An experiment is a snapshot plus an append-only metrics/notes log. Use a plain snapshot to save or restore code state; use an experiment when you want to track and compare runs.
vmn snapshot |
vmn exp |
|
|---|---|---|
| Captures code state (tracked + untracked) | β | β |
| Metrics / params / notes log | single note | β append-only log |
| Run a command and record its outcome | β | β
(exp run) |
| Compare across runs | diff only | diff + metric deltas + compare |
| Typical use | saving WIP before a risky change, sharing a dev build | tracking and comparing ML/research runs |
Every snapshot produces a version string derived from the content itself:
{base_version}-dev.{commit_hash}.{diff_hash}
Identical code state always produces the identical version string. If nothing changed, you get the same snapshot version back.
vmn snapshot create my_model --note "promising results"
# => 1.2.0-dev.a1b2c3d.e4f5g6h
vmn snapshot list my_model
# [1] 1.2.0-dev.a1b2c3d.e4f5g6h (2h ago) - promising results
# [2] 1.2.0-dev.x9y8z7w.q1r2s3t (1d ago)
vmn snapshot show my_model # latest by default
vmn snapshot note my_model --note "confirmed: best run"
vmn snapshot diff my_model -v 1.2.0-dev.a1b # second side defaults to the working tree
vmn snapshot export my_model -o ./experiment_42
# Restore a snapshot (dirty work is auto-saved first); vmn goto also works
vmn snapshot restore my_model -v 1.2.0-dev.a1b2c3d.e4f5g6h
vmn goto -v 1.2.0-dev.a1b2c3d.e4f5g6h my_modelWhat's stored in a snapshot
.vmn/{app}/snapshots/{version}/
metadata.yml # version, branch, timestamp, note, dirty states
working_tree.patch # uncommitted changes (git diff HEAD)
local_commits.patch # local commits not yet pushed
untracked_files.tar.gz # untracked files
deps/{dep_name}/... # dependency patches (same structure)
artifacts/{filename} # attached files
Everything needed to reconstruct the working tree is stored alongside the metadata. Dependencies get their own patch set so multi-repo states restore correctly with vmn goto.
All snapshot flags
Actions: create (default), list, show, note, diff, export, restore.
Version-taking actions default to the latest snapshot and accept a full version, a
unique prefix, --latest, or @N.
| Flag | Description |
|---|---|
-v, --version |
Target a specific snapshot version (prefix / @N / --latest all work) |
--latest |
Use the most recent snapshot |
--last N |
Show only the N most recent snapshots (for list) |
--note |
Attach or update a text note |
--to |
Second version for diff (defaults to current, the working tree) |
--tool |
External diff tool (meld, vimdiff, etc.). Falls back to git config diff.tool |
-o, --output |
Export destination path |
--meta |
Repeatable key=value metadata pairs |
--meta-file |
YAML file with additional metadata |
--filter |
Repeatable key=value pairs for filtering list output |
--backend |
Storage backend: local (default) or s3 |
--bucket |
S3 bucket name |
--endpoint-url |
Custom S3 endpoint (MinIO, DigitalOcean Spaces, etc.) |
--prefix |
S3 key prefix (default: vmn-snapshots) |
--verbose |
Show extended snapshot details |
Storage backends
Local (default) -- snapshots live under .vmn/{app}/snapshots/ in the repository. Nothing to configure.
S3 -- push snapshots to any S3-compatible store:
vmn snapshot create my_model --backend s3 --bucket my-snapshots
vmn snapshot list my_model --backend s3 --bucket my-snapshotsCustom endpoints work for MinIO, DigitalOcean Spaces, and similar services:
vmn snapshot create my_model \
--backend s3 \
--bucket dev-snapshots \
--endpoint-url http://localhost:9000 \
--prefix team/ml
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 snapshot |
Capture dev state (uncommitted work) | vmn snapshot create my_app |
vmn experiment |
Track ML experiments (alias: exp) |
vmn exp create my_model --metrics loss=0.34 |
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.1.1-rc.1 (prerelease)
vmn stamp my_app # => 0.1.2 (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 the current commit already matches. Auto-initializes repo and app on first run.
Stamping without -r, -r vs --orm, and all flags
Without -r: only works during a prerelease sequence (continues the existing prerelease). Errors on a release commit. Exception: with conventional_commits enabled, -r is always optional -- the release mode is inferred from commit messages.
-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 the target version. |
All stamp flags:
| Flag | Description |
|---|---|
-r, --release-mode |
Release mode: major, minor, patch, hotfix, micro |
--orm, --optional-release-mode |
Optional release mode: major, minor, patch, hotfix |
--pr, --prerelease |
Create a prerelease (e.g., --pr rc produces X.Y.Z-rc.N) |
--pull |
Pull remote before stamping; retries on conflict |
--dry-run |
Preview the version without committing or tagging |
-e, --extra-commit-message |
Append extra text to the stamp commit message |
--ov, --override-version |
Force a specific version string |
--orv, --override-root-version |
Force a specific root-app version |
--dont-check-vmn-version |
Skip the vmn version compatibility check |
vmn init-app flags:
| Flag | Description |
|---|---|
-v, --version |
Initial version (default 0.0.0) |
--dry-run |
Preview without writing |
--orm, --default-release-mode |
Set default release mode: optional or strict |
Enable conventional_commits and never type -r again. Commit prefixes map directly to release modes: fix: -> patch, feat: -> minor, BREAKING CHANGE or ! after type -> major.
git commit -m "feat: add search endpoint"
vmn stamp my_app # => 0.2.0 (minor, auto-detected)conf:
conventional_commits: true
default_release_mode: optional
changelog:
path: "CHANGELOG.md"
github_release:
draft: falsevmn release
vmn release -v 0.0.1-rc.1 my_app # explicit version -- tag-only
vmn release my_app # auto-detect from current commit
vmn release --stamp my_app # full stamp flow -- new commit + tag + pushPromotes prerelease to final. Idempotent. -v and --stamp are mutually exclusive.
vmn show
vmn show my_app # current version
vmn show --dev my_app # dev version with commit+diff hash
vmn show --verbose my_app # full YAML metadata dump
vmn show --raw my_app # without template formatting
vmn show --type my_app # release / prerelease / metadata
vmn show -u my_app # unique ID (version+commit_hash)
vmn show --conf my_app # show app configuration
vmn show --root my_root_app # root app version (integer)vmn goto
vmn goto -v 1.2.3 my_app # repo + deps restored to exact state
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 -v 1.2.0-dev.a1b2c3d.e4f5g6h my_model # restore dev snapshotDeps auto-cloned if missing. Dev restore: checkout base, replay commits, apply working tree patch.
vmn gen
vmn gen -t version.j2 -o version.txt my_app
vmn gen -t version.j2 -o version.txt -c custom.yml my_appTemplate variables: version, base_version, name, release_mode, prerelease, previous_version, stamped_on_branch, release_notes, changesets, root_name, root_version, root_services.
vmn add
vmn add -v 0.0.1 --bm build42 my_appAttaches build metadata to an existing version tag (e.g. 0.0.1+build42). Optional --vmp for metadata file path.
vmn config
vmn config # list all managed apps
vmn config my_app # interactive TUI editor
vmn config my_app --vim # open in $EDITOR
vmn config --branch my_app # branch-specific override
vmn config --global # repo-level .vmn/conf.ymlRelease candidates
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.0Python library
vmn can be called programmatically. vmn_run accepts a command-line argument list and returns (exit_code, context):
from version_stamp.cli.entry import vmn_run
ret, ctx = vmn_run(["show", "my_app"])Note:
vmn_runprints to stdout/stderr. To capture output programmatically, wrap calls withcontextlib.redirect_stdout/redirect_stderr.
Environment variables
| 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 |
vmn stamp can automatically write the stamped version into your project files. Add a version_backends section to .vmn/<app>/conf.yml:
| 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 |
version_backends:
npm:
path: "relative/path/to/package.json"Hatchling / uv dynamic versioning (hatch-vcs)
Instead of file injection, read the version directly from git tags at build time:
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
tag-pattern = "my_app_(?P<version>.*)"No version_backends entry needed -- the build tool pulls the version from the tag vmn created.
Generic backends (regex and Jinja2)
generic_selectors -- regex find-and-replace in any file:
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 -- render a Jinja2 template:
version_backends:
generic_jinja:
- input_file_path: f1.jinja2
output_file_path: jinja_out.txt
custom_keys_path: custom.ymlSame variables as vmn gen.
vmn auto-generates .vmn/<app-name>/conf.yml when an app is first stamped. Edit it directly or use vmn config (see Commands). Full conf.yml structure:
conf:
template: '[{major}][.{minor}][.{patch}][.{hotfix}][-{prerelease}][.{rcn}][-dev.{dev_commit}.{dev_diff_hash}][+{buildmetadata}]'
hide_zero_hotfix: true
extra_info: false
create_snapshots: false
conventional_commits: true
default_release_mode: optional # "optional" (--orm) or "strict" (-r required). Top-level key, not nested under conventional_commits.
changelog:
path: "CHANGELOG.md"
github_release:
draft: false
deps:
../:
other_repo:
vcs_type: git
version_backends:
npm:
path: "package.json"
policies:
whitelist_release_branches: ["main"]
snapshot_storage:
backend: local
bucket: my-bucket
prefix: vmn-snapshots
endpoint_url: https://...
experiment:
metrics:
loss: { goal: min, primary: true }
acc: { goal: max }Migration note:
create_verinfo_fileshas been renamed tocreate_snapshots. The old key still works but prints a deprecation warning.
Per-branch configuration. Place {branch}_conf.yml next to conf.yml in .vmn/<app>/. vmn checks for a branch-specific config first and falls back to conf.yml. Stale branch configs from other branches are auto-cleaned on stamp.
Use the official GitHub Action for stamping in CI pipelines:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # vmn needs full history
- uses: progovoy/vmn-action@latest
with:
app-name: my_app
do-stamp: true
stamp-mode: patch
env:
GITHUB_TOKEN: ${{ github.token }}fetch-depth: 0 is required -- vmn reads git tags and history to compute the next version.
vmn is actively developed. File an issue to vote or suggest.
-
vmn exp plot-- ASCII metric charts in the terminal (vmn exp plot --metric loss my_model) - Monorepo auto-discovery -- detect apps from Cargo workspaces, pnpm-workspace.yaml, Python namespace packages
- PR version annotation -- GitHub Action that auto-comments the next version using
vmn stamp --dry-run - Post-stamp hooks -- run custom commands after a successful stamp (deploy, notify, update docs)
- Homebrew tap --
brew install vmn
See the full roadmap and backlog for more.
vmn can't find tags / shows wrong version
Most CI systems default to shallow clones. vmn needs full history and tags:
# GitHub Actions
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full historyOr manually: git fetch --tags --unshallow
"Another vmn process is running" / lock file error
vmn uses a per-repo lock file to prevent concurrent stamps. If a previous run crashed:
rm .vmn/.vmn.lock # default location
# or if VMN_LOCK_FILE_PATH is set:
rm "$VMN_LOCK_FILE_PATH"Tag name collision with existing tags
vmn tags follow the format {app_name}_{version}. If your repo already has tags matching this pattern, either rename the app or clean up conflicting tags before the first stamp.
"Dirty" state warnings on stamp
vmn checks for uncommitted changes and unpushed commits. To stamp despite dirty state, commit or stash your changes first. vmn show --verbose shows the exact dirty flags (pending, outgoing, detached).
Step-by-step guides for common migration paths:
- 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)