How to Write Commit Messages That Make Your Changelog Write Itself
Most developers treat commit messages as throwaway notes to themselves. "fix", "wip", "update stuff", "more changes." Then, at release time, someone is asked to write a changelog and discovers the git history is useless for the task. The information about what changed exists, but it was never captured in a usable form. Good commit messages flip this: they turn your git history into the raw material for a changelog that practically writes itself. This guide shows you how.
Why commit messages matter for documentation
A commit message is the only place where the intent of a change is recorded at the moment it is freshest in the author's mind. The diff shows what changed mechanically; the commit message should explain what changed meaningfully. That meaning is exactly what a changelog needs.
When commit messages are written well, they become a structured, chronological record of meaningful change — which is the definition of a changelog. When they are written badly, that record is lost, and someone has to reconstruct it later from diffs and memory, which is slow and error-prone. Investing a few extra seconds per commit pays off every single release.
Introduction to conventional commits
Conventional Commits is a lightweight specification for commit messages that makes them both human-readable and machine-parseable. It is widely adopted precisely because it solves the changelog problem: a tool can read conventional commits and generate a categorized changelog automatically.
The core idea is a small, fixed vocabulary of change types plus a structured format. Once a team adopts it, the git history stops being noise and becomes data — data that powers automated changelogs, automated version bumps, and clearer code review.
The conventional commit format explained
The format is:
type(scope): description
[optional body]
[optional footer]
The type is one of a small set (covered below). The scope is an optional noun in parentheses indicating what part of the codebase changed — auth, api, billing. The description is a short, imperative summary: "add OAuth login," not "added OAuth login" or "adds OAuth login." The body optionally explains the why and any context. The footer carries metadata like BREAKING CHANGE: notes or issue references.
A breaking change is signaled either with a ! after the type/scope (feat(api)!: remove deprecated endpoint) or with a BREAKING CHANGE: footer. This is important because breaking changes drive major version bumps and need prominent changelog placement, as we discuss in our changelog writing guide.
Practical examples: feat, fix, docs, refactor, etc
Here are the common types with examples:
- feat — a new feature:
feat(search): add fuzzy matching to repo search - fix — a bug fix:
fix(auth): prevent session expiry during active use - docs — documentation only:
docs(readme): clarify local setup steps - refactor — code change that neither fixes a bug nor adds a feature:
refactor(api): extract validation into middleware - perf — a performance improvement:
perf(db): add index on user_id for faster lookups - test — adding or fixing tests:
test(billing): cover mid-cycle proration - chore — maintenance, tooling, dependencies:
chore(deps): bump typescript to 5.6 - style — formatting only, no logic change:
style: apply prettier to api routes
For a changelog, feat and fix are the headline categories — they become "Added" and "Fixed." perf often appears too. docs, test, style, and chore are typically excluded from the changelog because they have no user-facing impact, which is exactly the kind of filtering a generator can do automatically when commits are typed.
Want this automated for your repos?
Pushpen connects to GitHub and generates your documentation automatically on every push.
Start free — no credit card requiredSetting up conventional commits in your team
Adoption is mostly cultural, supported by light tooling. Start by documenting the convention in CONTRIBUTING.md with the type list and a few examples. Then lower the friction: provide a commit message template, or an interactive prompt that walks contributors through type, scope, and description.
Crucially, do not rely on memory alone — add a check. A commit-message linter in a pre-commit hook or in CI rejects malformed messages before they enter history. Within a few weeks the format becomes automatic. The payoff compounds: clearer code review (reviewers see intent at a glance), automated versioning (the type determines the bump), and automated changelogs (the history is already structured).
Tools for enforcing conventional commits
A few categories of tooling help. Commit linters validate that each message follows the convention and can run in a pre-commit hook or CI. Interactive commit tools prompt the author through the format so they do not have to remember it. Changelog and release tools read the conventional history to generate changelogs and pick version bumps automatically.
The combination — a linter to enforce, a prompt to assist, and a generator to consume — turns conventional commits from a guideline into a reliable pipeline. The discipline is enforced mechanically, so it does not depend on everyone remembering on every commit.
How commit messages fuel automated changelog generation
This is where it all pays off. When your history is conventional, generating a changelog is mechanical: group commits by type, map types to changelog categories, translate each description into a reader-facing entry, and order by version. You can do this on demand with the free changelog generator — paste your git log and watch structured commits become a clean changelog.
For continuous, hands-off changelogs, Pushpen reads each push and adds the appropriate changelog entry automatically, in the right category and in reader-facing language, opening a pull request for review. Good commit messages make this even better, because the clearer your commit intent, the cleaner the generated entry. Our guide on generating a changelog from git commits covers the full automation setup. And if you want to know how well your current commits would support automation, the free commit message analyzer scores how many of your commits follow the convention.
Frequently Asked Questions
What is the conventional commits format?
type(scope): description — for example feat(auth): add OAuth login. The type comes from a fixed set (feat, fix, docs, refactor, perf, test, chore, style), the scope optionally names the affected area, and the description is a short imperative summary. Breaking changes are marked with ! or a BREAKING CHANGE: footer.
Why do conventional commits help with changelogs?
Because the type tells a generator how to categorize the change and the description becomes the entry. A feat becomes an "Added" item, a fix becomes "Fixed," and non-user-facing types like chore are filtered out — all automatically.
Which commit types belong in a changelog?
Typically feat, fix, and perf, since they have user-facing impact. docs, test, style, and chore are usually excluded. A good generator applies this filtering for you when commits are properly typed.
How do I enforce conventional commits on my team?
Document the convention, provide a template or interactive prompt to lower friction, and add a commit-message linter in a pre-commit hook or CI to reject malformed messages. Enforcement should be mechanical so it does not depend on memory.
Can I check whether my commits are changelog-ready?
Yes. The free commit message analyzer scores what percentage of your commits follow the convention and whether a changelog could be auto-generated from them, with suggestions for improvement.
Related articles
Tired of outdated documentation?
Start free