Skip to content

commit-lint: require a scope on feat: and fix: to close the unscoped-commit release gap #293

Description

@Reefact

Problem

The release tooling partitions commits into release trains by Conventional Commit scope: tools/trains.sh maps each scope to a train (lib, cli, dum), and tools/packaging/release-notes.sh + tools/changelog/collect-prs.sh build each train's GitHub Release notes and changelog from the commits whose scope belongs to it. A commit with no scope matches no train and is dropped ([ -z "$scope_group" ] && continue). That drop is deliberate for infrastructure (ci:, chore:, build: …).

But commit-lint currently makes the scope optional for every type (the (scope) group is ? in tools/commit-lint/lint-commit-message.sh). So a contributor can write a genuinely user-facing feat: or fix: without a scope — and it is then silently dropped from both the release notes and the changelog. The change ships in the binaries but never appears in the release record.

#231 fixed the documentation of this (CONTRIBUTING now states the rule). This issue is about enforcing it mechanically, so it can't be forgotten.

Impact

A user-facing feature or bug fix can silently disappear from the public release record on a real release. It is invisible — no error, no warning — and only surfaces later as a gap a user notices. Documentation mitigates but relies on the author remembering; the linter can make the mistake impossible.

Direction

Make commit-lint (the commit-msg hook + the CI check, single source lint-commit-message.sh) require the scope group for the two version-driving types, feat and fix, and keep it optional for the non-version-driving types (build, chore, ci, docs, perf, refactor, revert, style, test) — the legitimate unscoped/infrastructure case. Reject an unscoped feat:/fix: at write time and in CI, with a message that names the consequence ("…or it won't appear in any release train").

Decisions for the maintainer (this is a behaviour change to an enforced contract, not a silent tweak):

  • Is there any legitimate unscoped feat:/fix:? (A user-facing change to the FirstClassErrors.Usage samples, or a genuinely cross-cutting change?) If yes, decide how it is expressed — a dedicated scope, or an explicit escape hatch.
  • Applies from adoption on; prior history is not rewritten (consistent with CONTRIBUTING → "Adoption").

Proposed ADR

This changes an enforced, cross-cutting contract, so per CLAUDE.md / AGENTS.md it warrants an ADR — drafted Proposed, and decided by the maintainer (I propose, I never accept). Sketch of the decision it would record:

The commit scope is a load-bearing part of the release contract for version-driving types. feat:/fix: MUST carry a scope, because the release tooling partitions by scope and drops the unscoped; the scope is therefore not "readability only" (a notion already corrected in the docs by #231) but a required routing key.

The ADR file itself belongs in the implementing PR (drafted Proposed, indexed, linked), not here.

Acceptance criteria

  • commit-lint rejects a feat:/fix: with no scope, identically at the commit-msg hook and in CI, with a clear message naming the release-record consequence.
  • The non-version-driving types remain scope-optional.
  • CONTRIBUTING (EN + FR, in lockstep) states the enforced rule (the wording added by Docs: fix the four confirmed documentation/contract drifts #231 becomes machine-enforced).
  • The new rejection is covered by a test case if commit-lint has a test harness.
  • An ADR is drafted Proposed and linked from the implementing PR.

Context

Follow-up to #231 (which fixed the documentation drift; docs PR #292). The underlying footgun was surfaced by the 2026-07-20 architecture, design & ecosystem audit. Relevant files: tools/trains.sh, tools/packaging/release-notes.sh, tools/changelog/collect-prs.sh (routing + the unscoped drop), and tools/commit-lint/lint-commit-message.sh (the optional-scope regex).


Filed as a follow-up to #231 by Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions