Files
apf_portal/docs/decisions/0007-pre-commit-hooks-and-conventional-commits.md
Julien Gautier 0e58e32d29 chore: relocate ADRs from decisions/ to docs/decisions/ to consolidate documentation
Move the ADR folder under docs/ alongside the rest of the project
documentation. Convention (flat folder, globally-sequential 4-digit
numbering, tags-based categorization, MADR 4.0.0 format) is unchanged
- only the path moved.

- git mv decisions docs/decisions preserves history for all 18 ADRs +
  README + template (19 files renamed in this commit).
- ADR-0001 amended in-place with a dated note documenting the
  relocation. Status remains 'accepted' - the location detail
  changed, the decision did not.
- All cross-references updated:
  - CLAUDE.md (~17 ADR links + 3 mentions of decisions/ in the Project
    rules section)
  - docs/README.md (now references decisions/ as a sibling under docs/)
  - docs/setup/03-angular-nx-monorepo.md (paths shortened from
    ../../decisions/ to ../decisions/, since setup/ and decisions/ are
    now both inside docs/)
  - docs/decisions/0003 ../CLAUDE.md adjusted to ../../CLAUDE.md
    (one extra level of nesting)
  - docs/decisions/template.md mention of the README path
  - notes/asvs-level-decision-briefing-rssi.md mention of the index

Sanity verified: every ADR link in CLAUDE.md, docs/setup/03, and
docs/decisions/0001 resolves to an existing file. pnpm nx run-many
-t lint passes on 8 projects.
2026-04-30 18:57:59 +02:00

5.6 KiB

status, date, decision-makers, tags
status date decision-makers tags
accepted 2026-04-29 R&D Lead
process

Pre-commit hooks and Conventional Commits

Context and Problem Statement

Without local enforcement, trivial issues — unformatted code, lint errors, ill-formed commit messages — only surface in CI. Each round-trip costs minutes of feedback latency and pollutes the commit history with fixup noise. We need a lightweight, well-known mechanism that catches these issues at commit time and enforces a consistent commit message format, while staying fast enough not to discourage frequent commits.

Which tooling do we adopt for git pre-commit checks and commit message validation?

Decision Drivers

  • Fast feedback: catch trivial issues locally; CI is defense in depth, not the first line.
  • Consistency of code style and commit history across contributors.
  • Conventional commit history that machines can read (later automation: changelog, release notes, semver bumps).
  • Mainstream tooling — no exotic shell hooks, no single-maintainer projects.
  • Easy onboarding: no extra runtime to manage beyond Node/pnpm.

Considered Options

  • Husky + lint-staged + commitlint with Conventional Commits. (Chosen.)
  • No git hooks — rely on CI.
  • pre-commit (Python framework).
  • lefthook (Go single-binary hook manager).
  • Custom shell scripts in .git/hooks/.

Decision Outcome

Chosen option: Husky + lint-staged + commitlint, with Conventional Commits as the commit-message specification.

Hooks installed under .husky/:

Hook Action
pre-commit pnpm exec lint-staged — runs lint and format on staged files only
commit-msg pnpm exec commitlint --edit "$1" — validates the commit message against Conventional Commits

Configuration:

  • package.json: "prepare": "husky" script (Husky 9+ pattern), lint-staged config block.
  • commitlint.config.cjs: module.exports = { extends: ['@commitlint/config-conventional'] }.

Scope of lint-staged: only fast checks on staged files (lint, format). Type-check and tests stay in pnpm nx affected -t lint test build in CI — running the full graph on every commit would slow commits unacceptably and discourage frequent commits.

CI re-runs the same checks (defense in depth — hooks can be bypassed with --no-verify).

Consequences

  • Good, because trivial issues are caught locally and don't pollute PR history.
  • Good, because Conventional Commits gives us a machine-readable history — enables automated changelogs, semver inference, and release tooling without retroactive cleanup.
  • Good, because Husky / lint-staged / commitlint are the de facto standard in JS/TS projects — wide community, abundant documentation, low surprise factor.
  • Good, because lint-staged keeps commit time low (only changed files are checked).
  • Bad, because hooks can be bypassed (git commit --no-verify); CI must remain authoritative.
  • Bad, because Husky 9 changed its installation pattern (no husky install, just husky); contributors with stale instructions can be confused. Mitigated by the setup guide.
  • Bad, because Conventional Commits adds a small learning curve; mitigated by IDE plugins, commitlint error messages, and a one-page contributor cheatsheet (future doc).

Confirmation

  • package.json declares husky, lint-staged, @commitlint/cli, @commitlint/config-conventional as devDependencies.
  • package.json has a "prepare": "husky" script.
  • .husky/pre-commit runs pnpm exec lint-staged and is executable.
  • .husky/commit-msg runs pnpm exec commitlint --edit "$1" and is executable.
  • commitlint.config.cjs exists at the workspace root and extends @commitlint/config-conventional.
  • CI pipeline (future ADR) re-runs lint and format checks on every push, plus a Conventional Commits validation on the PR commit range.

Pros and Cons of the Options

Husky + lint-staged + commitlint (chosen)

  • Good, because the trio is the de facto standard in the JS/TS ecosystem.
  • Good, because each tool is single-purpose and composable.
  • Good, because the configuration lives with the repo and is versioned with the code.
  • Bad, because three packages instead of one — slightly more dependency surface.

No git hooks

  • Good, because zero local setup.
  • Bad, because every trivial issue costs a CI round-trip. Wasteful and noisy.

pre-commit (Python)

  • Good, because language-agnostic, used in mixed-language repos (Python + JS + Go).
  • Bad, because adds a Python runtime dependency to a Node-only project — extra setup for contributors, especially under WSL where Python toolchains are not always uniform.

lefthook

  • Good, because single Go binary, very fast, parallel hook execution, declarative YAML config.
  • Bad, because smaller community than Husky in JS/TS land; less prior art and fewer answers when something breaks.
  • Status: kept on the watch list for re-evaluation if commit performance becomes a pain.

Custom shell scripts in .git/hooks/

  • Good, because zero dependency.
  • Bad, because hooks aren't versioned with the repo by default; a wrapper layer is needed anyway. Bricolage.

More Information