chore(tooling): migrate pre-commit framework to husky

Replaces the Python pre-commit framework (.pre-commit-config.yaml) with
husky 9, kept faithful to the existing checks:

- Block direct commits to main (was: no-commit-to-branch).
- git diff --cached --check covers trailing-whitespace and merge-conflict
  marker detection.
- Custom large-file check (>500KB), excluding crawler test fixtures.
- Backend Go: gofmt -l (fail on diff), go vet, golangci-lint — only when
  backend/*.go is staged.
- Backend deps: go mod tidy -diff — only when go.mod/go.sum is staged.
- Web: prettier --check, eslint, svelte-check — only when web/ is
  staged.

lint-staged was intentionally not adopted — the previous config ran
hooks tree-wide (pass_filenames: false), so per-file optimisation would
be a behaviour change.

Install: pnpm install at repo root (the prepare script wires husky into
.git/hooks via core.hooksPath=.husky/_).
This commit is contained in:
2026-04-30 22:15:51 +02:00
parent dee4cee23c
commit bef8657d81
5 changed files with 123 additions and 66 deletions

3
.gitignore vendored
View File

@@ -14,6 +14,9 @@ vendor/
eval-report.json
cat-eval-report.json
# ── Root tooling (husky) ─────────────────────
/node_modules/
# ── Web ──────────────────────────────────────
/web/node_modules/
/web/.svelte-kit/

84
.husky/pre-commit Executable file
View File

@@ -0,0 +1,84 @@
#!/bin/sh
# Pre-commit checks for marktvogt monorepo.
# Replaces .pre-commit-config.yaml (Python pre-commit framework).
# Install via: pnpm install (at repo root). Husky 9 reads this file directly.
set -e
red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
# 1. Block direct commits to main.
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
if [ "$branch" = "main" ]; then
red "ERROR: direct commits to main are blocked. Create a feature branch."
exit 1
fi
# 2. Detect whitespace errors and merge-conflict markers in staged hunks.
# Replaces pre-commit-hooks: trailing-whitespace, check-merge-conflict.
if ! git diff --cached --check; then
red "ERROR: whitespace errors or merge-conflict markers in staged changes."
exit 1
fi
# 3. Reject staged files larger than 500 KB (excluding crawler test fixtures).
# Replaces pre-commit-hooks: check-added-large-files.
big=$(git diff --cached --name-only --diff-filter=ACMR -z |
while IFS= read -r -d '' f; do
case "$f" in
backend/internal/domain/discovery/crawler/testdata/*) continue ;;
esac
[ -f "$f" ] || continue
size=$(wc -c <"$f" 2>/dev/null || echo 0)
if [ "$size" -gt 524288 ]; then
printf '%s (%s bytes)\n' "$f" "$size"
fi
done)
if [ -n "$big" ]; then
red "ERROR: large files staged (>500 KB):"
printf '%s\n' "$big" >&2
exit 1
fi
# Helper: list staged files matching a pattern.
staged_match() {
git diff --cached --name-only --diff-filter=ACMR | grep -E "$1" || true
}
# 4. Backend Go checks — only when backend/*.go is staged.
if [ -n "$(staged_match '^backend/.*\.go$')" ]; then
echo "→ backend: gofmt"
unformatted=$(cd backend && gofmt -l .)
if [ -n "$unformatted" ]; then
red "ERROR: gofmt would change these files:"
printf '%s\n' "$unformatted" >&2
exit 1
fi
echo "→ backend: go vet"
( cd backend && go vet ./... )
echo "→ backend: golangci-lint"
( cd backend && golangci-lint run --config .golangci.yml ./... )
fi
# 5. go.mod / go.sum tidy — only when those files are staged.
if [ -n "$(staged_match '^backend/go\.(mod|sum)$')" ]; then
echo "→ backend: go mod tidy (diff check)"
( cd backend && go mod tidy -diff ) || {
red "ERROR: go mod tidy would change go.mod/go.sum. Run \`cd backend && go mod tidy\` and stage the result."
exit 1
}
fi
# 6. Web checks — only when web/ files are staged.
if [ -n "$(staged_match '^web/')" ]; then
echo "→ web: prettier --check"
( cd web && pnpm run format:check )
echo "→ web: eslint"
( cd web && pnpm run lint )
echo "→ web: svelte-check"
( cd web && pnpm run check -- --threshold error )
fi

View File

@@ -1,66 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
exclude: ^helm/marktvogt/templates/
- id: check-json
exclude: ^web/tsconfig\.json$
- id: check-merge-conflict
- id: check-added-large-files
exclude: ^backend/internal/domain/discovery/crawler/testdata/
- id: no-commit-to-branch
args: ['--branch', 'main']
- repo: local
hooks:
- id: golangci-lint
name: golangci-lint
entry: bash -c 'cd backend && golangci-lint run --config .golangci.yml ./...'
language: system
files: ^backend/.*\.go$
pass_filenames: false
- id: go-fmt
name: go fmt
entry: bash -c 'cd backend && gofmt -w .'
language: system
files: ^backend/.*\.go$
pass_filenames: false
- id: go-vet
name: go vet
entry: bash -c 'cd backend && go vet ./...'
language: system
files: ^backend/.*\.go$
pass_filenames: false
- id: go-mod-tidy
name: go mod tidy
entry: bash -c 'cd backend && go mod tidy'
language: system
files: ^backend/go\.(mod|sum)$
pass_filenames: false
- id: prettier
name: prettier
entry: bash -c 'cd web && pnpm run format:check'
language: system
files: ^web/
pass_filenames: false
- id: eslint
name: eslint
entry: bash -c 'cd web && pnpm run lint'
language: system
files: ^web/
pass_filenames: false
- id: svelte-check
name: svelte-check
entry: bash -c 'cd web && pnpm run check -- --threshold error'
language: system
files: ^web/
pass_filenames: false

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "marktvogt-monorepo",
"private": true,
"description": "Repo-root tooling — husky pre-commit hooks. Application code lives in backend/, web/, app/.",
"packageManager": "pnpm@10.33.0",
"scripts": {
"prepare": "husky"
},
"devDependencies": {
"husky": "^9.1.7"
}
}

24
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,24 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
husky:
specifier: ^9.1.7
version: 9.1.7
packages:
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
snapshots:
husky@9.1.7: {}