Commit Message Format#
We use Conventional Commits. They make the changelog automatic and tell you what changed at a glance.
The Format#
Every commit message looks like this:
type(scope): subject
[optional body]
[optional footer]
type - Required. Pick from the list below.
scope - Optional. The part of code you touched (cli, api, docs).
subject - Required. Short, lowercase, no period at end.
body - Optional. Explain why or add context.
footer - Optional. Reference issues like Closes #123.
Commit Types#
- feat
New feature. Bumps MINOR version (0.1.0 → 0.2.0).
Example:
feat(api): add endpoint for calendar anonymization- fix
Bug fix. Bumps PATCH version (0.1.0 → 0.1.1).
Example:
fix(cli): handle empty input files correctly- docs
Documentation changes only. No version bump.
Example:
docs: update installation instructions- refactor
Code change that doesn’t fix a bug or add a feature.
Example:
refactor(core): simplify property filtering logic- test
Adding or updating tests.
Example:
test(api): add tests for URL validation- chore
Build process, dependencies, tooling. Not user-facing.
Example:
chore: update dependencies- ci
CI configuration changes.
Example:
ci: add codecov integration- perf
Performance improvements.
Example:
perf(core): optimize calendar parsing- build
Build system or dependency changes.
Example:
build: update pyproject.toml metadata- style
Code formatting changes (whitespace, semicolons). No logic changes.
Example:
style: fix indentation in parser.py- revert
Reverting a previous commit.
Example:
revert: revert "feat: add calendar filtering"
Breaking Changes#
For breaking changes, add ! after the type:
feat(api)!: remove deprecated /v1/anonymize endpoint
Or put it in the footer:
feat(api): remove deprecated /v1/anonymize endpoint
BREAKING CHANGE: The /v1/anonymize endpoint is gone.
Use /anonymize instead.
Bumps MAJOR version (0.1.0 → 1.0.0).
Rules#
Subject in lowercase
No period at end
Max 72 characters for first line
Use imperative mood: “add feature” not “added feature”
Reference issues in footer when relevant
Good Examples#
feat(cli): add --output flag for file writing
Closes #42
fix(api): prevent SSRF in URL fetching
Add URL validation to block internal IP ranges.
Closes #58
docs: add examples for Python API usage
Bad Examples#
Added new feature
Update docs.
Fixed bug in API endpoint
FEAT: Add CLI flag
First three don’t follow the format. Last one is uppercase.
Pull Request Titles#
We use “Squash and merge” for PRs. The PR title becomes the commit message on main.
PR titles must follow the same format:
docs: add conventional commits configuration
feat(api): add calendar anonymization endpoint
fix(cli): handle empty input files
Why this matters:
When you squash merge, all commits in the PR are combined into one. The PR title is used as that commit message. Individual commits in the PR branch are discarded.
CI will check:
PR titles are validated automatically. Fix the title if the check fails.
Enforcement#
Local enforcement:
Install pre-commit hooks:
pip install pre-commit commitizen
pre-commit install --hook-type commit-msg
Hooks run commitizen to validate your commit messages before they’re created.
CI enforcement:
Two checks run on every PR:
Commit message validation: Checks all commits in the PR
PR title validation: Ensures PR title follows format (for squash merge)
Both must pass before you can merge.
To bypass locally (use sparingly):
git commit --no-verify -m "wip: experimenting with new approach"
Don’t abuse this. All commits merged to main must follow the format.