testsAndMisc/meta/scripts/install_hooks.sh
Krzysztof Rudnicki 2b3c1a69f7 fix(hooks): make git hooks and their toolchain mandatory and fail-closed
A completely absent `prettier` binary went unnoticed long enough for
unformatted markdown to reach main. Two independent causes, both fixed here.

No pre-push hook existed. The three hooks declaring `stages: [pre-push]` in
.pre-commit-config.yaml — prettier, ci-mirror, pytest-coverage — therefore
never ran locally at all. Adds one.

The pre-commit hook failed open. shfmt and shellcheck were wrapped in
`if command -v`, and the entire pre-commit framework was skipped with a
warning when its binary was missing. A silently skipped gate is
indistinguishable from a passing one, so a missing tool now aborts.

- lib/common.sh: require_tool tries `sudo -n pacman -S` and aborts with the
  exact command when a tool cannot be installed non-interactively;
  verify_toolchain applies it to all eight binaries the hooks invoke.
- The pre-push hook resolves git's stdin refs into --from-ref/--to-ref.
  Without a range, `pre-commit run --hook-stage pre-push` checks *staged*
  files, and nothing is staged at push time, so prettier would have received
  zero files and passed vacuously.
- ensure_hooks_path repairs core.hooksPath drift on every hook run.
- meta/scripts/install_hooks.sh covers the one case the hooks cannot
  self-heal — a fresh clone where the config was never set, so nothing runs.
  `--check` reports state and exits non-zero when incomplete.

Note `pre-commit install` must never be used here: it writes into
.git/hooks/, which git ignores entirely while core.hooksPath is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 11:09:11 +02:00

146 lines
3.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# install_hooks.sh — make this clone's git hooks active and its toolchain complete.
#
# Run once per machine / per fresh clone:
# ./meta/scripts/install_hooks.sh
#
# This is the bootstrap that the hooks themselves cannot perform: if
# core.hooksPath is unset and .git/hooks is empty, git runs nothing at all, so
# no hook is around to repair the wiring. Everything after that first run is
# self-healing — each hook re-asserts core.hooksPath and verifies the toolchain.
#
# Note: `pre-commit install` is deliberately NOT used. It writes into
# .git/hooks/, which git ignores entirely while core.hooksPath is set, and
# newer pre-commit refuses outright. The hooks in linux_configuration/.githooks/
# invoke `pre-commit run` directly instead, and being tracked they cannot go
# missing on a clone.
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
usage() {
echo "Usage: $SCRIPT_NAME [--check]"
echo "Options:"
echo " --check Verify wiring and toolchain, change nothing, exit 1 if incomplete"
echo " -h,--help Show this help"
exit 0
}
CHECK_ONLY=false
while [[ $# -gt 0 ]]; do
case $1 in
--check)
CHECK_ONLY=true
shift
;;
-h | --help)
usage
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
# shellcheck source=linux_configuration/.githooks/lib/common.sh
source "${repo_root}/linux_configuration/.githooks/lib/common.sh"
# Every hook that must exist and be executable for the gate to be complete.
readonly MANAGED_HOOKS=(pre-commit pre-push)
# Report on wiring/toolchain without changing anything; non-zero when incomplete.
run_check() {
local failures=0 current hook path
current="$(git config --get core.hooksPath || true)"
if [[ $current == "$HOOKS_PATH_REL" ]]; then
echo "✓ core.hooksPath = ${HOOKS_PATH_REL}"
else
echo "✗ core.hooksPath is '${current:-<unset>}', expected '${HOOKS_PATH_REL}'" >&2
failures=$((failures + 1))
fi
for hook in "${MANAGED_HOOKS[@]}"; do
path="${HOOKS_PATH_REL}/${hook}"
if [[ -x $path ]]; then
echo "✓ ${hook} hook present and executable"
else
echo "✗ ${hook} hook missing or not executable: ${path}" >&2
failures=$((failures + 1))
fi
done
local entry cmd
for entry in "${REQUIRED_TOOLS[@]}"; do
cmd="${entry%%:*}"
if command -v "$cmd" >/dev/null 2>&1; then
echo "✓ ${cmd}"
else
echo "✗ ${cmd} not installed" >&2
failures=$((failures + 1))
fi
done
if ((failures > 0)); then
echo "" >&2
echo "${failures} problem(s). Run ./meta/scripts/install_hooks.sh to fix." >&2
return 1
fi
echo ""
echo "Hooks are wired and the toolchain is complete."
}
# Point git at the tracked hooks directory and make the hooks runnable.
install_wiring() {
local hook path
ensure_hooks_path
echo "✓ core.hooksPath = ${HOOKS_PATH_REL}"
for hook in "${MANAGED_HOOKS[@]}"; do
path="${HOOKS_PATH_REL}/${hook}"
if [[ ! -f $path ]]; then
hook_abort "Aborted: ${path} is missing from the working tree. The hooks are tracked in git — restore them with 'git checkout -- ${path}'."
fi
chmod +x "$path"
echo "✓ ${hook} hook executable"
done
}
main() {
if [[ $CHECK_ONLY == true ]]; then
run_check
return
fi
echo "Wiring git hooks..."
install_wiring
echo ""
echo "Verifying toolchain..."
verify_toolchain
echo "✓ all ${#REQUIRED_TOOLS[@]} required tools present"
echo ""
echo "Pre-building pre-commit hook environments..."
pre-commit install-hooks
echo ""
echo "Done. Commits run the pre-commit stage; pushes run prettier, ci-mirror,"
echo "and pytest-coverage. Verify any time with:"
echo " ./meta/scripts/install_hooks.sh --check"
}
main "$@"