Conventional Commits and Changelog
Commit messages are the only documentation guaranteed to survive. Write them so a release note can be generated from them and a future engineer can git blame a line and understand why.
Part A — Committing
1. Inspect before writing
git status --short
git diff --staged # what will be committed
git diff # what is not yet staged
git log --oneline -15 # match the repo's existing style and scopes
Check for a commitlint config, .gitmessage, CONTRIBUTING.md or cliff.toml — follow local rules over this skill when they conflict.
2. Split mixed changes
If the staged diff contains more than one logical change (a bug fix plus a refactor plus formatting), split it: git add -p for hunks, then commit each separately. Never bundle formatting with behaviour changes. Never commit generated files, secrets, .env, or debug logging.
3. Write the message
<type>(<scope>): <imperative summary, ≤72 chars, no trailing period>
<why this change, what was wrong before, what trade-off was made — wrapped at 72>
BREAKING CHANGE: <what breaks and the migration path>
Refs: #123
Types: feat (user-visible capability), fix (bug), perf, refactor (no behaviour change), docs, test, build (deps, build system), ci, chore (nothing else fits), revert.
Rules:
- Summary in imperative mood: "add retry to webhook delivery", not "added" or "adds".
- Scope is the module or package, consistent with history (
auth,billing,cli). - Body explains why; the diff already shows what. Skip the body only for trivial changes.
!after the type/scope (feat(api)!:) plus aBREAKING CHANGE:footer for incompatible changes.- Reference issues in footers, not in the summary.
4. Verify
git commit -F msg.txt
git log -1 --format=%B # re-read it
npx commitlint --last # if configured
Part B — Releasing
1. Determine the version
From commits since the last tag (git log $(git describe --tags --abbrev=0)..HEAD --format='%s%n%b'):
- any
BREAKING CHANGEor!→ major (or minor while still 0.x, if the project follows that convention); - any
feat→ minor; - only
fix/perf/others → patch. Confirm the proposed version with the user before tagging.
2. Generate the changelog
Prefer an existing tool if configured (git-cliff, conventional-changelog, changesets, release-please, semantic-release). Otherwise produce this in CHANGELOG.md, newest first, Keep-a-Changelog style:
## [2.3.0] - 2026-09-01
### Breaking
- api: remove deprecated `/v1/users` endpoint; use `/v2/users` (#410)
### Added
- billing: support partial refunds (#402)
### Fixed
- auth: session cookie not refreshed on token rotation (#415)
### Performance
- ...
[2.3.0]: https://github.com/org/repo/compare/v2.2.1...v2.3.0
Group by type, drop chore/ci/test unless notable, link PR numbers, and rewrite entries in user language (what changed for them), not commit jargon.
3. Write release notes (for humans)
Above the changelog detail, add three to six sentences: the theme of the release, the one or two highlights, upgrade steps for breaking changes, and thanks to external contributors (git shortlog -sn <range>).
4. Cut the release
# bump version file(s) per ecosystem, then:
git commit -am "chore(release): v2.3.0"
git tag -a v2.3.0 -m "v2.3.0"
git push --follow-tags
gh release create v2.3.0 --title "v2.3.0" --notes-file RELEASE_NOTES.md
Only push and publish when the user has asked for it.
Output format
For commits: the list of commits created (hash + summary). For releases: proposed version with reasoning, the changelog section, the release notes, and the exact commands run or proposed.
Checklist
- One logical change per commit; formatting-only commits separate.
- Summary ≤72 chars, imperative, typed and scoped like the repo's history.
- Breaking changes flagged with
!and a migration note. - Version bump derived from commits and confirmed.
- Changelog entries readable by a user, linked to PRs.
- No secrets or generated artefacts committed.
Pitfalls
- "fix: fix bug" and "chore: updates" are useless; if you cannot name the change, you do not understand it yet.
- Squash merges lose individual commits; write the PR title in Conventional format so the squash commit is correct.
- Do not rewrite public history (
--amend, rebase) on shared branches to fix messages; add a follow-up commit. - Changelogs copied straight from commit subjects read as noise; edit for the audience.
- Tagging before CI passes on the release commit ships broken versions.