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.tomldel proyecto consumidor. Los hooks invocanmise 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
| Archivo | Hooks | Stack |
|---|---|---|
lefthook/base.yaml | pre-commit: toml, sort-package-json, hadolint, shfmt, shellcheck, lefthook-validate, renovate-validate | Todos |
lefthook/base.yaml | prepare-commit-msg (czg wizard), commit-msg (commitlint) | Todos |
lefthook/base.yaml | post-commit: graphify incremental update (no-op si no hay grafo) | Todos |
lefthook/base.yaml | post-merge, post-checkout, post-rewrite (bun install) | Todos |
lefthook/js.yaml | placeholder — 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.yamllocal 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 ejecutargit commit(sin-m) desde una terminal. Desde Claude Code, CI ogit commit -m, el hook detecta{2}(fuente del commit) y sale inmediatamente sin bloquear. El flujo recomendado para commits interactivos esbun 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>, nuncaeval "$(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: trueen hooks que modifican archivos (formatters) para auto-stagear los cambios. - Los hooks de
jobs:(mise trust,mise install) corren antes de loscommands:— 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