Skip to content

ci: enforce Conventional Commits with a commitlint gate#660

Open
EivMeyer wants to merge 1 commit into
devfrom
eivind/ci-commitlint-gate
Open

ci: enforce Conventional Commits with a commitlint gate#660
EivMeyer wants to merge 1 commit into
devfrom
eivind/ci-commitlint-gate

Conversation

@EivMeyer

Copy link
Copy Markdown
Collaborator

What

Adds a commitlint gate so the Conventional Commit messages that semantic-release relies on are actually enforced.

  • .commitlintrc.json — extends @commitlint/config-conventional; long body/footer lines allowed (squash-merge bodies have Markdown tables and links). Mirrors the config in the-open-engine/agents, per "same approach everywhere."
  • .husky/commit-msg — local validation hook (none existed; only pre-commit/pre-push were present).
  • CI commitlint job — validates every PR commit's message via npx, running before the expensive check job so bad messages fail fast. Dependency-light (no npm ci needed for the gate).

Why

semantic-release derives the version bump from commit-message type (fix: → patch, feat: → minor, etc.). Nothing validated those messages, so a non-conventional subject silently produces no bump or the wrong one — a contributor to the version drift tracked in #640.

Validation

Ran the exact CI config against real messages:

FAIL  | updated stuff              -> subject may not be empty [subject-empty]
FAIL  | foo: bar                   -> type must be one of [...] [type-enum]
FAIL  | Feat: add thing            -> type must be lower-case [type-case]
PASS  | fix: correct version drift
PASS  | feat(release): enforce conventional commits

(Note: a doubled prefix like feat: feat(x): ... passes — commitlint reads feat as the type and the rest as the subject. This gate targets missing/unknown/wrong-case types, which is what breaks version computation.)

Scope boundary

This PR is deliberately just the gate. The trunk cutover items from #640 ride with the dev/main reconciliation, not here:

  • removing the enforce-main-pr-source CI job (which currently mandates the dev → main promotion that causes the divergence)
  • swapping the release job's GITHUB_TOKEN for a repo-scoped release GitHub App
  • the main-didn't-move guard on release.yml

Follow-ups surfaced while implementing (for the reconciliation, not this PR)

  1. Dual semantic-release config. main has .releaserc.json (with @semantic-release/git + changelog, which commit the version back), but dev's package.json has an inline release block without the git plugin — so under dev's config no version-bump commit is written. Likely a direct contributor to branch ≠ npm drift. Consolidate to one config.
  2. Squash-merge nuance. If PRs squash-merge, the message that reaches the trunk is the PR title, not these per-commit messages. If that's the flow, we should also lint the PR title. Left out to keep this focused.

Base is dev per the repo's current enforce-main-pr-source policy.

semantic-release derives the release version from commit-message types,
but nothing validated those messages, so a non-conventional subject
(missing, unknown, or wrong-case type) can silently break version bumps.

- add .commitlintrc.json (extends config-conventional; long body/footer
  lines allowed for squash-merge bodies)
- add a Husky commit-msg hook for local validation
- add a fast, dependency-light commitlint CI job that validates PR commit
  messages via npx, failing before the expensive check job

Part of #640. The trunk cutover items (removing enforce-main-pr-source,
the GitHub App release token) ride with the dev/main reconciliation, not
this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant