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

Dimensioncargo-releaserelease-plz
Trigger modelManual, developer-invoked command (cargo release)PR-bot: opens/maintains a "release PR" automatically from main
Changelog handlingWorks with a curated CHANGELOG.md; can run hooks to edit itAuto-generates changelog from Conventional Commits
Workspace publish orderBuilt-in: publishes members in dependency order, supports lockstep or independent versionsBuilt-in: computes order, also opinionated about per-crate versioning
Version bumpingBumps [package].version + internal workspace.dependencies pins in lockstepBumps versions per-crate based on detected changes
Required secrets / infraCARGO_REGISTRY_TOKEN for publish; no bot, no extra appCARGO_REGISTRY_TOKEN plus a GitHub token/app for the release-PR bot
Operational modelFits an existing tag-triggered pipeline: bump+tag locally, CI publishes on the tagReplaces the manual flow with a continuously-updated release PR
Maintenance costLow: one config file (release.toml), no running botHigher: bot behavior, PR hygiene, commit-message discipline enforced
Fit with current practiceHigh — matches curated CHANGELOG.md, lockstep 0.3.0-everywhere, and release.yml v*.*.* triggerLower — 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.md with 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.md mandates a "lockstep version update across public crates"), and
  • a tag-triggered pipeline (.github/workflows/release.yml already fires on v*.*.*).

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 versioningshared-version = true so all publishable crates move to the same version in one bump, and the internal workspace.dependencies pins are updated to match.
  • Dependency-ordered publishingcargo-release publishes workspace members in topological dependency order: paladin-corepaladin-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.yml pipeline keys off v*.*.*).

Canonical Publish Order

Per Milestone 7 Appendix B, publishable crates are released dependency-first:

  1. paladin-core (package name paladin-ai-core)
  2. paladin-ports
  3. paladin-battalion, paladin-llm, paladin-memory, paladin-web, paladin-notifications, paladin-content, paladin-storage (parallel-safe tier)
  4. paladin (facade, package name paladin-ai)
  5. 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:

  1. Validates VERSION is a valid semver string (fails fast otherwise).
  2. Runs make release-check (format, lint, full tests, audit, release build).
  3. Bumps every public crate to VERSION in lockstep and updates internal dependency pins.
  4. Moves the ## [Unreleased] changelog section under a ## [VERSION] - <date> heading.
  5. Commits, creates the v VERSION tag, 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