Skip to content

releasetools/release-notes

v0.5.0Apache-2.0

Draft a release's changelog entry from the commits since the last tag

release-notes, as an agent plugin

Draft the changelog entry for a release by reading every commit since the last tag against its own diff, ruling on each one, and writing what survives to CHANGELOG.md and to a scratch file the release itself can publish.

The ruling is the point. A model asked for release notes writes them well and leaves changes out, so every commit in the range gets a row saying whether a person running the software can observe it, and the table is on the screen before a word of the entry is written.

Install

Add the marketplace once, then install the plugin.

Claude Code:

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

Codex:

codex plugin marketplace add releasetools/agent-plugins
codex plugin add release-notes@release-tools

Hermes and Antigravity clone this repository and read plugin.json, so they install from GitHub with no marketplace to add:

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

agy takes every plugin in the repository. 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 at all under squash merges, where the commit body already is the pull request body.

Commands

Command
/release-notes:write [--pr <number>]This change's note, into the pull request and the changelog
/release-notes:prepare <version> [--path <dir>]A release's entry, collated from the notes its changes declared
/release-notes:helpWhat the plugin does, and what it will not

One change at a time

/release-notes:write is run on a branch, before the change is merged, which is while the person who made it still knows why. It rules the change against the one test, writes the note to $GIT_DIR/NOTE_EDITMSG, puts the block in the pull request's description and the entry in the changelog of every project the change lands in.

Which projects those are, where each keeps its version and its changelog, and whether the repository writes entries per change at all are read from .releasetools.yaml at the repository root, the file every releasetools tool reads. A note that lands in the wrong package's changelog is worse than no note, and that file is what stands between the two. A repository without one is told so and nothing is written; npx @releasetools/config adopt writes a starter.

It never writes a manifest version. Where a project's version is one that was already released, it says so and names what the change asks for instead, and the note stays in NOTE_EDITMSG until the bump lands.

A note the author wrote

A change can carry its own release note, in a fenced block in its commit message or pull request description:

```release-note
Batch mode processes up to 10,000 records per request. Enable it with the
batch=true query parameter.
```

That note is what gets published, taken as written, because the person who wrote it was there. NONE in the block says a reader can observe nothing, and the commit is ruled that way without its patch being read. A change with no block is drafted from its diff at release, and /release-notes:prepare's ruling table says which of the two each row came from.

The block has to reach the commit that lands on the default branch, because that is the copy a release reads months later. A merge that writes its own commit body copies the block into it verbatim.

The releasetools conventions define the block, and the subject's Conventional Commits type supplies the category: a feat lands under Added, a fix under Fixed, and a ! or a BREAKING CHANGE: footer makes it breaking. None of that is guessed from the prose.

One subtree, or the whole repository

--path <dir> releases a subtree: a plugin in a monorepo, a package in a workspace. The range covers only the commits that touched it, the entry lands in <dir>/CHANGELOG.md, and the scratch file is that subtree's own, so a subtree draft and a repository draft can be in flight at once. A subtree is usually versioned on its own rather than tagged, so pass --since with it.

/release-notes:prepare 0.3.0 --path plugins/release-notes

A release written up after the fact

An entry is dated today, which is right for a release being cut now. --at <rev> dates it from that commit instead, so a version released in August and written up in September is dated August. It is the way to give a subtree that was versioned before it kept a changelog one section per version, each dated from the commit that released it.

/release-notes:prepare 0.1.0 --path plugins/mutex --since v0.0.9 --at eb4814e

A date read off a commit cannot be mistyped, and cannot contradict the history the entry describes.

A repository can keep both kinds: one changelog per released thing, and one for itself. They are drafted separately and nothing reconciles them, so the repository's entry can summarise what the subtree entries said or say something none of them did.

The 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. The file is created with a preamble when the repository has none.

RELEASE_EDITMSG gets the same body with no version heading, for whatever publishes the release. NOTE_EDITMSG beside it holds one change's note while it is being written. It lives 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 the way the plugin does, 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.

A release procedure that reads that file needs to know nothing else 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 entries and writes files. It never tags, commits, pushes, publishes or opens a pull request, and it never picks the version number: that is an argument, because a version guessed from commits is a version somebody has to notice is wrong.

License

Apache-2.0. See LICENSE.