Rust Crate / CLI Release Workflow (shared)
Canonical publish sequence for xberg-io repos that ship a Rust core crate and CLI
with polyglot bindings. Cargo.toml is the single source of truth for the
version, synced to every binding manifest. Confirm exact task names with
task --list — a repo may expose task set-version or task version:set.
1. Set the version
task set-version -- X.Y.Z # some repos: task version:set -- X.Y.Z
The version is passed as a -- positional argument. The task rewrites
Cargo.toml and syncs the version to all binding manifests (and any pinned
version constants), then runs cargo update. Never hand-edit version in
Cargo.toml or in binding manifests — use the task so everything stays in
lockstep. Verify with grep -m1 '^version' Cargo.toml.
2. Update the CHANGELOG
Move every [Unreleased] bullet in CHANGELOG.md into a new
## [X.Y.Z] - YYYY-MM-DD section (grouped Added / Changed / Fixed / Removed).
Re-create an empty [Unreleased]. Never tag an empty section.
3. Clean-tree precondition (hard gate)
Never release a dirty or failing tree.
poly lint . # lint clean (task lint)
poly fmt --check . # formatting clean (task format:check)
task test # tests pass
Apply formatting with poly fmt --fix . (or task format) and re-stage. Fix any
lint or test failure at its source — do not release past a failure.
4. Commit, tag, and publish the GitHub release
Push main before creating the tag, not after — tagging first and pushing
second risks the tag pointing at a commit origin/main never contains if the
push is rejected or rebased in between, and --force-with-lease does not work
on tags.
git add -A
git commit -m "chore(release): X.Y.Z"
git push origin main
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z
gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes
Add --prerelease for RC/beta tags. Prefer --notes-file built from the new
CHANGELOG section when the entry is rich. A bare git tag is not a release —
always run gh release create.
Do not assume what triggers your repo's publish workflow — check it. These
repos are not uniform: some publish on the GitHub release (on: release: types: [published]), some require an explicit workflow_dispatch with no
automatic trigger at all, and at least one fires on a tag push
(on: push: tags:). Read your own .github/workflows/publish*.yaml on:
block before relying on any one of these — assuming the wrong mechanism is how
a tagged, pushed release silently never reaches its registry.
5. Install the released build locally
cargo install --path . # CLI crate at a subpath: cargo install --path crates/<cli-crate>
Installs the released binary from the workspace so it shadows any crates.io copy
on $PATH. Confirm which <binary> and <binary> --version reflect X.Y.Z.
6. Clean up build artifacts
task clean # runs cargo clean (removes target/) plus repo-specific cleanup
Reclaims disk by removing the target/ directory (and any generated caches such
as .alef/, dist/). Where no clean task exists, run cargo clean directly.
Anti-patterns
- Hand-editing
versioninCargo.tomlor a binding manifest instead of the version task. - Releasing a dirty or lint/test-failing tree.
- Tagging without
gh release create. - AI attribution in commit/tag/release text.