Why conventional commits matter
You've been committing code for years, but somewhere between "fixed bug" and "update" lies a gap that costs your team time. Conventional commits: a Git & GitHub guide for developers is about adopting a format that machines understand as well as humans do. When your commit messages follow a predictable structure, your CI/CD pipeline can automatically generate changelogs, bump semantic versions, and trigger the right deployments—all without manual intervention.
Conventional commits aren't just style guidelines. They're a contract between you and your tooling. Over the next six sections, we'll walk through the format, show you real-world examples, and explain how to wire this into your workflow so that every commit becomes data your team can act on.
The core commit types: feat, fix, docs, refactor, and chore
The Conventional Commits specification defines a small set of commit types that cover nearly every change you'll make. Each type signals intent and scope to both humans reviewing code and tools parsing the commit history.
feat: A new feature for the end user. This is a minor version bump in semantic versioning. When you add a new API endpoint, a new database field accessible through the UI, or a new command-line flag, you use feat:.
git commit -m "feat: add customer export to CSV format"In a CRM system, exporting customer lists is new functionality. The changelog tools will pick this up and list it under "Features" in the next release.
fix: A bug fix for the end user. This bumps the patch version. If a race condition was causing duplicate charges or a query was returning stale cache, you use fix:.
git commit -m "fix: prevent duplicate transaction processing in concurrent requests"Tools reading your log will know this is a patch-level change. Your release notes will automatically mention it under "Bug Fixes."
docs: Documentation changes only. No code changes, no version bump. This includes README updates, API documentation, inline comments that don't change logic, and architecture diagrams.
git commit -m "docs: clarify retry logic behavior in async worker documentation"Some teams ignore docs: commits in release notes entirely, treating them as housekeeping. Others include them for transparency. The point is that changelog generators can filter them out or highlight them separately.
refactor: Code changes that don't alter behavior. Extracting a method, renaming a variable for clarity, moving code between files, or simplifying logic all fall here. No patch bump, no user-visible change.
git commit -m "refactor: extract payment validation into dedicated service"Refactors are critical for maintainability, but they're not features or fixes. When reviewing history, "refactor" signals that if something broke, it wasn't this commit's intent—the logic was moved, not changed.
chore: Maintenance tasks. Dependency updates, build system changes, test infrastructure, CI/CD tweaks. Anything that keeps the project running but isn't visible to the end user.
git commit -m "chore: upgrade Node.js runtime to 20.x in Docker image"Chores are often filtered out of user-facing changelogs because end users don't care that you upgraded TypeScript. But your team does, and the log records it.
Signaling breaking changes with ! and BREAKING CHANGE
A breaking change bumps the major version and must be called out explicitly. Conventional commits support two ways to flag this: the ! modifier and the BREAKING CHANGE footer.
The ! goes between the type and the colon:
git commit -m "feat!: remove deprecated /v1/orders endpoint
BREAKING CHANGE: The /v1/orders endpoint is no longer available. Migrate to /v2/orders which returns orders in the new format."This signals to changelog generators that a major version bump is needed. The BREAKING CHANGE: footer in the commit body provides details about what broke and how to migrate.
Alternatively, if you're making a fix that introduces a breaking change (rare, but possible), you'd use:
git commit -m "fix!: correct API response format to match documented spec
BREAKING CHANGE: The 'created_at' field now returns ISO 8601 format instead of Unix timestamp. Clients expecting numbers must parse strings."Without the ! or BREAKING CHANGE, changelog tools assume a minor or patch bump. This protects you from accidental major releases and ensures breaking changes are deliberate, reviewed, and documented.
Scopes: adding context to your commits
A scope is optional but powerful. It narrows down what part of the system changed:
git commit -m "feat(auth): add two-factor authentication via authenticator app"
git commit -m "fix(payment): handle expired Stripe API keys gracefully"
git commit -m "refactor(database): consolidate user query helpers"Scopes vary by project. A web app might use auth, api, ui, database. A monorepo might use package names: pricing-service, notification-worker. The convention doesn't mandate a list—just pick naming that's consistent within your team and relevant to your architecture.
Tools use scopes to generate more detailed changelogs. Your changelog might group changes by scope, making it easier for different stakeholders to find what they care about.
Automatically generating changelogs and release notes
Once you've adopted conventional commits, changelog generation becomes a single command. Tools like standard-changelog, semantic-release, and others parse your commit history and construct human-readable release notes.
Here's what a generated changelog excerpt looks like:
## [2.3.0] - 2024-01-15
### Features
- auth: add two-factor authentication via authenticator app
- api: expose customer lifetime value in reporting endpoint
### Bug Fixes
- payment: handle expired Stripe API keys gracefully
- database: fix N+1 query in order detail page
### Refactoring
- service: consolidate user query helpers into repository layer
### Chores
- ci: add code coverage thresholds to pull request checksNo manual writing, no cherry-picking commits, no risk of forgetting what changed. Every conventional commit becomes a changelog entry automatically. In a CI/CD pipeline, you can even generate this during release and commit it back to the repository.
For teams using GitHub releases, tools can push the changelog directly into the release notes, making it instantly visible to users downloading the package.
Driving semantic version bumps in your release pipeline
Semantic versioning ties directly to commit types. Major.Minor.Patch maps to Breaking.Feature.Fix. With conventional commits, your CI/CD can determine the next version without guessing.
A simplified release workflow looks like this:
- Scan commits since the last release tag.
- Count
feat:commits (minor bump) andfix:commits (patch bump). - If any commit has
!orBREAKING CHANGE, bump major instead. - Tag the repository with the new version.
- Trigger deployment to production.
Here's a conceptual example in a GitHub Actions workflow:
- name: Determine next version
id: version
run: |
COMMITS=$(git log v$(cat VERSION)..HEAD --oneline)
if echo "$COMMITS" | grep -q "BREAKING CHANGE"; then
NEXT_VERSION=$(bump major)
elif echo "$COMMITS" | grep -q "^feat"; then
NEXT_VERSION=$(bump minor)
else
NEXT_VERSION=$(bump patch)
fi
echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT
- name: Tag release
run: git tag v${{ steps.version.outputs.next_version }}This removes human error from versioning. No more "Should this be 3.0 or 2.1?" debates. The commits dictate the version, and the version dictates the changelog.
Common mistakes and how to avoid them
Mixing types in one commit: A commit that fixes a bug and adds a feature should be split. One commit, one logical change, one type. This keeps history clean and makes it easier to revert specific changes if needed.
Vague subjects: feat: stuff or fix: issue defeats the purpose. Your subject line should answer "What did I do?" in a way that makes sense 6 months from now. Aim for 50 characters or less, present tense, no period.
Forgetting the breaking change marker: If you're removing a public API or changing a behavior that consumers depend on, use ! or BREAKING CHANGE:. Forgetting it means your users might deploy a "patch" that breaks their code.
Over-using chore: Not every dependency update is a chore. If you're upgrading a major version of a critical library that changes behavior, that might warrant feat: or fix:. Use your judgment; chore is for routine maintenance.
Inconsistent scopes: Don't call something feat(auth) in one commit and feat(authentication) in the next. Pick a scope naming convention early and document it in your contributing guide.
Key takeaways
- Conventional commits follow the pattern
type(scope): subjectand optionally include a body and footer. - The five core types are
feat,fix,docs,refactor, andchore. Each signals intent to both humans and tools. - Breaking changes are marked with
!after the type or with aBREAKING CHANGE:footer in the commit body. - Changelog generators parse conventional commits to auto-generate release notes, eliminating manual work and reducing errors.
- Semantic version bumps (major, minor, patch) are driven automatically by commit types, keeping versioning consistent and predictable.
- Consistency in commit messages across the team is more important than perfection; document your scopes and conventions in your contributing guide.






Comments
Loading comments…