Enforce Conventional Commits via commit-msg hook #70

Closed
opened 2026-06-16 12:13:08 +00:00 by james · 0 comments
Owner

Follow-up from #16 (see ADR-0014 "Consequences").

The release pipeline added in #16 generates release notes with git-cliff configured to parse Conventional Commits. Commits that don't match a conventional prefix land in a catch-all "Other" group — which works as a transition period but degrades release-note quality if it becomes the steady state.

Today the convention is documented (CLAUDE.md, ADR-0014) but unenforced. Anyone — including future-me — can land a non-conventional commit message and nothing catches it until a release goes out with a half-empty changelog.

Scope

  • Add a commit-msg hook to lefthook.yml that rejects commit messages not matching the Conventional Commits grammar (<type>(<optional scope>)!?: <subject>).
  • Allowed types should mirror what cliff.toml parses: feat, fix, perf, refactor, docs, test, build, ci, chore, revert.
  • Pick the linter — options include commitlint (npm, well-known, JSON config), a small regex shell script (zero deps), or a lighter pure-Go tool. Document the choice; "zero new tool" is a fine answer if a regex covers the cases.
  • Decide and document the failure mode for the maintainer who needs to land a one-off non-conformant commit: LEFTHOOK_EXCLUDE=commit-msg git commit ... mirrors the existing gitleaks-skip pattern; --no-verify stays the last-resort.
  • Mirror the rule in a pull_request CI check (probably a small step in the existing secrets.yml or a dedicated commit-lint.yml) so a contributor who skipped local hooks still gets caught at PR time. The CI check should scan the PR's commit range only, not the whole branch history.

Acceptance criteria

  • git commit -m "did a thing" is rejected locally with a message pointing at the convention doc.
  • git commit -m "feat(release): wire conventional-commit gate" succeeds.
  • A PR with a non-conventional commit fails the new CI check; one with conventional messages passes.
  • Documentation in CLAUDE.md / docs/ci.md updated to point at the enforcement, not just the convention.
  • An entry in lavamoat.allowScripts for whatever new dev dep gets pulled in, if any (commitlint pulls a handful).

Out of scope

  • Rewriting old non-conventional commits. The first few releases will keep their "Other" bucket; that's acceptable.
  • Auto-suggesting commit messages, AI commit-message tools, or anything that fixes the message for the user.

Part of epic #2.

Follow-up from #16 (see [ADR-0014](docs/adr/0014-release-pipeline.md) "Consequences"). The release pipeline added in #16 generates release notes with `git-cliff` configured to parse [Conventional Commits](https://www.conventionalcommits.org/). Commits that don't match a conventional prefix land in a catch-all "Other" group — which works as a transition period but degrades release-note quality if it becomes the steady state. Today the convention is documented (CLAUDE.md, ADR-0014) but unenforced. Anyone — including future-me — can land a non-conventional commit message and nothing catches it until a release goes out with a half-empty changelog. ## Scope - Add a `commit-msg` hook to `lefthook.yml` that rejects commit messages not matching the Conventional Commits grammar (`<type>(<optional scope>)!?: <subject>`). - Allowed types should mirror what `cliff.toml` parses: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `build`, `ci`, `chore`, `revert`. - Pick the linter — options include [commitlint](https://commitlint.js.org/) (npm, well-known, JSON config), a small regex shell script (zero deps), or a lighter pure-Go tool. Document the choice; "zero new tool" is a fine answer if a regex covers the cases. - Decide and document the failure mode for the maintainer who needs to land a one-off non-conformant commit: `LEFTHOOK_EXCLUDE=commit-msg git commit ...` mirrors the existing gitleaks-skip pattern; `--no-verify` stays the last-resort. - Mirror the rule in a `pull_request` CI check (probably a small step in the existing `secrets.yml` or a dedicated `commit-lint.yml`) so a contributor who skipped local hooks still gets caught at PR time. The CI check should scan the PR's commit range only, not the whole branch history. ## Acceptance criteria - [ ] `git commit -m "did a thing"` is rejected locally with a message pointing at the convention doc. - [ ] `git commit -m "feat(release): wire conventional-commit gate"` succeeds. - [ ] A PR with a non-conventional commit fails the new CI check; one with conventional messages passes. - [ ] Documentation in CLAUDE.md / `docs/ci.md` updated to point at the enforcement, not just the convention. - [ ] An entry in `lavamoat.allowScripts` for whatever new dev dep gets pulled in, if any (commitlint pulls a handful). ## Out of scope - Rewriting old non-conventional commits. The first few releases will keep their "Other" bucket; that's acceptable. - Auto-suggesting commit messages, AI commit-message tools, or anything that fixes the message for the user. Part of epic #2.
james closed this issue 2026-06-18 02:11:56 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#70
No description provided.