Conventional Commits: Structured Commit Messages for Automated Changelogs
Module 1 briefly introduced Conventional Commits as one option for writing clear commit messages. This lesson returns to that convention for a complete, deep treatment, revealing its full specification and — directly building on the previous lesson's Semantic Versioning — exactly how it enables fully automated version bumping and changelog generation, removing manual guesswork entirely.
Learning Objectives
- Write a commit message following the complete Conventional Commits specification, including type, optional scope, and description.
- Use the BREAKING CHANGE footer (or ! shorthand) to signal a breaking change.
- Understand how each commit type maps directly to a specific SemVer increment.
- Explain how tooling uses this structure to fully automate releases and changelogs.
Key Terms to Know Before Learning Conventional Commits
- Conventional Commits: A specification for structuring commit messages with a type, optional scope, and description, enabling automated tooling to parse and act on commit history.
- Commit type: The required prefix (like feat, fix, or chore) identifying the category of change a commit represents.
- Scope: An optional parenthetical addition to a commit type, identifying which part of the codebase the change affects (e.g., feat(auth): ...).
- BREAKING CHANGE footer: A specific commit message footer (or ! shorthand after the type/scope) explicitly signaling that a commit introduces a breaking change.
How the Conventional Commits Specification Actually Works
The full Conventional Commits format structures a message as:
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
The **type** is required and communicates the category of change, drawn from a small, standard set: `feat` (a new feature), `fix` (a bug fix), `docs` (documentation only), `style` (formatting, no code meaning change), `refactor` (a code change that neither fixes a bug nor adds a feature), `test` (adding or correcting tests), `chore` (routine maintenance, build process, tooling), among a few others some teams add.
The **scope**, optional and enclosed in parentheses immediately after the type, identifies *which part* of the codebase the change affects — for example, `feat(auth): add password reset flow` clarifies the feature specifically relates to the authentication module, which becomes especially valuable for filtering and organizing changelog entries in a project with many distinct areas.
The **description** is a short, imperative-mood summary (directly echoing Module 1's commit message guidance), and an optional longer **body** can follow after a blank line for additional context, exactly as covered back in Module 1.
The most consequential part of the specification, directly connecting to the previous lesson's Semantic Versioning, is how it signals a **breaking change** — this can be done two ways: adding a `!` immediately after the type/scope (`feat!: remove deprecated API`), or adding a `BREAKING CHANGE:` footer with an explanation, which can be combined with any type, even `fix`:
```
fix: correct legacy authentication behavior
BREAKING CHANGE: the previous (incorrect) authentication behavior is no longer
supported; callers must migrate to the new token format.
```
This structure enables a **direct, deterministic mapping** to Semantic Versioning's rules from the previous lesson:
- Any commit with a `BREAKING CHANGE` footer (or `!` shorthand) → triggers a **MAJOR** version bump.
- Any `feat:` commit (without a breaking change) → triggers a **MINOR** version bump.
- Any `fix:` commit (without a breaking change) → triggers a **PATCH** version bump.
- Other types (`docs`, `style`, `chore`, `test`, etc.) → typically don't trigger any version bump on their own, since they don't represent a functional change to the software itself.
This precise, parseable mapping is exactly what tools like `semantic-release` (mentioned briefly in the previous lesson) rely on to **fully automate** the release process: scanning all commits since the last release, determining the single highest-precedence version bump warranted across all of them (a MAJOR bump anywhere in the batch takes priority over any MINOR or PATCH commits also present), automatically tagging the new version accordingly, and generating a structured, categorized changelog (grouping entries under headings like 'Features' and 'Bug Fixes', derived directly from each commit's type) — with **zero manual version-number decision-making or changelog-writing required**, provided the team consistently follows this exact commit message structure.
Conventional Commits to Automated Release: Visual Walkthrough
Draw a pipeline: 'Commit history since last release' (showing a mix of tagged commits: 'fix: correct rounding error', 'feat: add dark mode', 'feat(api)!: remove deprecated endpoint (BREAKING CHANGE)', 'chore: update dependencies') → arrow labeled 'Automated tooling (e.g., semantic-release) parses commit types' → two outputs: 1) 'Version bump decision: BREAKING CHANGE present → MAJOR bump wins (highest precedence)' → 'New tag: v3.0.0', 2) 'Auto-generated CHANGELOG.md: grouped under headings — Features: dark mode / BREAKING CHANGES: removed deprecated endpoint / Bug Fixes: rounding error correction (chore: entries typically omitted from user-facing changelog).'
Conventional Commits Types: Quick Reference Table
| Commit Type / Signal | SemVer Impact | Example |
|---|---|---|
| BREAKING CHANGE footer or ! shorthand | MAJOR bump | feat!: remove deprecated legacy API |
| feat: (no breaking change) | MINOR bump | feat: add dark mode toggle |
| fix: (no breaking change) | PATCH bump | fix: correct discount rounding error |
| docs:, style:, chore:, test:, refactor: | No version bump (typically) | chore: update build dependencies |
Conventional Commits: Full Format Examples
# A new, backward-compatible feature (triggers a MINOR bump)
git commit -m "feat(checkout): add support for saved payment methods"
# A bug fix (triggers a PATCH bump)
git commit -m "fix(checkout): correct rounding error in bulk discount calculation"
# A breaking change using the ! shorthand (triggers a MAJOR bump)
git commit -m "feat(api)!: remove deprecated v1 endpoints"
# A breaking change using the full BREAKING CHANGE footer, with more explanation
git commit -m "fix(auth): correct legacy token validation
BREAKING CHANGE: the previous, incorrect validation behavior is no longer
supported. Callers must migrate to the new token format documented in
the v3 migration guide."
# A commit that doesn't affect the version number at all
git commit -m "chore: update CI pipeline dependencies"
Breaking Down the Conventional Commits Example
Each commit demonstrates a specific type and its corresponding SemVer implication, directly applying the previous lesson's rules. The `feat(checkout): ...` commit includes a scope narrowing it to the checkout module, and would trigger a MINOR bump if it were the highest-precedence change in a release batch. The `fix(checkout): ...` commit would trigger a PATCH bump on its own. The two breaking-change examples show both signaling methods — the compact `!` shorthand and the more explanatory `BREAKING CHANGE:` footer — both of which trigger a MAJOR bump regardless of the underlying type (note the second example uses `fix:` as its type, yet still triggers MAJOR because of the footer, illustrating that the breaking-change signal takes precedence over the base type). The final `chore:` commit demonstrates a type that typically doesn't affect versioning at all.
How Conventional Commits Powers Real Automated Release Pipelines
- semantic-release and similar automated release tools are widely adopted specifically because they eliminate the entirely manual, error-prone process of deciding a version number and writing release notes by hand, relying entirely on Conventional Commits' structured history.
- Many prominent open-source JavaScript/TypeScript projects enforce Conventional Commits via a commit-msg hook (directly connecting to Module 8's hooks coverage) combined with a tool like commitlint, ensuring every commit in their history is reliably machine-parseable.
- Monorepo tooling (the focus of a later lesson this module) frequently relies on Conventional Commits scopes specifically to determine which individual package within a monorepo actually needs a new version, based on which scope a given commit's changes affect.
- Engineering teams adopting Conventional Commits often report a secondary benefit beyond automation: the discipline of correctly categorizing every commit (feat vs. fix vs. chore) tends to improve overall commit hygiene and clarity, even independent of the automation payoff.
Conventional Commits Interview Questions and Answers
Q1. What is the full structure of a Conventional Commits message?
A required type (like feat or fix), an optional scope in parentheses identifying which part of the codebase is affected, a colon, and a short imperative-mood description, optionally followed by a longer body and footer(s) after blank lines — for example, 'feat(auth): add password reset flow'.
Q2. How does Conventional Commits directly enable automated Semantic Versioning?
Each commit type maps deterministically to a specific SemVer increment: a BREAKING CHANGE footer or ! shorthand triggers a MAJOR bump, a feat: commit (without breaking changes) triggers a MINOR bump, and a fix: commit triggers a PATCH bump. Automated tooling can scan all commits since the last release, determine the highest-precedence bump warranted, and tag the new version accordingly, with no manual decision-making required.
Q3. How would you signal that a commit introduces a breaking change, using two different methods?
Either add a ! immediately after the type (and scope, if present), such as feat!: remove deprecated API, or add a BREAKING CHANGE: footer with an explanation after the commit's description, which can be combined with any commit type, including fix:, and always triggers a MAJOR version bump regardless of the underlying type.
Conventional Commits Quiz: Test Your Understanding
1. Which Conventional Commits type would trigger a MINOR version bump under SemVer?
- fix:
- feat: (without a breaking change)
- chore:
- docs:
Answer: B. feat: (without a breaking change)
Explanation: A feat: commit represents new, backward-compatible functionality, which maps directly to a MINOR version bump under Semantic Versioning's rules, provided it doesn't also include a breaking change signal.
2. How can a Conventional Commit signal a breaking change?
- Only by using the word 'breaking' anywhere in the description
- By adding a ! after the type/scope, or including a BREAKING CHANGE: footer
- Breaking changes cannot be represented in Conventional Commits
- By using the type 'break:'
Answer: B. By adding a ! after the type/scope, or including a BREAKING CHANGE: footer
Explanation: Conventional Commits supports two ways to signal a breaking change: the compact ! shorthand immediately after the type/scope, or a full BREAKING CHANGE: footer with an explanation, either of which triggers a MAJOR version bump.
3. What does a fix: commit with a BREAKING CHANGE footer trigger under SemVer, despite its base type being fix?
- A PATCH bump, since the type is fix
- A MAJOR bump, since the BREAKING CHANGE footer takes precedence over the base type
- No version bump at all
- A MINOR bump
Answer: B. A MAJOR bump, since the BREAKING CHANGE footer takes precedence over the base type
Explanation: The BREAKING CHANGE signal always triggers a MAJOR bump regardless of the commit's base type — even a fix: commit with a BREAKING CHANGE footer results in a MAJOR version bump, since the breaking-change signal overrides the type's default implication.
Common Mistakes When Writing Conventional Commits
- Using an incorrect or inconsistent commit type, breaking the deterministic mapping to SemVer increments that automated tooling depends on.
- Forgetting to signal a genuinely breaking change with ! or a BREAKING CHANGE footer, causing an automated release tool to under-increment the version number.
- Assuming only feat: commits can introduce breaking changes, when a BREAKING CHANGE footer can actually be combined with any commit type, including fix:.
- Inconsistently applying the convention across a team, undermining the reliability of any automated tooling built on top of parsing commit history.
Conventional Commits: Exam-Ready Quick Notes
- Format: <type>[optional scope]: <description>, optional body, optional footer(s).
- Common types: feat, fix, docs, style, refactor, test, chore.
- Breaking change signals: ! after type/scope, OR a BREAKING CHANGE: footer — always triggers MAJOR, regardless of base type.
- SemVer mapping: BREAKING CHANGE → MAJOR, feat: → MINOR, fix: → PATCH, other types typically → no bump.
Conventional Commits: Key Takeaways
- Conventional Commits' full specification — type, optional scope, description, breaking change signals — provides a precise, machine-parseable structure.
- This structure maps deterministically to Semantic Versioning's rules, enabling fully automated version bumping with no manual decision-making.
- Automated tooling built on this foundation can also generate structured, categorized changelogs directly from commit history, eliminating manual release-note writing.
Frequently Asked Questions About Conventional Commits
Q1. What is the full structure of a Conventional Commits message?
A required type (like feat or fix), an optional scope in parentheses, a colon, and a short description — for example, feat(auth): add password reset flow — optionally followed by a longer body and footer(s) for additional context or breaking change explanations.
Q2. How do I signal that a commit introduces a breaking change?
Either add a ! immediately after the type (and scope, if present), like feat!: remove deprecated API, or add a BREAKING CHANGE: footer with an explanation — both trigger a MAJOR version bump under Semantic Versioning, regardless of the commit's base type.
Q3. How does Conventional Commits relate to Semantic Versioning?
Each commit type maps to a specific SemVer increment: a breaking change signal triggers MAJOR, feat: triggers MINOR, and fix: triggers PATCH. This precise, deterministic mapping is what lets automated tooling determine the correct next version number without any manual decision-making.
Q4. Can a fix: commit trigger a MAJOR version bump?
Yes, if it includes a BREAKING CHANGE footer. The breaking-change signal always takes precedence over the base commit type's default implication, so even a fix: commit with that footer results in a MAJOR bump.
Q5. What does an automated tool like semantic-release actually do with Conventional Commits?
It scans all commits since the last release, determines the highest-precedence version bump warranted across all of them, automatically tags the new version accordingly, and generates a structured, categorized changelog grouping entries by type — completely automating what would otherwise be a manual version-numbering and release-note-writing process.
Summary
The full Conventional Commits specification structures a message as `<type>[optional scope]: <description>`, optionally followed by a body and footer(s). The required type (feat, fix, docs, chore, and others) identifies the category of change, and an optional scope in parentheses identifies which part of the codebase is affected. A breaking change can be signaled two ways — a `!` immediately after the type/scope, or a `BREAKING CHANGE:` footer, which can be combined with any type, even `fix:` — and always takes precedence over the base type's default implication. This structure maps deterministically onto Semantic Versioning's rules from the previous lesson: a breaking change signal triggers a MAJOR bump, `feat:` triggers a MINOR bump, `fix:` triggers a PATCH bump, and other types typically trigger no version bump at all. This precise, parseable mapping is exactly what enables tools like `semantic-release` to fully automate the release process — scanning commit history since the last release, determining the highest-precedence version bump warranted, tagging accordingly, and generating a structured, categorized changelog — with zero manual version-number decisions or changelog writing required, provided the team consistently follows this exact commit message structure.