build(commitlint): enforce Conventional Commit messages locally and in CI - #28
Conversation
|
|
| Filename | Overview |
|---|---|
| .githooks/commit-msg | New commit-msg hook that pipes cleaned message to commitlint via docker compose run; includes docker/compose-v2 preflight checks and a lockfile-mtime staleness guard to keep local version in sync with CI. |
| .github/workflows/commitlint.yml | New CI workflow that runs commitlint over the full PR commit range using fetch-depth: 0, npm ci for lockfile-pinned install, and npx --no-install to prevent ad-hoc downloads. |
| docker-compose.yml | Adds a node:24-alpine service behind the commitlint profile so it is never started by bare docker compose up; the hook reaches it via docker compose run which starts profiled services on demand. |
| composer.json | Adds post-install-cmd/post-update-cmd that configure core.hooksPath .githooks with a git-repo guard and ` |
| commitlint.config.mjs | Minimal config extending the stock @commitlint/config-conventional preset; shared by both the local hook and CI. |
| package.json | Adds @commitlint/cli and @commitlint/config-conventional as dev dependencies; npm ci is always used so the ^ range in package.json doesn't affect installed versions. |
| package-lock.json | Generated lockfile pinning both commitlint packages at 19.8.1; committed so npm ci produces identical installs locally and in CI. |
| .gitignore | Adds node_modules/ exclusion to prevent the container-installed packages from being accidentally committed. |
| CONTRIBUTING.md | Documents the Conventional Commits format with real examples from the repo history, and explains the two-entry-point enforcement (local hook + CI). |
Sequence Diagram
sequenceDiagram
participant Dev as Developer
participant Git as Git
participant Hook as .githooks/commit-msg
participant Docker as Docker Compose (node service)
participant CL as commitlint
Dev->>Git: git commit -m "feat: ..."
Git->>Hook: invoke with commit-msg tempfile
Hook->>Hook: check docker binary
Hook->>Hook: check docker compose v2 plugin
Hook->>Hook: git rev-parse --show-toplevel
Hook->>Hook: sed (strip CRLF + comment lines)
Hook->>Docker: docker compose run --rm -T --no-deps node sh -c '...'
Docker->>Docker: check node_modules/.bin/commitlint exists AND lockfile not newer
alt node_modules missing or stale
Docker->>Docker: npm ci (lockfile-pinned install)
end
Docker->>CL: "exec node_modules/.bin/commitlint (stdin = cleaned message)"
CL-->>Git: exit 0 (pass) or exit 1 (fail)
Git-->>Dev: commit accepted or rejected
Note over Dev,CL: CI path (pull_request event)
participant GHA as GitHub Actions
participant NPM as npm ci
participant CL2 as commitlint
GHA->>GHA: checkout (fetch-depth: 0)
GHA->>NPM: npm ci --no-audit --no-fund
NPM-->>GHA: packages installed (lockfile-pinned)
GHA->>CL2: npx --no-install commitlint --from base.sha --to head.sha --verbose
CL2-->>GHA: exit 0 (pass) or exit 1 (fail)
Reviews (3): Last reviewed commit: "fix(commitlint): run the node service be..." | Re-trigger Greptile
|
… tool locally and in CI Commit subjects had been following the Conventional Commits pattern (type(scope): lowercase subject) by convention only; nothing rejected a stray unprefixed message. Adopt commitlint as the single enforcement tool: rules live in commitlint.config.mjs (the stock config-conventional preset, which matches the repo history), versions are pinned by package-lock.json, and a new docker compose `node` service runs it — so contributors need docker but no host Node toolchain. The tracked .githooks/commit-msg hook pipes the message (comment lines stripped, worktree-safe) into commitlint via that service, and composer install/update wires the hook up automatically through core.hooksPath. CONTRIBUTING documents the format with real examples from the history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run the lockfile-pinned commitlint over every commit in the PR range (full-history checkout), with the same commitlint.config.mjs the local commit-msg hook uses — one tool, one version, one rule set in both places. The CI job is the authoritative check backing the local hook, which contributors can bypass with --no-verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… plugin Two robustness gaps in the commit-msg hook found in review: - The install guard only reinstalled when the commitlint binary was absent, so a version bump a collaborator pulls in (new package-lock.json, untouched node_modules) left the hook linting with the old version while CI used the pinned one -- defeating the "same version everywhere" goal. Reinstall when node_modules is missing OR the lockfile is newer than the installed binary; the already-fresh common case still skips npm ci. - The hook checked for `docker` but then runs `docker compose`, so a box with docker-engine but no Compose v2 plugin hit Docker's own cryptic error. Add a `docker compose version` preflight with a useful message.
The node service carried a `tail -f /dev/null` keepalive, so a bare `docker compose up` started and kept it running. That keepalive was also pointless here: the commit-msg hook (and CI) invoke it with `docker compose run`, which supplies its own command and never needs the service pre-started. Put it behind a `commitlint` profile and drop the keepalive -- `docker compose run node` still starts the profiled service on demand, and it no longer shows up in the default stack.
b2140a4 to
06810ec
Compare
Commit subjects have followed the Conventional Commits pattern
(type(scope): lowercase subject) by convention only; nothing rejected a
stray unprefixed message. This adopts commitlint as the single
enforcement tool, with one rule set and one pinned version shared by
both entry points:
Rules:
commitlint.config.mjs, the stock config-conventional preset,which already matches the repo history (types build/chore/ci/docs/
feat/fix/perf/refactor/revert/style/test, optional scope, lowercase
subject, 100-char header, merge/revert subjects ignored).
Versions: pinned by
package.json+package-lock.json(private,dev-only; the PHP package proper stays in
composer.json).Local: composer install wires up the tracked .githooks/commit-msg
hook via core.hooksPath. The hook pipes the message (comment lines
stripped, worktree-safe) into commitlint through a new docker compose
node service (node:24-alpine), so contributors need docker but no
host Node toolchain.
CI: a new Commitlint workflow runs the same lockfile-pinned
commitlint over the full PR commit range. It is the authoritative
check backing the local hook, which can be bypassed with --no-verify.
CONTRIBUTING documents the format with real examples from the history.