semver-sync 0.2.1

Keep one version across every crate in a cargo workspace; bumps are checked as docker tags before any manifest is written.
semver-sync-0.2.1 is not a library.

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

cargo install semver-sync

From a checkout:

cargo install --path .

Use

semver-sync alias   # install the shorter `ss`, then: source ~/.zshrc
ss                  # what every crate is at
ss patch            # 0.1.0 -> 0.1.1
ss minor            # 0.1.0 -> 0.2.0
ss major            # 0.1.0 -> 1.0.0
ss tag rc           # 0.1.0 -> 0.1.0-rc.0, then rc.1, rc.2 on the same tag
ss set 1.4.0-rc.0   # one exact version
ss update           # reinstall this tool from crates.io, cargo decides if it is newer
ss version          # the version of semver-sync itself
ss config           # write the config file at the repo root
ss config --global  # write it in the user config dir instead

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:

ss patch --dry-run

What a write touches

  • [package] version and [workspace.package] version in 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

ss config           # semver-sync.toml at the repo root
ss config --global  # semver-sync.toml in the user config dir

The first file that exists is the one read, so a repo can say something different from the machine it sits on:

  1. the repo root: semver-sync.toml, then .semver-sync.toml
  2. $HOME: semver-sync.toml, then .semver-sync.toml
  3. 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

git-tag = false    # the default: a write ends at the manifests

Turn it on and a write finishes the release in git as well:

git add .
git commit -m v1.4.0
git tag -a v1.4.0 -m v1.4.0
git push
git push --tags

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

ss completions zsh > ~/.zfunc/_semver-sync
compdef _semver-sync ss    # the script binds `semver-sync`; this binds the alias

Development

cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt --check