chore(dev): husky pre-commit / pre-push / commit-msg guardrails #273

Merged
timothy merged 3 commits from feat/autolint-precommit into main 2026-07-11 21:07:36 +02:00
Owner

Adds git hooks (husky + lint-staged) so local commits/pushes catch the failures we've been hitting in CI. Prompted by a real miss this session: an SPA lint error reached CI because typecheck/build/vitest were run after an edit but not npm run lint. These hooks close that gap.

Hooks

Hook Runs Catches
pre-commit lint-staged (eslint --fix + typecheck) on staged web/ files; then dotnet format --verify-no-changes on staged .cs (repo root) web lint/type errors + .NET formatting drift, scoped to staged files
pre-push cd web && check:api && lint && typecheck && build generated v1.json/v1.d.ts drift + a staged change that breaks an unstaged file (lint-staged can't see it) — the "green locally, red in CI" class
commit-msg requires a Co-Authored-By: trailer (merge commits exempt) CLAUDE.md commit protocol

Monorepo / worktree handling (the tricky bits)

  • No root package.json; husky lives in web/. prepare does cd .. && husky so it installs .husky at the repo root and sets core.hooksPath correctly (husky v9 rejects a .. target dir, and npm keeps web/node_modules/.bin on PATH across the cd).
  • pre-push unsets GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE — git exports these while running hooks, and inside a worktree they make nested git (e.g. check:api's git diff) mislocate the tree and silently pass. Without this, drift slips through in worktrees.
  • pre-commit skips the dotnet format step entirely when no .cs is staged (avoids the ~20-40s sln load for web-only commits). Note: commits that touch C# take ~20-40s at pre-commit for the scoped format verify — the accepted cost.

Verification (evidence)

  • Wiring: fresh web/ npm install → git config core.hooksPath resolves into the committed .husky.
  • pre-commit eslint: staged web file with an unused var → commit blocked; clean → passes; a non-web/ commit isn't slowed (~0.7s, "no staged files matching").
  • pre-commit dotnet-format: staged badly-formatted .cs → --verify-no-changes exit 2 (block).
  • commit-msg: message without the trailer → exit 1 (block); with it → exit 0.
  • pre-push: an unstaged lint error → exit 1 (block, the exact class that bit us); clean tree check:api+lint → pass (no false positive).

Docs: docs/ci-cd.md gains a "Pre-commit hooks" section.

Enabling for others: hooks activate after a web/ npm install (the prepare script). Anyone who prefers to skip a hook once can git commit/push --no-verify.

🤖 Generated with Claude Code

Adds git hooks (husky + lint-staged) so local commits/pushes catch the failures we've been hitting in CI. Prompted by a real miss this session: an SPA lint error reached CI because `typecheck`/`build`/`vitest` were run after an edit but not `npm run lint`. These hooks close that gap. ## Hooks | Hook | Runs | Catches | |---|---|---| | **pre-commit** | `lint-staged` (eslint --fix + typecheck) on staged `web/` files; then `dotnet format --verify-no-changes` on staged `.cs` (repo root) | web lint/type errors + .NET formatting drift, scoped to staged files | | **pre-push** | `cd web && check:api && lint && typecheck && build` | generated `v1.json`/`v1.d.ts` drift + a staged change that breaks an **unstaged** file (lint-staged can't see it) — the "green locally, red in CI" class | | **commit-msg** | requires a `Co-Authored-By:` trailer (merge commits exempt) | CLAUDE.md commit protocol | ## Monorepo / worktree handling (the tricky bits) - No root `package.json`; husky lives in `web/`. `prepare` does `cd .. && husky` so it installs `.husky` at the **repo root** and sets `core.hooksPath` correctly (husky v9 rejects a `..` target dir, and `npm` keeps `web/node_modules/.bin` on PATH across the `cd`). - pre-push **unsets `GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE`** — git exports these while running hooks, and inside a worktree they make nested `git` (e.g. `check:api`'s `git diff`) mislocate the tree and silently pass. Without this, drift slips through in worktrees. - pre-commit skips the `dotnet format` step entirely when no `.cs` is staged (avoids the ~20-40s sln load for web-only commits). **Note:** commits that touch C# take ~20-40s at pre-commit for the scoped format verify — the accepted cost. ## Verification (evidence) - **Wiring**: fresh `web/` `npm install` → `git config core.hooksPath` resolves into the committed `.husky`. - **pre-commit eslint**: staged web file with an unused var → commit **blocked**; clean → passes; a non-`web/` commit isn't slowed (~0.7s, "no staged files matching"). - **pre-commit dotnet-format**: staged badly-formatted `.cs` → `--verify-no-changes` exit 2 (**block**). - **commit-msg**: message without the trailer → exit 1 (**block**); with it → exit 0. - **pre-push**: an **unstaged** lint error → exit 1 (**block**, the exact class that bit us); clean tree `check:api`+`lint` → pass (no false positive). Docs: `docs/ci-cd.md` gains a "Pre-commit hooks" section. Enabling for others: hooks activate after a `web/` `npm install` (the `prepare` script). Anyone who prefers to skip a hook once can `git commit/push --no-verify`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added the ci-cd label 2026-07-11 20:49:16 +02:00
timothy added 2 commits 2026-07-11 20:49:16 +02:00
Installs husky git hooks (via web/'s lint-staged + npm, since the JS/TS
project lives in web/ with no root package.json) to catch lint, format,
type, and generated-API-drift errors locally before they reach CI.

Hooks (committed at repo root under .husky/):
- pre-commit: (a) lint-staged runs eslint --fix on staged
  web/src/**/*.{ts,tsx} + a project-wide typecheck; (b) if any *.cs are
  staged, dotnet format --verify-no-changes on just those files (skipped
  when no .cs staged, so web-only commits skip the sln load).
- pre-push: CI-parity gate — cd web && check:api && lint && typecheck &&
  build. Blocks pushing drift or a change that breaks an unstaged file.
- commit-msg: requires a Co-Authored-By trailer (merge commits exempt).

Wiring: web/package.json gains husky + lint-staged devDeps, a lint-staged
config, and a `prepare` script (cd .. && husky) that points git's
core.hooksPath at the repo-root .husky dir on npm install. A fresh
`web/` npm install installs all four hooks automatically.

Monorepo/worktree gotchas handled:
- husky init hard-checks for .git in cwd, so `prepare` cd's to the repo
  root before invoking husky (npm keeps web/node_modules/.bin on PATH).
- git exports GIT_DIR while running hooks; in a worktree/subdir that made
  pre-push's `git diff` (check:api) mislocate the working tree and pass
  silently on drift — pre-push now unsets GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE.

docs/ci-cd.md: new "Pre-commit hooks (web/)" section covering all four.

Verified: eslint error blocks commit; clean commit passes; bad-format .cs
blocks (dotnet format ~6-7s scoped), good .cs passes; check:api drift and
a lint error each block `git push --dry-run`, clean state passes; missing
Co-Authored-By blocks commit-msg, present passes; non-web/.cs commits skip
lint/format. npm run lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into feat/autolint-precommit
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m13s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 6m26s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
6c9b998d7d
timothy added 1 commit 2026-07-11 20:57:21 +02:00
Merge remote-tracking branch 'origin/main' into feat/autolint-precommit
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m53s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
2d0935b651
timothy merged commit bb310f6bc7 into main 2026-07-11 21:07:36 +02:00
timothy deleted branch feat/autolint-precommit 2026-07-11 21:07:36 +02:00
Sign in to join this conversation.