#!/usr/bin/env bash # Pre-push gate for pi-devbox: shellcheck every shell script before it leaves # this clone. Thin wrapper — all logic lives in scripts/lint-shell.sh, which is # the SAME script the CI release gate runs. One copy, not two: a duplicated # check that drifts is the failure this repo keeps paying for. # # Install per clone: git config core.hooksPath hooks # Bypass this gate: git push --no-verify (a guard, not a wall) # # WHY THIS HOOK EXISTS # v1.8.14's first release attempt died at scripts/smoke-test.sh:770 after # build-base had already spent ~46 minutes. shellcheck had ALREADY caught the # defect — SC2289 at severity error, on the very push that introduced it — and # the lint job stayed red for 24 hours, unread, across three runs. The fix at # the time was to gate the release on the same script (the `lint-gate` job). # This hook is the cheaper end of that: the same finding, before the push, # in seconds rather than after a 40 s CI gate or a 46 min build. # # WHY IT COULD NOT EXIST UNTIL NOW # Measured on v1.8.14 (2026-09-09): shellcheck was absent from the devbox # image by all three routes — PATH, dpkg and a filesystem search. So # lint-shell.sh exited 2 in every container, and a hook calling it would have # refused EVERY push rather than gating anything. `shellcheck` was added to # Dockerfile.base in the same change that added this file; on an image built # before that, enable this hook and you will simply be told the gate cannot # run. That is the correct behaviour, but it is not a working hook — so do not # set core.hooksPath on a container older than the release that bakes it. # # NOTE ON SCOPE: this lints the WORKING TREE, not the exact commit range being # pushed. That is deliberate and matches what the CI gate does to the tagged # tree. It means a defect you have staged-but-not-committed is also reported, # which is noisy in the safe direction. set -euo pipefail HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)" LINTER="$REPO_ROOT/scripts/lint-shell.sh" tag="[lint-shell]" # Same rule the gate itself applies, applied one level up: a missing check is # not a pass. If the script is gone, the push is refused rather than waved # through on the assumption that CI will catch it. if [ ! -r "$LINTER" ]; then echo "$tag refusing the push: $LINTER is missing, so the gate cannot" >&2 echo "$tag run. A gate that cannot run must not pass." >&2 exit 2 fi # Point the message at the actual remedy when the binary is absent, because the # linter's own message ("install it or run this in CI") is written for a CI # runner and is misleading inside a container the developer cannot apt-install # into persistently. if ! command -v shellcheck >/dev/null 2>&1; then echo "$tag refusing the push: shellcheck is not installed, so the gate" >&2 echo "$tag cannot run. A gate that cannot run must not pass." >&2 echo "$tag" >&2 echo "$tag This container predates the image that bakes shellcheck." >&2 echo "$tag Either recreate onto an image that has it, or unset the hook:" >&2 echo "$tag git config --unset core.hooksPath" >&2 echo "$tag To push this once without the gate: git push --no-verify" >&2 exit 2 fi exec bash "$LINTER" "$REPO_ROOT"