release, as an agent plugin
Runs the steps a release takes, in order, stopping at the first refusal. What
each step needs comes from .releasetools.yaml, so the same command releases a
Python package, a Rust crate and a monorepo's one plugin.
Installing
claude plugin marketplace add releasetools/agent-plugins --scope project
claude plugin install release@release-tools --scope project
codex plugin add release@release-tools
--scope project declares it in the repository rather than in whoever ran it,
so the next person to cut a release is offered the same plugin.
What it needs
releasetools/cli v0.4.0 or newer for the
checks and the bump (brew install releasetools/tap/releasetools-cli), gh
authenticated for the pull request and the workflows, and the
release-notes plugin for the changelog entry.
What it reads
projects:
- path: ./
manifest: pyproject.toml
changelog: CHANGELOG.md
bump: uv version {version}
release:
branch: main
merge: squash
checks: tests.yml
publish: publish.yml
trigger: tag
registry: https://pypi.org/pypi/my-package/{version}/json
| key | what it is | default |
|---|---|---|
branch | what a release is cut from | the remote's default branch |
merge | how the pull request lands: squash, rebase or merge | squash |
checks | the workflow that must be green on the release commit | none, and the wait is skipped |
publish | the workflow that publishes, watched to the end | none, and it stops at the push |
trigger | what starts publish: tag, dispatch, or a list of both | tag |
registry | a URL that must 404 before releasing | none |
Under tag the plugin pushes the signed tag and the push starts publish.
Under dispatch it runs publish with the version and the release commit, and
the workflow creates the tag. /release:setup declares which, and changes the
publish workflow to match.
The release tag is v<version>, as defined by the conventions. There is no
tag-shape setting in .releasetools.yaml.
bump is the command the project declares for setting its version, which every
ecosystem ships: uv version {version}, npm version {version} --no-git-tag-version, cargo set-version {version}. Nothing here parses or
rewrites a manifest.
The steps
Release preparation runs in a linked worktree and branch owned by this release. The main checkout stays clean on the remote's default branch. One operator owns its updates through publication; other agents work in their own linked checkouts. A release from another branch uses a separate publication worktree too, and its preparation branch starts at that release branch's fetched tip.
| 1 | reserve the main checkout, synchronize it, then run rt release::prechecks in the publication checkout |
| 2 | create a linked release worktree, prepare the notes and version, review the files, commit and push |
| 3 | the pull request, with the changelog entry as its body, and its checks watched |
| 4 | merge the recorded PR, synchronize main and verify the exact merged commit |
| 5 | under tag, sign and push a tag naming that commit; under dispatch, run the workflow that tags it |
The release tag leads back to the PR's merged commit even if the default
branch advances during verification. Under dispatch it names that commit, or
a built commit whose Source-Commit trailer does. The version and required checks are verified
at that SHA. Local builds and tests use a linked checkout at the verified
release commit, and artifacts are built from that checkout.
What it will not do
Move a tag, force anything, stash your work, delete an unmerged branch, retry a failed check in the hope that it passes, or release a version nobody named.
Where a publish workflow fails after it has already uploaded, the answer is to re-run that workflow, not to re-tag.
License
Apache-2.0. See LICENSE.