Releases use Semantic Versioning. The workspace
version is the single source of truth, defined in
workspace.package.version in the root Cargo.toml. All
workspace crates inherit this version.
Before tagging a release:
- Lints are clean (
make lint) - All tests pass locally (
make test) - Dependency audit passes (
make audit) - SemVer compliance verified (
make semver) - Version in root
Cargo.tomlis bumped (bothworkspace.package.versionand anyworkspace.dependenciesinter-crate versions) -
Cargo.lockis regenerated with the new version -
make publish-dry-runsucceeds (add--allow-dirtywhen running against uncommitted changes) -
SECURITY.mdlists the new minor version - GitHub Release changelog is drafted (see below)
Tags follow the format v<MAJOR>.<MINOR>.<PATCH> (e.g.
v0.1.0) and must match workspace.package.version;
the release workflow rejects mismatched tags. Push the
tag to the repository:
git tag v0.1.0
git push origin v0.1.0Pushing the tag runs the release pipeline
(.github/workflows/release.yaml):
- Validate the tag against the workspace version
- Run the full test suite
- Verify every release crate packages cleanly (publish dry run)
- Build and publish the container image to GHCR
- Create the GitHub release with generated notes
Publishing to crates.io stays a manual step: the
pipeline stops at the dry run. Run cargo publish per
crate, in dependency order, once the release is tagged
and green.
Container images are published to GitHub Container
Registry (GHCR) by the release pipeline. Outside of a
release, the Publish workflow
(.github/workflows/publish.yaml) can be triggered
manually via workflow_dispatch to publish from any
branch or tag.
The publish steps produce these tags per run:
| Pattern | Example | Description |
|---|---|---|
sha-<hash> |
sha-abc1234 |
Git commit SHA |
<branch> |
main |
Branch name |
<version> |
0.1.0 |
Full semver (from git tag) |
<major>.<minor> |
0.1 |
Major.minor shorthand |
Semver tags are only generated when the workflow runs against a semver git tag.
Changelogs live in GitHub Releases. The release pipeline creates the release with GitHub's generated notes; edit afterwards for clarity. There is no separate CHANGELOG file.
Release branches are optional and created from tags when
backports are needed. The naming convention is
release/v<MAJOR>.<MINOR>.x (e.g. release/v0.1.x).
Fixes are cherry-picked onto the release branch, a new patch tag is created from it, and the release pipeline runs as usual.
The Containerfile builds a minimal Alpine image:
- Static musl build using the release profile (LTO, single codegen unit, stripped symbols)
- Dependency layers cached via manifest-first stub builds, so source changes do not rebuild dependencies
- Runs as a non-root user
The template image runs the probe binary to completion.
When scaffolding a long-running service, add EXPOSE
and a HEALTHCHECK, and update the container workflow
to wait for healthy status.