mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 19:14:02 +01:00
23fdfcfpinned shellcheck because a version split between CI and developer machines kept the workflow red for a day. The same trap was still open for every other `language: system` tool, so close it for all of them. The risk is not identical across the four, and the code says which is which: shellcheck, zsh run in CI. Drift shows up as a CI failure the developer cannot reproduce -- theb83ce5dcase exactly. prettier, shfmt never run in CI (pre-push and local git hook). They REWRITE files, so two developers on different versions reformat the repo back and forth. check_shellcheck_version becomes a table-driven check_pinned_tool_versions over PINNED_TOOL_VERSIONS, and _tool_version normalises the four different formats these tools report (shellcheck's `version:` line, shfmt's leading v, zsh's second field). zsh is compared on major.minor only: apt cannot select a patch release on the runner image, so 5.9.2 vs 5.9 is not a real divergence and must not become a permanent warning. CI gains a step installing prettier at an exact npm version and shfmt from a checksum-verified release binary, each asserting what PATH actually resolves to afterwards. Those two are unexercised by the current job by design -- the pin records the expected version in one place and protects any future workflow that does run them. Verified: YAML parses with all pins ahead of the hook step; the shfmt install ran verbatim with `sha256sum --check --strict` reporting OK and the version gate passing; prettier@3.9.5 resolves from the registry; the drift warning fires per-tool against stubbed prettier 3.0.0 and shfmt v3.0.0 (confirming v normalisation and that it reports every drifted tool, not just the first) and stays silent against the real toolchain; common.sh is shellcheck clean after rewording a comment that began "# shellcheck, zsh --" and was being parsed as a malformed directive; 1193 Python tests at 100% coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ReVsw7qovYkkGbqcU9tn7
181 lines
6.4 KiB
Bash
181 lines
6.4 KiB
Bash
# shellcheck shell=bash
|
|
|
|
# ============================================================================
|
|
# Shared helpers for the tracked git hooks in linux_configuration/.githooks/.
|
|
#
|
|
# Sourced by the pre-commit and pre-push hooks and by
|
|
# meta/scripts/install_hooks.sh. Not executable on its own.
|
|
#
|
|
# Design note: these hooks FAIL CLOSED. A missing tool means "stop", never
|
|
# "skip the check" — a silently skipped gate is indistinguishable from a
|
|
# passing one, which is how a missing prettier went unnoticed long enough to
|
|
# let unformatted markdown reach main.
|
|
# ============================================================================
|
|
|
|
# Where git must look for hooks. Relative to the repo root so it survives the
|
|
# repo being cloned to a different path.
|
|
readonly HOOKS_PATH_REL="linux_configuration/.githooks"
|
|
|
|
# Every external binary the hooks shell out to, as "command:pacman-package".
|
|
# Derived from the `language: system` hooks in .pre-commit-config.yaml plus
|
|
# the tools this hook directory invokes directly (shfmt, jscpd via npm).
|
|
# Keep in sync when a new system-language hook is added.
|
|
readonly REQUIRED_TOOLS=(
|
|
"pre-commit:python-pre-commit"
|
|
"prettier:prettier"
|
|
"shellcheck:shellcheck"
|
|
"shfmt:shfmt"
|
|
"zsh:zsh"
|
|
"node:nodejs"
|
|
"npm:npm"
|
|
"python3:python"
|
|
)
|
|
|
|
# Emit a hook progress line.
|
|
hook_log() {
|
|
printf ' %s\n' "$1"
|
|
}
|
|
|
|
# Abort the current hook with a message on stderr.
|
|
hook_abort() {
|
|
printf '\n%s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# Ensure git actually routes hooks through this tracked directory.
|
|
#
|
|
# Self-heal: if core.hooksPath drifted (unset by a tool, pointed elsewhere, or
|
|
# never set on a fresh clone that then ran a hook some other way), put it back.
|
|
# Note the inherent limit — if hooksPath is unset AND .git/hooks is empty, no
|
|
# hook runs at all and nothing here executes. That gap is what
|
|
# meta/scripts/install_hooks.sh exists to close on a new machine.
|
|
ensure_hooks_path() {
|
|
local current
|
|
current="$(git config --get core.hooksPath || true)"
|
|
|
|
if [[ $current == "$HOOKS_PATH_REL" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
git config core.hooksPath "$HOOKS_PATH_REL"
|
|
hook_log "↻ repaired core.hooksPath: '${current:-<unset>}' → ${HOOKS_PATH_REL}"
|
|
}
|
|
|
|
# Ensure a single command is available, installing it via pacman when possible.
|
|
#
|
|
# $1 — command name to look for on PATH
|
|
# $2 — pacman package that provides it
|
|
#
|
|
# Tries a non-interactive install first; a package needing a TTY (password
|
|
# prompt, AUR) aborts with the exact command to run by hand rather than
|
|
# hanging on a prompt inside a git hook.
|
|
require_tool() {
|
|
local cmd="$1" pkg="$2"
|
|
|
|
if command -v "$cmd" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
hook_log "→ ${cmd} missing; installing ${pkg}..."
|
|
|
|
if sudo -n pacman -S --noconfirm --needed "$pkg" >/dev/null 2>&1 &&
|
|
command -v "$cmd" >/dev/null 2>&1; then
|
|
hook_log "✓ ${cmd} installed"
|
|
return 0
|
|
fi
|
|
|
|
hook_abort "Aborted: required tool '${cmd}' is missing and could not be installed automatically.
|
|
Run this yourself, then retry:
|
|
! sudo pacman -S ${pkg}"
|
|
}
|
|
|
|
# Versions of the `language: system` tools that the gates depend on. Every
|
|
# entry is `command:expected:where`, where `where` names the file that pins
|
|
# the other side so a mismatch report can point at it.
|
|
#
|
|
# Why this table exists: these hooks run whatever binary happens to be on
|
|
# PATH. When the local copy and the pinned copy disagree about what counts as
|
|
# a finding -- or, for the formatters, about how a file should be formatted --
|
|
# a commit passes every local gate and then fails elsewhere on output the
|
|
# developer cannot reproduce. That is precisely what kept the pre-commit
|
|
# workflow red from b83ce5d onward, and it cost a full session to diagnose
|
|
# because the failure was invisible on the developer's machine.
|
|
#
|
|
# Two distinct risks are covered here. The linters (shellcheck and zsh) run
|
|
# in CI, so drift there shows up as an unreproducible CI failure. The
|
|
# formatters (prettier and shfmt) never run in CI -- they are pre-push and
|
|
# local-git-hook only -- but they REWRITE files, so drift between two
|
|
# developers silently reformats the repo back and forth, producing diff churn
|
|
# nobody intended.
|
|
readonly PINNED_TOOL_VERSIONS=(
|
|
"shellcheck:0.11.0:.github/workflows/pre-commit.yml (SHELLCHECK_VERSION)"
|
|
"prettier:3.9.5:.github/workflows/pre-commit.yml (PRETTIER_VERSION)"
|
|
"shfmt:3.13.1:.github/workflows/pre-commit.yml (SHFMT_VERSION)"
|
|
"zsh:5.9:.github/workflows/pre-commit.yml (zsh comes from apt; major.minor only)"
|
|
)
|
|
|
|
# Print the version of a pinned tool, normalised to bare digits.
|
|
#
|
|
# Each tool reports differently: shellcheck uses a `version:` line, shfmt and
|
|
# node prefix a `v`, zsh puts it in field 2. Normalising here keeps the
|
|
# comparison below a plain string match.
|
|
_tool_version() {
|
|
local cmd="$1" raw=""
|
|
|
|
case "$cmd" in
|
|
shellcheck) raw="$(shellcheck --version 2>/dev/null | awk '/^version:/ { print $2 }')" ;;
|
|
prettier) raw="$(prettier --version 2>/dev/null)" ;;
|
|
shfmt) raw="$(shfmt --version 2>/dev/null)" ;;
|
|
# zsh patch releases are not selectable via apt, so compare major.minor
|
|
# only -- 5.9.2 locally and 5.9 in CI are not a meaningful divergence.
|
|
zsh) raw="$(zsh --version 2>/dev/null | awk '{ print $2 }' | cut -d. -f1,2)" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
|
|
printf '%s' "${raw#v}"
|
|
}
|
|
|
|
# Warn when a local tool disagrees with the version pinned for CI.
|
|
#
|
|
# Deliberately warnings, not aborts: these arrive via pacman on a rolling
|
|
# distro, so bumps are routine and must not block a commit. What must not
|
|
# happen silently is the disagreement itself.
|
|
check_pinned_tool_versions() {
|
|
local entry cmd expected where installed drifted=0
|
|
|
|
for entry in "${PINNED_TOOL_VERSIONS[@]}"; do
|
|
cmd="${entry%%:*}"
|
|
where="${entry##*:}"
|
|
expected="${entry#*:}"
|
|
expected="${expected%%:*}"
|
|
|
|
command -v "$cmd" >/dev/null 2>&1 || continue
|
|
|
|
installed="$(_tool_version "$cmd")" || continue
|
|
[[ -n $installed ]] || continue
|
|
[[ $installed == "$expected" ]] && continue
|
|
|
|
hook_log "⚠ ${cmd} ${installed} locally, pinned ${expected}"
|
|
hook_log " update: ${where}"
|
|
hook_log " update: linux_configuration/.githooks/lib/common.sh (PINNED_TOOL_VERSIONS)"
|
|
drifted=1
|
|
done
|
|
|
|
if [[ $drifted -eq 1 ]]; then
|
|
hook_log " Results may differ between this machine and CI/other developers."
|
|
fi
|
|
}
|
|
|
|
# Ensure every tool in REQUIRED_TOOLS is present. Fails closed on the first
|
|
# one that cannot be provided.
|
|
verify_toolchain() {
|
|
local entry cmd pkg
|
|
for entry in "${REQUIRED_TOOLS[@]}"; do
|
|
cmd="${entry%%:*}"
|
|
pkg="${entry##*:}"
|
|
require_tool "$cmd" "$pkg"
|
|
done
|
|
|
|
check_pinned_tool_versions
|
|
}
|