Changelog on Tag
A tag is created; CHANGELOG.md gains a section for it, built from the commits
between the previous tag and this one. Nothing else changes. The commits are
raw material, not the output — an entry names what changed for the person
using the software.
What this skill does and does not do
Does: resolve a tag's commit range, group the commits, write or replace that
version's section in CHANGELOG.md, keep the compare links correct, and
install the automation that runs it on every future tag.
Does not: decide version numbers, bump manifests, create tags, publish GitHub Releases, or push anything. Those belong to a release process.
Set up the automation (one time per repository)
scripts/install-tag-hook.sh --hook --ci
--hook writes .git/hooks/reference-transaction, which fires the moment a
tag ref is created locally. --ci copies a workflow to
.github/workflows/changelog-on-tag.yml, which runs on push: tags so a tag
pushed from any machine is still covered.
Install both. The hook gives the person tagging an immediate, reviewable diff; CI is the backstop for tags created elsewhere or through the GitHub UI. Running both on the same tag is safe — the writer replaces a version's section rather than appending a second one.
Before running the installer, confirm with the user:
- The tag pattern that counts as a release (default
v*, semver only). - Whether the CI job may commit to the default branch. If that branch is
protected, use
--ci-mode prso the workflow opens a pull request instead. - The changelog path, if it is not
CHANGELOG.md.
Check what already exists first. If the repository uses git-cliff,
changesets, release-please or semantic-release, say so and stop: two
tools writing the same file will fight. This skill is for repositories with no
changelog automation.
Write a tag's section
1. Read the range
scripts/tag-range.sh v2.4.0
Prints the previous tag, the commit range, the tag date, and one record per
commit with subject, body and author. With no argument it uses the most recent
tag reachable from HEAD.
2. Draft the entries
Read references/entry-rules.md before writing a line. It carries the grouping, the phrasing rules and the exclusion list.
The short version: one entry per user-visible change, in the present tense, naming the effect rather than the commit. Merge commits, formatting, CI config, dependency bumps with no behaviour change and internal refactors do not get entries. If a tag range contains nothing user-visible, say so and write a one-line section rather than padding it.
3. Write the section
Pipe the drafted markdown into the writer:
scripts/changelog-write.py --tag v2.4.0 --stdin <<'EOF'
### Breaking
- The `/v1/users` endpoint is removed. Use `/v2/users`; the response shape is unchanged.
### Added
- Refunds can be issued for part of a payment.
### Fixed
- Sessions no longer end early when the access token rotates.
EOF
The writer inserts the section in version order, creates CHANGELOG.md with a
Keep a Changelog header if it is missing, replaces an existing section for the
same tag instead of duplicating it, and maintains the [2.4.0]: …/compare/…
link definitions at the foot of the file.
--from-commits skips the drafting step and groups commit subjects
mechanically by Conventional Commit type. That is what the hook and CI use
unattended. It produces a correct file, not a well-written one — when a person
is in the loop, draft the entries yourself.
4. Verify
scripts/changelog-write.py --tag v2.4.0 --check
git diff CHANGELOG.md
--check exits non-zero if the section is missing or the compare links are
broken. Read the diff before committing: confirm exactly one section changed
and that no earlier release was rewritten.
Backfilling
For a repository with tags but no changelog:
for tag in $(git tag --sort=version:refname); do
scripts/changelog-write.py --tag "$tag" --from-commits
done
Then rewrite the entries for the last two or three releases by hand. Older sections can stay mechanical; nobody reads them, and a wrong summary is worse than a commit subject.
Rules
- Never rewrite a section for an already-released version except to correct a factual error, and never in the same commit as a new release's section.
- The changelog commit lands after the tag, so its content is not inside the
tag. This is the accepted trade-off for tag-triggered generation. If a
release artifact must contain its own changelog, write the section against
HEADbefore tagging:scripts/changelog-write.py --tag v2.4.0 --range v2.3.1..HEAD. - Do not commit or push from the hook. It edits the file and prints one line. The person tagging decides what to commit.
- Do not invent entries for commits you cannot explain. Ask, or quote the commit subject unchanged.
- A tag that only moves (
git tag -f) is not a release. The hook ignores it.
Output
Report the tag, the previous tag, the commit count in the range, how many
became entries, and the path written. Name anything you dropped and why, and
anything you could not classify. Do not claim the changelog is updated without
having seen git diff output in this run.