Saltar al contenido principal

Wiedii — Política de Git Hooks con Lefthook

Todo repositorio debe tener lefthook.yaml con hooks activos. Los hooks se instalan automáticamente via [hooks].postinstall en mise.toml al ejecutar mise install --yes, con detección de CI para no ejecutarse en pipelines.

Configuraciones compartidas — wiedii-configs

La mayoría de los hooks están centralizados en wildcat/wiedii-configs y se consumen vía remotes. El lefthook.yaml local solo define los linters y herramientas específicas del proyecto.

Requisito: todas las herramientas que usan los hooks deben estar declaradas en el mise.toml del proyecto consumidor. Los hooks invocan mise exec -- <herramienta> — si la herramienta no está en el entorno mise del proyecto, el hook fallará.

Herramientas mínimas requeridas en mise.toml: bun, lefthook, taplo. Opcionales según contenido del repo: hadolint (Dockerfiles), shfmt + shellcheck (scripts .sh).

Ver wiedii-configs para la tabla completa con versiones.

# lefthook.yaml — todos los repos Wiedii
remotes:
- git_url: git@gitlab.wiedii.co:wildcat/wiedii-configs.git
configs:
- lefthook/base.yaml # suficiente para la mayoría de proyectos
ArchivoHooksStack
lefthook/base.yamlpre-commit: toml, sort-package-json, hadolint, shfmt, shellcheck, lefthook-validate, renovate-validateTodos
lefthook/base.yamlprepare-commit-msg (czg wizard), commit-msg (commitlint)Todos
lefthook/base.yamlpost-commit: graphify incremental update (no-op si no hay grafo)Todos
lefthook/base.yamlpost-merge, post-checkout, post-rewrite (bun install)Todos
lefthook/js.yamlplaceholder — hooks exclusivos JS/TS futuros (ej. tsc)JS / TS
lefthook/python.yaml(próximamente)Python
lefthook/go.yaml(próximamente)Go

No duplicar en el lefthook.yaml local los hooks que ya proveen los remotes. Duplicarlos genera doble ejecución y divergencia.


lefthook.yaml estándar por tipo de proyecto

JS / TS con Bun (consumiendo wiedii-configs)

remotes:
- git_url: git@gitlab.wiedii.co:wildcat/wiedii-configs.git
configs:
- lefthook/base.yaml

pre-commit:
parallel: true
jobs:
- run: mise trust
- run: mise install --yes
commands:
prettier:
priority: 1
glob: '*.{js,cjs,mjs,ts,json,md,yaml}'
run: mise exec -- bun prettier --write --cache {staged_files}
stage_fixed: true
eslint:
priority: 2
glob: '*.{js,cjs,mjs,ts,json}'
run: mise exec -- bun eslint --config eslint.config.mjs --cache --fix {staged_files}
stage_fixed: true

Los hooks commit-msg, prepare-commit-msg, sort-package-json, post-merge/post-checkout/post-rewrite y todos los validadores universales vienen de base.yaml — no se incluyen localmente.

Sin remotes (repo standalone sin wiedii-configs)

Solo usar este patrón si el repo no puede consumir wiedii-configs. Incluir manualmente todos los hooks:

pre-commit:
parallel: true
jobs:
- run: mise trust
- run: mise install --yes
commands:
prettier:
priority: 1
glob: '*.{js,cjs,mjs,ts,json,md,yaml}'
run: mise exec -- bun prettier --write --cache {staged_files}
stage_fixed: true
eslint:
priority: 2
glob: '*.{js,cjs,mjs,ts,json}'
run: mise exec -- bun eslint --config eslint.config.mjs --cache --fix {staged_files}
stage_fixed: true
sort-package-json:
priority: 3
glob: '**/package.json'
run: |
PKG_DIR=$(find . -maxdepth 2 -name 'package.json' ! -path '*/node_modules/*' | sort | head -1 | xargs dirname 2>/dev/null || echo ".")
if [ -f "$PKG_DIR/node_modules/.bin/sort-package-json" ]; then
mise exec -- bun "$PKG_DIR/node_modules/.bin/sort-package-json" {staged_files}
else
echo "sort-package-json: skipping — add sort-package-json to devDependencies"
fi
stage_fixed: true
toml:
priority: 4
glob: '*.toml'
run: mise exec -- taplo fmt {staged_files}
stage_fixed: true
hadolint:
priority: 5
glob: 'Dockerfile'
run: mise exec -- hadolint {staged_files}
shfmt:
priority: 6
glob: '*.sh'
run: mise exec -- shfmt --write {staged_files}
stage_fixed: true
shellcheck:
priority: 7
glob: '*.sh'
run: mise exec -- shellcheck {staged_files}
renovate-validate:
priority: 8
glob: '{renovate.json,.renovaterc,.renovaterc.json}'
run: |
if (git diff --name-only --cached --diff-filter=ACMRD || true) | grep -E "(renovate\.json|\.renovaterc)" >/dev/null \
|| (git ls-files -m || true) | grep -E "(renovate\.json|\.renovaterc)" >/dev/null; then
mise exec -- bunx --yes --package renovate@latest -- renovate-config-validator --strict
else
echo "renovate-validate: no config changes, skipping"
fi

commit-msg:
parallel: false
jobs:
- run: mise trust
- run: mise install --yes
commands:
commitlint:
priority: 1
run: |
PKG_DIR=$(find . -maxdepth 2 -name 'package.json' ! -path '*/node_modules/*' | sort | head -1 | xargs dirname 2>/dev/null || echo ".")
ABS_PKG_DIR=$(cd "$PKG_DIR" && pwd)
GIT_ROOT=$(pwd)
CONFIG=""
for ext in js cjs mjs; do
if [ -f "$GIT_ROOT/commitlint.config.$ext" ]; then
CONFIG="$GIT_ROOT/commitlint.config.$ext"; break
elif [ -f "$ABS_PKG_DIR/commitlint.config.$ext" ]; then
CONFIG="$ABS_PKG_DIR/commitlint.config.$ext"; break
fi
done
(cd "$ABS_PKG_DIR" && mise exec -- bun commitlint ${CONFIG:+--config "$CONFIG"} --edit "$1")

prepare-commit-msg:
commands:
czg:
interactive: true
run: |
[ -n "{2}" ] && exit 0
PKG_DIR=$(find . -maxdepth 2 -name 'package.json' ! -path '*/node_modules/*' | sort | head -1 | xargs dirname 2>/dev/null || echo ".")
exec < /dev/tty 2>/dev/null && (cd "$PKG_DIR" && mise exec -- bun czg --hook {1} {2}) || true

post-merge:
parallel: false
jobs:
- run: mise trust
- run: mise install --yes
commands:
bun-install:
glob: '**/{package.json,bun.lock,bunfig.toml}'
run: |
PKG_DIR=$(find . -maxdepth 2 -name 'package.json' ! -path '*/node_modules/*' | sort | head -1 | xargs dirname 2>/dev/null || echo ".")
if [ ! -d "$PKG_DIR/node_modules" ] || git diff --name-only ORIG_HEAD HEAD | grep -qE "(package\.json|bun\.lock|bunfig\.toml)$"; then
echo "bun-install: running bun install --frozen-lockfile"
mise exec -- bun install --cwd "$PKG_DIR" --frozen-lockfile
else
echo "bun-install: no dependency file changes detected, skipping"
fi

post-checkout:
parallel: false
jobs:
- run: mise trust
- run: mise install --yes
commands:
bun-install:
glob: '**/{package.json,bun.lock,bunfig.toml}'
run: |
PKG_DIR=$(find . -maxdepth 2 -name 'package.json' ! -path '*/node_modules/*' | sort | head -1 | xargs dirname 2>/dev/null || echo ".")
if [ ! -d "$PKG_DIR/node_modules" ] || git diff --name-only ORIG_HEAD HEAD | grep -qE "(package\.json|bun\.lock|bunfig\.toml)$"; then
echo "bun-install: running bun install --frozen-lockfile"
mise exec -- bun install --cwd "$PKG_DIR" --frozen-lockfile
else
echo "bun-install: no dependency file changes detected, skipping"
fi

Adiciones por tipo de proyecto

Astro / frontend — extender el glob de prettier:

prettier:
glob: '*.{js,cjs,mjs,ts,json,md,yaml,astro}'

Python — agregar a los comandos de pre-commit:

lint-py:
priority: 10
glob: "*.py"
run: mise exec -- ruff check --fix {staged_files}
stage_fixed: true
format-py:
priority: 11
glob: "*.py"
run: mise exec -- ruff format {staged_files}
stage_fixed: true
types-py:
priority: 12
glob: "*.py"
run: mise exec -- mypy {staged_files}

Go — agregar a los comandos de pre-commit:

lint-go:
priority: 10
glob: "*.go"
run: mise exec -- golangci-lint run {staged_files}
format-go:
priority: 11
glob: "*.go"
run: mise exec -- gofmt -w {staged_files}
stage_fixed: true

Caveat: proyectos con múltiples package.json

Los hooks de base.yaml (y el ejemplo standalone) detectan package.json con:

find . -maxdepth 2 -name 'package.json' ! -path '*/node_modules/*' | sort | head -1 | xargs dirname

Esto toma el package.json más superficial (raíz antes que src/). Si el proyecto tiene dos package.json —por ejemplo uno en raíz para herramientas de CI y otro en src/ con el código real— los hooks siempre usarán el de raíz.

Consecuencia: devDependencies como czg, commitlint y sort-package-json deben instalarse en el package.json raíz, no en src/package.json.

Si el proyecto tiene package.json únicamente en src/ (sin raíz), la detección lo encontrará correctamente.

czg (prepare-commit-msg): el wizard interactivo solo funciona con una TTY real — al ejecutar git commit (sin -m) desde una terminal. Desde Claude Code, CI o git commit -m, el hook detecta {2} (fuente del commit) y sale inmediatamente sin bloquear. El flujo recomendado para commits interactivos es bun commit.


Verificar que los hooks funcionan

lefthook install # (re)instalar hooks en .git/hooks/
lefthook run pre-commit # probar sin hacer commit

Reglas de escritura de hooks

  • Siempre usar mise exec -- <programa>, nunca eval "$(mise env)" ni llamadas directas al binario.
  • Los hooks deben ser idempotentes: comprobar si hay cambios relevantes antes de ejecutar comandos costosos.
  • Usar glob: para limitar la ejecución a los tipos de archivos afectados.
  • Usar stage_fixed: true en hooks que modifican archivos (formatters) para auto-stagear los cambios.
  • Los hooks de jobs: (mise trust, mise install) corren antes de los commands: — usarlos para preparar el entorno.

Referencias

  • wiedii-configs — documentación del repo de hooks compartidos: estructura, requisitos, cómo contribuir
  • lefthook — guía de instalación, integración con mise y troubleshooting
  • commits — política de mensajes de commit que valida el hook commit-msg