Two tools dominate Rust release automation and they disagree about who is driving. cargo release is a command you run, in your terminal, when you decide it is time. release-plz is a bot: it watches the main branch, prepares the next release as a pull request, and acts when you merge. Both end with a tag on git and a version on crates.io, and the choice affects how a team works more than what lands in the registry. This post runs each through the same scenario, a library with a breaking change and a fix since the last tag, and says which suits which maintainer. Tested with cargo-release 0.25, release-plz 0.3 and Cargo 1.99.
cargo release: a script you trigger
cargo install cargo-release --locked
cargo release minorWithout --execute, the command is a dry run. It prints each action it would take, the version it would write, the commit message, the tag name and the publish, and changes nothing. That default is the best design choice in the tool, since a release is the kind of command you want to read before it runs. When the plan looks right:
cargo release minor --executeThe sequence it runs is the one you would type by hand: verify the working tree is clean and on the right branch, bump version in Cargo.toml (and in other workspace crates that depend on it), update Cargo.lock, make a commit, create an annotated tag, run cargo publish, and push the commit and tag. You choose the bump, patch, minor, major, or an exact version, and it does not inspect your code to check you chose well. Configuration lives in release.toml or the manifest:
allow-branch = ["main"]
sign-tag = true
pre-release-commit-message = "release {{crate_name}} {{version}}"
tag-name = "v{{version}}"
pre-release-replacements = [
{ file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}" },
{ file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", exactly = 1 },
]The replacements are what keep a hand-written CHANGELOG.md in step: the "Unreleased" heading becomes the version at release time. This is the model for a maintainer who writes the changelog as work lands and wants a one-command release. It is local, auditable, and has no service to run.
release-plz: a pull request that is the release
name: release-plz
on:
push:
branches: [main]
jobs:
release-plz:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: release-plz/[email protected]
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}On every push to main, the action compares the repository against the last published version, works out what changed, and opens (or updates) a pull request titled something like "chore: release v0.5.0". The PR's diff is the release: the new version in Cargo.toml, the updated lockfile, and a changelog section generated from commits. A second job with command: release publishes to the registry, creates the tag and the GitHub release when that PR is merged. The human work shrinks to reading the diff and pressing merge.
The feature that distinguishes it is the version choice. release-plz can run cargo semver-checks to detect breaking API changes and pick the bump accordingly, and it reads Conventional Commits for the rest: a feat: gives a minor, a fix: a patch, feat!: a major. It will not let a breaking change ride out as a patch because the check fails the PR. That turns the version number from a decision remembered at release time into a property enforced on every merge.
[workspace]
changelog_update = true
semver_check = true
publish_allow_dirty = false
[[package]]
name = "client-internal"
release = false
[[package]]
name = "client"
changelog_path = "CHANGELOG.md"Comparing them on the same scenario
The library has an unreleased breaking change and a bug fix. Here is what each tool asks of the maintainer:
| Step | cargo release | release-plz |
|---|---|---|
| Decide the version | you pick minor, major or patch | computed from commits and semver-checks |
| Write the changelog | you, beforehand, in CHANGELOG.md | generated from commits, editable in the PR |
| Review | read the dry-run output | read the release PR diff |
| Publish | runs from your machine with your token | runs in CI with a scoped secret |
| Cadence | when you choose | a PR is always open; release when you merge |
| Needs | nothing but a terminal | CI, a token, a commit convention |
The difference that matters in practice is the commit convention. release-plz is only as good as the messages it reads. A project with disciplined Conventional Commit titles on every merged PR gets accurate versions and a changelog that needs light editing. A project that merges "updates" and "fix stuff" gets a release PR that says nothing, and the maintainer ends up rewriting it by hand, at which point cargo release would have been simpler. Enforce the format in CI on pull request titles if you choose the bot.
Workspaces and the publish order
Both tools understand that crates in a workspace depend on each other and must publish in dependency order, with each internal dependency carrying a version that matches. cargo release --workspace publishes members in topological order and bumps internal requirements in lockstep, or per crate with --package. release-plz releases each package whose files changed, and marks crates that are for internal use only with release = false. The rules for what crates.io accepts, including why path dependencies need versions, are in workspaces, features and publishing. A wise precaution on either tool is to run cargo publish --dry-run --workspace first, since the failure that wastes the most time is a partial publish where crate A is on the registry and crate B failed.
Credentials and blast radius
Publishing is the dangerous step, so look at where the token lives. cargo release uses whatever cargo login stored on the maintainer's machine, which means a laptop compromise is a registry compromise and releases depend on one person's availability. release-plz keeps the token as a CI secret, which removes the single maintainer bottleneck but widens the audience: anyone who can modify the workflow or merge to main can cause a publish. Scope the crates.io token to the packages and to the publish-update scope, require reviews on the release PR, and consider a protected environment that needs approval for the publish job. Tokens and rotation are covered in secrets management.
Which to pick
Use release-plz when you ship often, have more than one maintainer, and are willing to hold a commit convention. Use cargo release for a crate with one maintainer and few releases, or when you want everything to happen on a machine you control. Neither replaces a decision about what is breaking (what counts as a breaking change), and neither belongs on a crate that publishes twice a year, where a checklist is enough. The changelog side of the same pipeline is in changelogs and release automation.



