Skip to content

02 — Commits, branches & versioning

Commit messages: Conventional Commits 1.0.0

<type>(<scope>): <imperative summary, lower case, ≤ 72 chars, no trailing period>

<body: what and why>

<footers: BREAKING CHANGE: …, Refs: #12, Docs-Reviewed: …>

Types and scopes are listed in standards.toml ([commits]). A scope is required for feat, fix, perf and refactor.

ID Rule Enforced by
COM-1 Every commit follows the format above, with a listed type and scope. commit-msg hook (standards check-commit-msg); CI standards check-commits on every PR commit
COM-2 A breaking change has ! after the scope and a BREAKING CHANGE: footer. standards check-commit-msg
COM-3 One logical change per commit. Formatting-only changes go in their own style commit. review
COM-4 The PR title is a valid Conventional Commit, because it becomes the squash-merge message. CI standards check-pr-title
COM-5 Commits never contain secrets, generated artifacts or large data. gitleaks, detect-private-key, check-added-large-files
COM-6 fixup! / squash! commits are allowed locally, but never reach main. CI standards check-commits

Branches

ID Rule Enforced by
BR-1 Trunk-based: main is always releasable. Work happens on short-lived <type>/<topic> branches (e.g. feat/rule-compiler). branch-name hook (standards check-branch-name), CI on the PR head
BR-2 main is protected: no direct push, PR required, required checks green, linear history, squash merge only, no bypass for admins. GitHub ruleset
BR-4 A squash commit takes the PR title as its header and the branch commits' messages as its body, so footers (BREAKING CHANGE:, Docs-Reviewed:) survive the merge and still drive versioning and doc impact on main. repository setting squash_merge_commit_message = COMMIT_MESSAGES
BR-3 A PR is up to date with main before merging. ruleset "require branches to be up to date"

Versioning: Semantic Versioning 2.0.0, from tags

The git tag is the version. pyproject.toml declares dynamic = ["version"], and hatch-vcs reads it from the latest vX.Y.Z tag. The package exposes it as <package>.__version__. Because the code has no version of its own, the code and the tag cannot disagree.

  • fix, perf → patch. feat → minor. BREAKING CHANGE → major (minor while 0.y.z). Other types do not release.
ID Rule Enforced by
VER-1 No version is written by hand: pyproject.toml has no static version. version-tag hook (standards check-version)
VER-2 Every releasable commit on main is covered by a vX.Y.Z tag pushed to GitHub. release job, then standards check-version --released
VER-3 Before a push, the latest tag reachable from HEAD exists on origin and points to the same commit. version-tag hook
VER-4 Release notes are generated by commitizen from commit messages into the GitHub Release. No CHANGELOG.md is maintained by hand. release job
VER-5 Every release has the wheel, the sdist, a CycloneDX SBOM and build provenance attached. release job

Release flow (automatic, on every merge to main)

  1. All CI jobs pass on the new main commit.
  2. The release job refuses to run while a release-blocker issue is open (CI-7).
  3. cz bump --get-next computes the next version from the commits since the last tag. If none is releasable, the job stops.
  4. The job creates an annotated tag vX.Y.Z and pushes it. No commit is pushed to main, so branch protection needs no bypass.
  5. It runs uv build, generates the SBOM, attests provenance and creates the GitHub Release with the generated notes.
  6. standards check-version --remote origin --released asserts the result.