From 7323aec740aa5acc878f4447b3fa51d981ed869a Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sat, 20 Jun 2026 22:08:39 +0000 Subject: [PATCH] feat(hooks): install missing tools instead of warning setup-hooks.sh now installs shellcheck, jq, and yq if absent rather than warning and continuing. Follows the same install pattern as setup-gitleaks.sh (curl + install to TOOL_INSTALL_DIR=/usr/local/bin, OS/arch detection, pinned versions). Pinned versions: shellcheck 0.10.0, jq 1.7.1, yq 4.44.3. The deployed pre-commit hook retains its runtime fallbacks as a safety net for environments where tools are removed after setup. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01TP4EGbBg3XMcyF28Lx78XJ --- scripts/setup-hooks.sh | 77 ++++++++++++++++++++++++++++++++++++--- tests/test-setup-hooks.sh | 28 +++++++++----- 2 files changed, 90 insertions(+), 15 deletions(-) diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh index 77695a3..56b33aa 100755 --- a/scripts/setup-hooks.sh +++ b/scripts/setup-hooks.sh @@ -6,6 +6,11 @@ set -euo pipefail # TARGET_REPO — path to the git repo to configure (default: current directory) # Idempotent: safe to re-run; always replaces each managed block with the current version. +SHELLCHECK_VERSION="0.10.0" +JQ_VERSION="1.7.1" +YQ_VERSION="4.44.3" +TOOL_INSTALL_DIR="${TOOL_INSTALL_DIR:-/usr/local/bin}" + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TARGET="${1:-$(pwd)}" MARKER="# managed by setup-hooks.sh" @@ -16,18 +21,78 @@ if [[ ! -d "$TARGET/.git" ]]; then exit 1 fi -# --- Tool checks (optional — warn, don't fail) --- +# --- Tool installation --- -warn_if_missing() { +_os() { + case "$(uname -s)" in + Linux) echo "linux" ;; + Darwin) echo "darwin" ;; + *) echo "Error: unsupported OS '$(uname -s)'" >&2; exit 1 ;; + esac +} + +_arch() { + case "$(uname -m)" in + x86_64) echo "x86_64" ;; + aarch64 | arm64) echo "aarch64" ;; + *) echo "Error: unsupported architecture '$(uname -m)'" >&2; exit 1 ;; + esac +} + +install_shellcheck() { + local os arch tarball url tmp_dir + os="$(_os)" + arch="$(_arch)" + tarball="shellcheck-v${SHELLCHECK_VERSION}.${os}.${arch}.tar.xz" + url="https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/${tarball}" + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' RETURN + echo "Installing shellcheck v${SHELLCHECK_VERSION}..." + curl -fsSL "$url" -o "$tmp_dir/$tarball" + tar -xJf "$tmp_dir/$tarball" -C "$tmp_dir" --strip-components=1 + install -m 755 "$tmp_dir/shellcheck" "$TOOL_INSTALL_DIR/shellcheck" + echo "Installed: $TOOL_INSTALL_DIR/shellcheck" +} + +install_jq() { + local os arch binary url tmp_dir + os="$(_os)" + arch="$(_arch | sed 's/x86_64/amd64/; s/aarch64/arm64/')" + [[ "$(_os)" == "darwin" ]] && os="macos" + binary="jq-${os}-${arch}" + url="https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/${binary}" + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' RETURN + echo "Installing jq v${JQ_VERSION}..." + curl -fsSL "$url" -o "$tmp_dir/jq" + install -m 755 "$tmp_dir/jq" "$TOOL_INSTALL_DIR/jq" + echo "Installed: $TOOL_INSTALL_DIR/jq" +} + +install_yq() { + local os arch binary url tmp_dir + os="$(_os)" + arch="$(_arch | sed 's/x86_64/amd64/; s/aarch64/arm64/')" + binary="yq_${os}_${arch}" + url="https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/${binary}" + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' RETURN + echo "Installing yq v${YQ_VERSION}..." + curl -fsSL "$url" -o "$tmp_dir/yq" + install -m 755 "$tmp_dir/yq" "$TOOL_INSTALL_DIR/yq" + echo "Installed: $TOOL_INSTALL_DIR/yq" +} + +ensure_tool() { local tool="$1" if ! command -v "$tool" &>/dev/null; then - echo "Warning: $tool not installed — ${tool} checks in pre-commit will be skipped (install $tool to enable)" >&2 + "install_${tool}" fi } -warn_if_missing shellcheck -warn_if_missing jq -warn_if_missing yq +ensure_tool shellcheck +ensure_tool jq +ensure_tool yq # --- Marker-block helpers --- diff --git a/tests/test-setup-hooks.sh b/tests/test-setup-hooks.sh index f0c0db3..8adce9c 100644 --- a/tests/test-setup-hooks.sh +++ b/tests/test-setup-hooks.sh @@ -165,27 +165,37 @@ for hook_file in "$REPO5/.git/hooks/commit-msg" "$REPO5/.git/hooks/pre-commit" " fi done -# --- 7. Hooks contain graceful degradation paths for missing tools --- +# --- 7. Setup installs tools; no "not installed" warnings when tools present --- echo "" -echo "--- hooks contain graceful degradation for missing tools ---" +echo "--- setup does not warn when tools are available ---" REPO6="$(make_repo)" trap 'rm -rf "$REPO6"' EXIT -bash "$SCRIPT" "$REPO6" > /dev/null 2>&1 -PRE_COMMIT6="$REPO6/.git/hooks/pre-commit" +output6="$(bash "$SCRIPT" "$REPO6" 2>&1)" for tool in shellcheck jq yq; do - if grep -q "Warning:.*$tool\|$tool.*not installed\|$tool.*skipped" "$PRE_COMMIT6"; then - pass "pre-commit hook has graceful degradation path for missing: $tool" + if echo "$output6" | grep -qi "$tool not installed\|$tool.*not found"; then + fail "setup warned about missing $tool — should install or already be present" else - fail "pre-commit hook missing graceful degradation for: $tool" + pass "setup emits no 'not installed' warning for: $tool (present or installed)" fi done echo "" -echo "--- setup always exits 0 (no hard dependency on optional tools) ---" +echo "--- pre-commit hook retains runtime fallback for missing tools ---" +PRE_COMMIT6="$REPO6/.git/hooks/pre-commit" +for tool in shellcheck jq yq; do + if grep -q "Warning:.*$tool\|$tool.*not installed\|$tool.*skipped" "$PRE_COMMIT6"; then + pass "pre-commit hook has runtime fallback for missing: $tool" + else + fail "pre-commit hook missing runtime fallback for: $tool" + fi +done + +echo "" +echo "--- setup exits 0 when tools are present ---" REPO7="$(make_repo)" trap 'rm -rf "$REPO7"' EXIT if bash "$SCRIPT" "$REPO7" > /dev/null 2>&1; then - pass "setup exits 0 when all optional tools are present" + pass "setup exits 0 when tools are present" else fail "setup exited non-zero unexpectedly" fi