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