Conventional Commits Linter & Generator: Write Commit Messages That Actually Automate Something

Six months into a project, someone asks “when did we drop support for the old auth flow” and the answer is buried somewhere in two hundred commits labeled “fix,” “update,” “changes,” and “wip,” none of which say what actually changed or why. This isn’t a discipline problem exactly, it’s a format problem: without a consistent, parseable structure, a commit history is just a diary nobody can query, and every bit of tooling that could otherwise be built on top of it, automated changelogs, automatic version bumps, a searchable record of what shipped when, has nothing reliable to parse.

Conventional Commits is a lightweight specification for exactly this: a commit message format structured enough that tools can actually read it, while staying close enough to how developers already write commit messages that it doesn’t feel like fighting a rigid template. This tool checks whether a commit message follows the spec correctly, or helps you build one from scratch, and explains what the format actually unlocks once you’re using it consistently.

Lint or Generate a Commit Message

What Conventional Commits Actually Requires

The format is a structured header, an optional body, and optional footers: type(scope): description, where type is one of a small, fixed set of values (feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert), scope is an optional, freeform indicator of what part of the codebase changed, and description is a short, present-tense summary of the change itself. A breaking change is marked either with a ! immediately after the type or scope, or with a BREAKING CHANGE: footer in the commit body describing what breaks.

This is deliberately minimal. It doesn’t dictate line length, doesn’t require a particular tense beyond convention, and doesn’t need special tooling installed just to write a compliant message by hand. The entire value comes from the type and breaking-change marker being consistent and machine-readable, which is the specific part that unlocks everything else.

What This Format Actually Automates

Automated changelog generation is the most immediate, tangible payoff. Tools like semantic-release or conventional-changelog can parse a project’s commit history and generate a categorized changelog automatically, grouping every feat commit under “Features,” every fix under “Bug Fixes,” entirely from the commit messages themselves, with zero manual changelog writing required at release time. A project without this structure either maintains a changelog by hand, which reliably falls out of date, or skips one entirely, leaving anyone upgrading with no clear record of what actually changed.

Automatic semantic version bumping follows the same mechanism. A fix commit since the last release suggests a patch bump. A feat commit suggests a minor bump. Any commit marked as a breaking change, through the ! or the BREAKING CHANGE: footer, suggests a major bump. Tools built around Conventional Commits can compute the correct next version number automatically based purely on what actually happened since the last release, which connects directly to how semver ranges in a package.json are meant to reflect the real nature of a change; a project that mislabels a breaking change as a patch commit undermines the exact trust that semver ranges depend on to be safe.

A searchable, filterable project history is the less flashy but genuinely useful everyday benefit. Filtering a commit log to only fix commits touching a specific scope turns “what bugs have we fixed in the payment flow” from an archaeology project into a one-line search, which matters more than it sounds like it should on any project that’s been running for more than a few months.

The Types, and When Each One Actually Applies

TypeUse for
featA new feature or capability visible to users of the code, triggers a minor version bump under semver automation
fixA bug fix, triggers a patch version bump
docsDocumentation changes only, no code behavior changed
styleFormatting, whitespace, missing semicolons, no logic change at all
refactorA code change that neither fixes a bug nor adds a feature, restructuring without behavior change
perfA change specifically improving performance
testAdding or correcting tests, no production code change
buildChanges to the build system or external dependencies
ciChanges to CI configuration files and scripts
choreRoutine maintenance that doesn’t fit any other category and doesn’t touch src or test files
revertReverts a previous commit

The line between fix and refactor is the one people get wrong most often. A change that corrects incorrect behavior is a fix, even if it also happens to clean up the surrounding code. A change that restructures working code without changing what it actually does, extracting a function, renaming a variable, reorganizing files, is a refactor, even if it happens to make a later bug easier to find. Mislabeling a genuine behavior fix as a refactor means automated tooling won’t flag it as a fix in the changelog, silently under-representing what a release actually addressed.

Scopes: Optional, but Worth Standardizing

A scope isn’t required by the specification, but leaving it consistently defined across a project multiplies the format’s usefulness considerably. A project that consistently scopes commits to auth, api, billing, and similar defined areas gets a genuinely searchable history by feature area, essentially for free, once a habit of using the same scope names consistently is established. A project where scopes are freeform and inconsistent, auth in one commit and authentication in another for the same actual area, loses most of that searchability, since tooling and humans alike now need to know every variant spelling that’s crept in over time.

Breaking Changes: The Part Most Teams Get Wrong

A breaking change needs to be marked explicitly, every time

The specification doesn’t infer breaking changes from context, it only recognizes the explicit ! marker or a BREAKING CHANGE: footer. A commit that removes a public function, changes a required API parameter, or otherwise breaks backward compatibility, but doesn’t include either marker, will be treated by any automated tooling as a normal, non-breaking change, potentially triggering only a patch version bump for a change that should have been major. This directly undermines the trust behind a well-designed API’s versioning strategy, since consumers relying on semver to signal safe versus risky upgrades are getting a false signal if breaking changes aren’t marked consistently.

The BREAKING CHANGE: footer should describe what actually breaks and, ideally, how to migrate, since this footer is often what actually gets surfaced prominently in an automatically generated changelog specifically to warn consumers before they upgrade blind.

Enforcing the Format in a Real Project

Commitlint, paired with a Git hook tool like Husky, is the standard way to enforce Conventional Commits automatically, rejecting a commit locally before it’s even created if the message doesn’t match the expected format, rather than relying on developers remembering the convention or catching violations in code review after the fact. This closes the gap between “the team agreed to use this format” and “every commit actually follows it,” which matters since a changelog generator or version bumper built on top of an inconsistently followed convention produces unreliable output exactly where consistency matters most.

Enforcing this check as part of your CI pipeline, in addition to or instead of a local git hook, catches anything that slips through a local hook that was skipped or bypassed, and it’s a natural addition to a pipeline that’s already running other automated checks; this comparison of CI/CD tools covers the broader pipeline setup this kind of check fits into.

Real Scenarios

A student project or solo repository

Adopting the format costs nothing and is worth doing purely as good practice, but automated changelog and version tooling built on top of it is rarely worth the setup effort for a project with one contributor and no real release process.

A small team shipping a real product

This is where the format earns its keep quickly. Automated changelogs and semver-based version bumping remove real, recurring manual work at release time, and a small team benefits from the searchable history almost immediately as the project grows past a size anyone can hold entirely in memory.

An open-source library maintainer

Close to essential. Consumers depending on your published package’s version numbers to signal safe versus breaking upgrades are trusting exactly the discipline Conventional Commits and consistent breaking-change marking provide, and getting this wrong erodes that trust in a way that’s hard to earn back.

Common Mistakes

Using a vague, generic type like chore for changes that actually fit a more specific category, out of habit or uncertainty about which type applies, flattens a changelog into something far less useful than it could be. A changelog dominated by “chore” entries tells a reader almost nothing about what actually shipped.

Forgetting the breaking change marker on a genuinely breaking commit is the most consequential mistake, since it directly corrupts the automated versioning signal the entire format exists to provide, and it’s the kind of error that’s invisible until a consumer upgrades expecting a safe patch release and gets a breaking change instead.

Writing a description that’s too vague to be useful even with the correct type and scope, “fix: bug fix” or “feat: updates,” technically satisfies the format’s syntax while providing none of its actual value. The format enforces structure, not substance, and a genuinely useful commit history still requires descriptions specific enough to mean something to someone reading them later without the original context.

And enforcing the format only through documentation or team agreement, without an actual commitlint check in place, tends to erode over time as deadline pressure mounts and the format starts feeling like friction rather than a habit. A checked, automated enforcement point is what keeps the convention followed consistently enough for the automation built on top of it to actually be trustworthy.

FAQ

Is Conventional Commits required for using semantic-release or similar tools?

Effectively yes for the automated versioning and changelog features specifically, since those tools parse commit types and breaking-change markers to determine what changed and what version bump it warrants. Without a consistent format, that automation has nothing reliable to parse.

What’s the difference between fix and refactor?

A fix corrects behavior that was actually wrong. A refactor restructures code without changing its behavior at all. A change that both fixes a bug and cleans up surrounding code is still a fix, since the behavior correction is what matters for changelog and versioning purposes.

Do I need a scope on every commit?

No, scope is optional in the specification. It’s worth using consistently on a project with distinct functional areas, since it makes the history searchable by area, but a small project or one without clear functional boundaries can reasonably skip it.

How do I mark a breaking change correctly?

Either add an exclamation mark immediately after the type or scope in the header, like feat!: or feat(api)!:, or include a BREAKING CHANGE: footer in the commit body describing what breaks. Either marker is sufficient on its own, and using one consistently is what keeps automated version bumping accurate.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top