Generate a Keep a Changelog entry and the correct semver bump from the real commits since the last release.
Determine the range
- If
$ARGUMENTSnames a starting point (e.g.since v1.2.0), use that tag/ref as the lower bound:git log <ref>..HEAD. - If
$ARGUMENTSis empty, find the last release tag yourself withgit describe --tags --abbrev=0(the nearest tag reachable from HEAD — correct on maintenance/release branches). Do not substitutegit tag --sort=-v:refname | head -1: that returns the highest version anywhere in the repo and disagrees when a newer tag lives on another branch. If no tag exists, include every commit withgit log HEAD— do NOT use<root>..HEAD, because the exclusiveA..HEADrange drops the initial commit and breaks on multiple root commits. - State the resolved range (from-ref .. HEAD) before writing anything.
Read the commits
- Run
git log <range> --pretty=format:'%H%x09%s%x09%an%x1e%b%x1f' --stat. Always include%b:BREAKING CHANGE:footers live in the body and decide the major bump (line below), so you cannot omit it. The%x1e/%x1fbytes delimit the multi-line body so you can tell records apart. - Keep merge commits by default. Add
--no-mergesonly in squash-merge repos; in merge-based workflows the merge commit can carry the sole(#123)reference or the squashed context, so dropping it loses data. - Read every subject and body — do not sample; batch through all of them. Detect the commit convention (Conventional Commits
type(scope): …, gitmoji, or freeform) by inspecting the actual messages, and parse accordingly.
Classify each change (user-facing)
- Map to exactly one Keep a Changelog group by effect, not by the word in the subject:
- Added — new features/endpoints/flags/capabilities (
feat). - Changed — behavior, defaults, or output that shifts for existing users (
refactor/perfonly if a user notices). - Deprecated — still works but flagged for removal.
- Removed — deleted features, flags, or endpoints.
- Fixed — bug fixes (
fix). - Security — vulnerability fixes, CVEs, auth/crypto hardening.
- Added — new features/endpoints/flags/capabilities (
- Drop commits with no user-visible effect: internal refactors, test-only, CI, chore, docs, formatting, dependency bumps (unless a bump fixes a CVE → Security, or changes behavior → Changed).
- Rewrite each kept commit as one sentence describing what changed for the user, in present tense, capitalized, no trailing period, no commit hash or type prefix. Merge duplicates that describe the same change. Append
(#123)only if a PR/issue number is present in the message.
Decide the semver bump
- major if any commit is breaking:
!after type, aBREAKING CHANGE:footer, or anything in Removed / a backward-incompatible Changed. - else minor if anything landed in Added.
- else patch if only Fixed / Security / minor Changed.
- If the current major is
0, keep 0.x rules: breaking → minor, feature → patch, and say so. - Read the latest tag as the base version, compute the next version, and show the arithmetic (e.g.
1.4.2 → 1.5.0 (minor: new features, no breaking changes)).
Output
Print the entry ready to paste at the top of CHANGELOG.md, matching the file's existing heading style and date format if one exists:
## [<next-version>] - YYYY-MM-DD
### Added
- …
### Fixed
- …
- Include only non-empty groups, ordered as listed above; use today's date. End with the one-line bump rationale and a list of the commits you deliberately excluded, so the human can spot a miscategorization.
Rules
- Never invent, infer, or embellish changes not backed by a commit in the range; never dump raw commit subjects, hashes, or type prefixes into the entry.
- Always write from the user's perspective — what they can now do or no longer worry about — not from the code's.
- If the range is empty, say so and produce nothing.