Articles

What Are Conventional Commits? A Commit Convention

Conventional Commits structures commit messages as type(scope): description so tools can auto-generate changelogs and version bumps from git history.

The Lycoris Team The Lycoris Team · · 5 min read
Git version control interface on a laptop screen

Conventional Commits is a lightweight specification for formatting commit messages so that both humans and tools can parse them consistently. Instead of free-form messages like fixed stuff or updates, each commit starts with a structured prefix — fix: correct off-by-one in pagination — that machines can read to automate changelogs, version bumps, and release notes directly from git history.

The format

A conventional commit message has this shape:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

A few real examples:

feat(auth): add support for passkey login
fix(api): handle empty response body in retry logic
docs: update README install instructions
refactor(cache): extract eviction policy into its own module

The type says what kind of change this is. The optional scope in parentheses narrows it to a part of the codebase (auth, api, cache). The description is a short, present-tense summary — conventionally lowercase, no trailing period.

Standard types

The spec defines two types with special meaning for automated versioning, plus a set of conventional (but not semver-mapped) types most projects adopt:

  • feat — a new feature. Triggers a minor version bump under semantic versioning.
  • fix — a bug fix. Triggers a patch version bump.
  • docs — documentation-only changes.
  • style — formatting, whitespace, or other changes that don’t affect logic.
  • refactor — a code change that neither fixes a bug nor adds a feature.
  • perf — a performance improvement.
  • test — adding or correcting tests.
  • build — changes to the build system or dependencies.
  • ci — changes to CI configuration or scripts.
  • chore — routine maintenance that doesn’t fit elsewhere.

Only feat and fix carry defined version-bump semantics in the spec itself; the rest are conventions most tooling and teams have converged on anyway, because a consistent vocabulary makes commit history far easier to scan.

Breaking changes

A breaking change is marked either with a ! right after the type/scope, or a BREAKING CHANGE: footer:

feat(api)!: remove deprecated v1 endpoints

BREAKING CHANGE: the /v1/* routes have been removed. Migrate to /v2/*.

Either form signals a major version bump. This is the mechanism that lets a tool walk your commit history since the last release and compute the correct next version automatically — no more discussing in a PR whether a change is “actually” a major bump; it was declared at commit time.

Why teams adopt it

The payoff is automation, not just tidiness:

  • Automated changelogs. Tools like conventional-changelog or release-please group commits by type and generate a changelog section per release without a human transcribing it by hand.
  • Automated version bumps. Given the commit types since the last tag, a release tool can compute whether the next version is a patch, minor, or major bump under semver, and cut the release itself.
  • Enforceable at commit time. Pairing the convention with a git hook — commonly via commitlint run through Husky — rejects a non-conforming commit message before it ever lands, rather than relying on reviewers to catch it.
  • Readable history. Even without any tooling attached, git log --oneline becomes meaningfully more scannable when every entry starts with a type.

Where it fits with CI/CD

Conventional Commits is most valuable as one link in an automated release pipeline: commits are validated on push, merged to a trunk or release branch, and a release tool inspects the commit types since the last tag to decide the next version and changelog — see what CI/CD is for the surrounding pipeline concept. This tends to pair naturally with trunk-based development, where small, frequent commits to a shared branch make per-commit classification more meaningful than it would be against a workflow with long-lived feature branches and heavy rebasing that squashes history before it’s ever read by a changelog tool.

A worked example

Consider a small feature landing across a few commits. Written conventionally, the sequence tells its own story without opening a single diff:

feat(search): add debounced query input
test(search): cover debounce edge cases at 0ms and negative delays
fix(search): prevent stale results from overwriting a newer query
docs(search): document the debounce delay prop

A changelog generator walking this history sees one feat and one fix since the last release — enough on its own to compute a minor version bump, and enough to write a two-line changelog entry (“Added debounced search input; fixed a race condition with stale results”) without a human summarizing the PR by hand. Compare that to four commits reading wip, fix, more fixes, done — technically the same amount of work, but nothing in the history says what changed or how the version should move.

Common mistakes

A few patterns undermine the convention even when the format is technically followed:

  • Overusing feat and fix. If every commit is feat because it’s easier than thinking about the right type, the version-bump automation loses its signal — a release full of “features” that were really refactors or chores inflates the perceived scope of a release.
  • Vague descriptions that ignore the type. fix: fixed it still fails to tell a human (or a changelog) what actually changed; the type prefix doesn’t substitute for a clear description.
  • Forgetting the breaking-change marker. A change that removes or renames something public but ships as a plain feat or fix will get versioned as a minor or patch bump instead of the major bump it actually needs — the ! or BREAKING CHANGE: footer is doing real work, not just documentation.

Adopting it without disruption

You don’t need to enforce the full spec on day one. A common path:

  1. Start by writing commits in the format without any tooling — it costs nothing and builds the habit.
  2. Add commitlint as a commit-msg hook once the team is comfortable, so malformed messages fail fast locally instead of in review.
  3. Wire up automated changelog and version-bump generation once the commit history is consistently structured enough to trust.

Retrofitting old history isn’t necessary — automated tools only look at commits since the last tag, so the convention pays off going forward regardless of what came before.

The takeaway

Conventional Commits turns commit messages from free text into a small, structured vocabulary — type(scope): description, with feat and fix mapping to semver bumps and a ! or footer marking breaking changes. The format itself is nearly free to adopt; the value comes from what it unlocks — automated changelogs, automated version bumps, and commit-time linting — once a project’s history is consistent enough for tooling to trust it.

The Lycoris Team The Lycoris Team · · 4 min read

What Is Git Reflog? Recovering Lost Commits

Git reflog records every place HEAD and branch tips have pointed, letting you recover commits that look lost after a reset, rebase, or deleted branch.

#Git #Developer Tools #Version Control
The Lycoris Team The Lycoris Team · · 4 min read

Git Stash Explained: Shelve Changes Without Committing

Git stash saves uncommitted work to a hidden stack so you can switch branches cleanly. How stash, pop, apply, and stash branches actually work.

#Git #Developer Tools #Version Control