semver-sync
One version for every crate in a cargo workspace, moved by one command. These versions become docker tags, so they are checked as tags before anything is written.
Install
From a checkout:
Use
Run it from anywhere inside the workspace; the workspace is found through
cargo. Every command that writes takes --dry-run, which prints what would
move and then checks the manifests were left alone:
What a write touches
[package] versionand[workspace.package] versionin every manifest that holds one, including a workspace root that is a package itself.version = "..."pins on dependencies that point at another member of the workspace by path — the pins that stop cargo resolving when only the package versions move. Pins on crates.io dependencies are left alone.Cargo.lock, by asking cargo to resolve the workspace again once the manifests are written. If it does not resolve, the manifests are already on disk:git diff -- '*Cargo.toml'says what moved.
Crates that inherit version.workspace = true need nothing of their own; cargo
reports the version they resolve to, and that is what ss lists.
A level bump ignores a tag on the way: 1.2.3-rc.4 with ss patch gives
1.2.4. ss tag rc keeps the core version and starts a series — 1.2.3 gives
1.2.3-rc.0, and the same command again gives 1.2.3-rc.1. A different tag
starts its own series: 1.2.3-hotfix.0.
A version has to survive as a docker tag: start with a letter or a digit, then
A-Z a-z 0-9 . _ -, with no ... That rules out build metadata such as
1.2.3+g1a2b.
Configuration
The first file that exists is the one read, so a repo can say something different from the machine it sits on:
- the repo root:
semver-sync.toml, then.semver-sync.toml $HOME:semver-sync.toml, then.semver-sync.toml- the user config dir (
$XDG_CONFIG_HOME, else$HOME/.config): the same two names
The repo root is the one cargo resolves — the root every other command works on. A file that does not parse, or that names a key which is not a setting, is an error where it lies: reading the file behind it, or walking past a misspelled key, would run a bump under settings nobody asked for.
The file ss config writes names every setting below at the value a run reads
when no file is there at all, and it never replaces a file already there, under
either name.
git-tag
= false # the default: a write ends at the manifests
Turn it on and a write finishes the release in git as well:
It starts with git status, before a manifest is touched: if the tree holds
any file the run did not write — unstaged, staged or untracked — the run
stops there and names each one, so cleaning up and retrying still writes
the version this run was asked for. The same check stands in front of the
git add . below, which cannot then sweep work in progress into the
release commit. The manifests and Cargo.lock the run wrote are committed
as v1.4.0, the tag carries that commit, and both go to the remote; a run
whose bytes matched what was already committed skips the empty commit and
tags the commit HEAD stands on. A --dry-run preflights the tree and
prints what the release would do instead. Each step waits for the one
before, so a push that cannot reach the remote reports the release as
local rather than letting a bump pass for one.
Completions
Development