Skip to content

Git Commit Conventions

This document defines the git commit message format (Conventional Commits types/scopes), the version-bump rules that follow from it, and how the changelog and releases are generated from that history. Read it before making any commit to this repository.


All commit messages must follow the Conventional Commits specification:

type(scope): description

These six types drive version bumps and commit review; only feat, fix and breaking changes raise the version:

Type Description Version bump
feat A new feature or capability Minor
fix A bug fix Patch
refactor Code restructuring without behavior change None
docs Documentation-only changes None
chore Maintenance, dependencies, tooling None
BREAKING CHANGE: or ! Incompatible API change Major

Scopes identify the area of the codebase affected. Common scopes:

Scope Area
backend Backend Python code
frontend Frontend TypeScript/React code
auth Authentication & authorization
charts Charting & data visualization
ci CI/CD pipeline
security Security hardening
accounts Account management
snapshots Snapshot feature
i18n Internationalization
deps Dependency updates

Scopes are not limited to this list — use a descriptive scope that matches the feature area.

The scope must always be a real feature or module name — never an internal ticket ID, worktree/branch name, or session identifier; put that kind of identifier in the commit body or a trailer instead, if it’s useful there.

Real examples from project history:

fix(auth): revert SameSite=None to Lax
fix(ci): resolve 3 CI failures
feat(charts): add net worth change waterfall chart
  • Keep the description in imperative mood (“add”, not “added” or “adds”)
  • Do not end the description with a period
  • Limit the subject line to 72 characters
  • Use the body for additional context (wrapped at 72 chars) when needed
  • Reference issues in the footer: Refs: #123 or Closes: #123
  • GPG-sign every commit
  • Never reference AI/tooling authorship or a session/task identifier in the commit message
  • Never run git tag yourself — see Release Process below

Any commit that adds, removes, or changes behavior in backend/app/routers/*.py bumps backend/pyproject.toml’s version field in the same commit, using the same feat=minor/fix=patch/BREAKING=major mapping as the table above — scoped to the API surface only, so a router change bumps it while a service-layer refactor or test-only change does not. The same commit must re-export backend/openapi.json and refresh backend/uv.lock (cd backend && uv lock); see Contributing.

This is the backend package version — a separate number from the app-wide release version described in Releases; the two are never conflated.


The project uses git-cliff to generate CHANGELOG.md from conventional commit history.

Configuration lives in cliff.toml at the repository root. It defines:

  • Commit parsing rules (type + scope extraction)
  • Grouping by commit type
  • Sorting and formatting

Changelog entries are grouped into sections (see cliff.toml’s commit_parsers for the exact match order):

  • Features — feat commits
  • Bug Fixes — fix commits
  • Refactor — refactor commits
  • Documentation — doc/docs commits
  • Performance — perf/improvement commits
  • Styling — style commits
  • Testing — test commits
  • Miscellaneous Tasks — build, chore, ci commits
  • Security — fix(security)/feat(security) commits, or any commit whose body has a security: line
  • Deprecated — feat(deprecate)/chore(deprecate) commits
  • Revert — revert commits

This changelog grouping is a separate concern from the version-bump table above: it’s about which section of CHANGELOG.md a commit lands in, not whether it bumps backend/pyproject.toml’s version.

A perf/test/style commit gets its own changelog section for readability but bumps nothing (same as chore/refactor/docs); fix(security) bumps a patch (same as any other fix) while also being pulled into the Security section.

git cliff -o CHANGELOG.md is how the release workflow regenerates CHANGELOG.md. Contributors never run this themselves and never hand-edit CHANGELOG.md in a PR — it is auto-generated and any manual edit gets overwritten.


Contributors do not create git tags — vX.Y.Z tags are cut separately, by the release process, not by running git tag from a feature branch or PR.

At a high level, once a tag is cut:

  1. Tag — A semantic version tag vX.Y.Z is created as part of the release process (not by a contributor commit)
    • X — breaking changes
    • Y — new features (backward compatible)
    • Z — bug fixes / patches
  2. CI builds — Pushing the tag triggers CI to build and publish container images
  3. Changelog — git-cliff generates the CHANGELOG.md entry for the release

See Releases for who/what actually cuts the tag.


  • main — The integration branch. Always deployable.
  • Feature branches — Created from main, named descriptively (e.g., feat/waterfall-chart, fix/auth-samesite)
  • Pull requests — All changes merge to main via PR. CI must pass before merge.
flowchart LR accTitle: Branch and pull request workflow accDescr: Each change starts on its own branch off main, such as feat/, fix/ or chore/, and returns to main through a pull request. main["main"] main --> f1["feat/waterfall-chart"] --> pr1["PR → main"] main --> f2["fix/auth-samesite"] --> pr2["PR → main"] main --> f3["chore/deps-update"] --> pr3["PR → main"]