Skip to main content

Release notes plugin

Drafts the changelog entry for a version from the commits since the last tag, rules on each commit against a single test, and writes what survives to CHANGELOG.md and to a scratch file the release itself can publish.

This one is an agent plugin and nothing else. There is no CLI behind it and no GitHub Action, so a checkout of your own repository and git are all it works with.

GitHub Repository

The test

Can a person running the software observe it? A different result, a different line of output, a different exit code, a new flag, a changed message. If none of those changed, it is not an entry, whatever it cost to build.

That leaves out refactors, new tests, CI changes, documentation, and dependency bumps that change no behaviour. A command gaining an internal wrapper is not news. The same command refusing where it used to delete is.

The ruling comes before the prose

Every commit in the range gets a row, and the table is on your screen before a word of the entry is written:

FieldWhat goes in it
shaas printed
observableyes or no, against the test above
categoryone of the six Keep a Changelog categories, or none
breakingyes or no
entryone sentence: what a person running it sees differently
evidencethe paths in the diff that show it
discrepancywhere the message and the diff disagree, empty when they do not

Two things about that table earn their keep.

Coverage is decided per commit rather than by one instruction covering the whole range. Braintrust measured model-written release notes at writing quality 1.0 and comprehensiveness 0.5: the notes read perfectly and left changes out. Turning a single global dial traded one against the other and oscillated. A per-commit ruling you can read is what stops a change going missing quietly.

The discrepancy row is the reason the diff is read at all. A commit message is a claim and the patch is the evidence. A commit saying "fix typo" that also moves a default is caught in that row or nowhere.

Install

Claude Code and Codex install it from the marketplace:

claude plugin marketplace add releasetools/agent-plugins
claude plugin install release-notes@ReleaseTools
codex plugin marketplace add releasetools/agent-plugins
codex plugin add release-notes@ReleaseTools

Hermes and Antigravity clone the repository and read plugin.json at the plugin root, so neither needs a marketplace:

hermes plugins install releasetools/agent-plugins/plugins/release-notes
agy plugin install https://github.com/releasetools/agent-plugins

agy reads plugins/ as a bulk directory and takes every plugin in it. hermes takes the one its subdirectory names.

What it needs

git, and a repository with commits. gh is optional: --pr fetches the pull requests a commit landed through, which is worth a call under a merge workflow and nothing under squash merges, where the commit body already is the pull request body.

Commands

Command
/release-notes:draft <version>Rule on every commit, then write the entry
/release-notes:helpWhat the plugin does, and what it will not

draft takes the version as an argument and asks when it is missing. It never guesses one from the commits, because a version guessed wrong is a version somebody has to notice.

The two files it writes

CHANGELOG.md gets a ## <version> - <ISO date> section above every older release and below the file's preamble, in Keep a Changelog categories. A repository without one gets it created. A repository whose entries carry a heading outside the six keeps carrying it, because the skill reads the existing file before writing and follows what it already does.

RELEASE_EDITMSG gets the same body with no version heading, for whatever publishes the release. It sits in $GIT_DIR next to git's own COMMIT_EDITMSG and TAG_EDITMSG, which puts it outside the work tree, where nothing can commit it by accident. Resolve it with git, never by joining .git/:

git rev-parse --path-format=absolute --git-path RELEASE_EDITMSG

Both halves of that matter. Without --path-format=absolute the answer is relative in a main worktree and absolute in a linked one. With --git-path a linked worktree resolves to its own file, so two worktrees preparing releases cannot overwrite each other.

Given those, a release procedure needs to know nothing about how the entry was produced:

gh release create "v${VERSION}" --notes-file "$(git rev-parse --path-format=absolute --git-path RELEASE_EDITMSG)"

The file is truncated when a draft starts, so a run that dies halfway leaves an empty file rather than the previous release's body. If it and CHANGELOG.md ever disagree, CHANGELOG.md is the one that went through review.

What it will not do

It drafts an entry and writes two files. It never tags, commits, pushes, publishes or opens a pull request, and it never picks the version number.

Nor does it put a pull request number, an issue number, a branch name, a commit sha or an author handle in an entry. The reader of a changelog is deciding whether to upgrade, not auditing the work, and the compare link on the release already carries all of it.