Release Automation
This document records the evaluation of workspace release tooling for the Paladin framework, the selected tool, and the operator guide for cutting a release. It is part of Milestone 10 — CI Hardening and Release Automation, Epic 3.
Tooling Evaluation: cargo-release vs. release-plz
| Dimension | cargo-release | release-plz |
|---|---|---|
| Trigger model | Manual, developer-invoked command (cargo release) | PR-bot: opens/maintains a "release PR" automatically from main |
| Changelog handling | Works with a curated CHANGELOG.md; can run hooks to edit it | Auto-generates changelog from Conventional Commits |
| Workspace publish order | Built-in: publishes members in dependency order, supports lockstep or independent versions | Built-in: computes order, also opinionated about per-crate versioning |
| Version bumping | Bumps [package].version + internal workspace.dependencies pins in lockstep | Bumps versions per-crate based on detected changes |
| Required secrets / infra | CARGO_REGISTRY_TOKEN for publish; no bot, no extra app | CARGO_REGISTRY_TOKEN plus a GitHub token/app for the release-PR bot |
| Operational model | Fits an existing tag-triggered pipeline: bump+tag locally, CI publishes on the tag | Replaces the manual flow with a continuously-updated release PR |
| Maintenance cost | Low: one config file (release.toml), no running bot | Higher: bot behavior, PR hygiene, commit-message discipline enforced |
| Fit with current practice | High — matches curated CHANGELOG.md, lockstep 0.3.0-everywhere, and release.yml v*.*.* trigger | Lower — requires moving to Conventional-Commit-driven changelog + PR-bot workflow |
Recommendation & Decision: cargo-release
cargo-release is selected. The Paladin repository already has:
- a curated
CHANGELOG.mdwith a## [Unreleased]section (we want to keep authoring it, not auto-generate it), - lockstep versioning (every public crate is
0.3.0;docs/RELEASE_CHECKLIST.mdmandates a "lockstep version update across public crates"), and - a tag-triggered pipeline (
.github/workflows/release.ymlalready fires onv*.*.*).
cargo-release slots directly into this model: a maintainer runs a single command (wrapped by
make release VERSION=x.y.z) that bumps all crates in lockstep, finalizes the changelog, commits,
tags v x.y.z, and pushes. The push triggers CI, which publishes the crates to crates.io in
dependency order. No PR-bot, no GitHub App, and no change to the curated-changelog or
Conventional-Commit practice is required.
release-plz is a strong tool but optimizes for a different workflow (PR-bot + auto-changelog +
per-crate version detection) that would be a larger process change for marginal benefit here. It can
be revisited if the project later adopts strict Conventional Commits and prefers a continuous
release-PR model.
Reproducible Installation
cargo-release is installed the same way locally and in CI, pinned and --locked:
cargo install cargo-release --locked
(The CI publish job installs it with --locked so the build is reproducible from Cargo.lock.)
Release Configuration (release.toml)
The repo-root release.toml encodes:
- Lockstep versioning —
shared-version = trueso all publishable crates move to the same version in one bump, and the internalworkspace.dependenciespins are updated to match. - Dependency-ordered publishing —
cargo-releasepublishes workspace members in topological dependency order:paladin-core→paladin-ports→ the leaf tier (paladin-battalion,paladin-llm,paladin-memory,paladin-web,paladin-notifications,paladin-content,paladin-storage) →paladin(facade). - Tag/commit conventions — a single workspace tag
v{{version}}is created (the.github/workflows/release.ymlpipeline keys offv*.*.*).
Canonical Publish Order
Per Milestone 7 Appendix B, publishable crates are released dependency-first:
paladin-core(package namepaladin-ai-core)paladin-portspaladin-battalion,paladin-llm,paladin-memory,paladin-web,paladin-notifications,paladin-content,paladin-storage(parallel-safe tier)paladin(facade, package namepaladin-ai)paladin-cli(only when/if it exists as a separate publishable crate)
Operator Guide: Cutting a Release
A release is cut locally with a single command; CI does the publishing.
# 1. Ensure you are on the release branch with a clean tree and up-to-date CHANGELOG [Unreleased].
# 2. Cut the release (bumps all crates in lockstep, finalizes changelog, commits, tags, pushes):
make release VERSION=0.4.0
make release:
- Validates
VERSIONis a valid semver string (fails fast otherwise). - Runs
make release-check(format, lint, full tests, audit, release build). - Bumps every public crate to
VERSIONin lockstep and updates internal dependency pins. - Moves the
## [Unreleased]changelog section under a## [VERSION] - <date>heading. - Commits, creates the
v VERSIONtag, and pushes branch + tag.
Pushing the v*.*.* tag triggers .github/workflows/release.yml, which runs the test suite and then
publishes the crates to crates.io in dependency order, builds Docker images and binaries,
generates the SBOM, and creates the GitHub release.
Required Secret
crates.io publishing requires a repository secret:
CARGO_REGISTRY_TOKEN— a crates.io API token with publish scope.
If the secret is absent, the publish job is skipped (the rest of the release still runs), so the pipeline can be exercised safely before the token is configured.
Dry Run (no live publish)
To exercise the pipeline without publishing to crates.io, trigger the workflow manually with the
dry_run input set to true:
gh workflow run release.yml -f tag=v0.4.0-rc.1 -f dry_run=true
In dry-run mode the publish job runs cargo publish --dry-run for each crate in order instead of a
real publish. Locally, the same validation is available via:
make publish-dry-run