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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TP4EGbBg3XMcyF28Lx78XJ
206 lines
6.1 KiB
Bash
206 lines
6.1 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
SCRIPT="$REPO_ROOT/scripts/setup-hooks.sh"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
|
|
# Helper: make a bare git repo with no hooks yet
|
|
make_repo() {
|
|
local dir
|
|
dir="$(mktemp -d)"
|
|
git -C "$dir" init -q
|
|
echo "$dir"
|
|
}
|
|
|
|
# Fake binaries for tools we don't want to install-check during tests
|
|
FAKE_BIN="$(mktemp -d)"
|
|
trap 'rm -rf "$FAKE_BIN"' EXIT
|
|
for tool in shellcheck jq yq; do
|
|
printf '#!/bin/sh\necho "fake %s"\n' "$tool" > "$FAKE_BIN/$tool"
|
|
chmod +x "$FAKE_BIN/$tool"
|
|
done
|
|
export PATH="$FAKE_BIN:$PATH"
|
|
|
|
# --- 1. Rejects non-git directory ---
|
|
echo ""
|
|
echo "--- rejects non-git directory ---"
|
|
NON_GIT="$(mktemp -d)"
|
|
trap 'rm -rf "$NON_GIT"' EXIT
|
|
if bash "$SCRIPT" "$NON_GIT" > /dev/null 2>&1; then
|
|
fail "exited 0 for non-git directory — expected exit 1"
|
|
else
|
|
pass "exits non-zero for non-git directory"
|
|
fi
|
|
|
|
# --- 2. Creates commit-msg hook ---
|
|
echo ""
|
|
echo "--- creates commit-msg hook ---"
|
|
REPO="$(make_repo)"
|
|
trap 'rm -rf "$REPO"' EXIT
|
|
bash "$SCRIPT" "$REPO" > /dev/null 2>&1
|
|
HOOK="$REPO/.git/hooks/commit-msg"
|
|
if [[ -f "$HOOK" ]]; then
|
|
pass "commit-msg hook file created"
|
|
else
|
|
fail "commit-msg hook not created"
|
|
fi
|
|
if [[ -x "$HOOK" ]]; then
|
|
pass "commit-msg hook is executable"
|
|
else
|
|
fail "commit-msg hook is not executable"
|
|
fi
|
|
if grep -q "# managed by setup-hooks.sh" "$HOOK"; then
|
|
pass "commit-msg hook contains idempotency marker"
|
|
else
|
|
fail "commit-msg hook missing idempotency marker"
|
|
fi
|
|
|
|
# --- 3. commit-msg hook validates conventional commits ---
|
|
echo ""
|
|
echo "--- commit-msg hook: valid message passes ---"
|
|
REPO2="$(make_repo)"
|
|
trap 'rm -rf "$REPO2"' EXIT
|
|
bash "$SCRIPT" "$REPO2" > /dev/null 2>&1
|
|
HOOK2="$REPO2/.git/hooks/commit-msg"
|
|
TMPFILE="$(mktemp)"
|
|
trap 'rm -f "$TMPFILE"' EXIT
|
|
|
|
for valid_msg in "feat: add validation" "fix(core): correct path resolution" "chore!: drop python dep" "docs: update readme" "refactor(hooks): extract marker logic"; do
|
|
echo "$valid_msg" > "$TMPFILE"
|
|
if bash "$HOOK2" "$TMPFILE" > /dev/null 2>&1; then
|
|
pass "commit-msg hook accepts: $valid_msg"
|
|
else
|
|
fail "commit-msg hook wrongly rejected: $valid_msg"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "--- commit-msg hook: invalid message is rejected ---"
|
|
for invalid_msg in "added some stuff" "WIP" "Fix the thing" "FEAT: bad case" "feat bad colon"; do
|
|
echo "$invalid_msg" > "$TMPFILE"
|
|
if bash "$HOOK2" "$TMPFILE" > /dev/null 2>&1; then
|
|
fail "commit-msg hook wrongly accepted: $invalid_msg"
|
|
else
|
|
pass "commit-msg hook rejects: $invalid_msg"
|
|
fi
|
|
done
|
|
|
|
# --- 4. Appends pre-commit validation block ---
|
|
echo ""
|
|
echo "--- appends validation block to pre-commit hook ---"
|
|
REPO3="$(make_repo)"
|
|
trap 'rm -rf "$REPO3"' EXIT
|
|
bash "$SCRIPT" "$REPO3" > /dev/null 2>&1
|
|
PRE_COMMIT="$REPO3/.git/hooks/pre-commit"
|
|
if [[ -f "$PRE_COMMIT" ]]; then
|
|
pass "pre-commit hook created"
|
|
else
|
|
fail "pre-commit hook not created"
|
|
fi
|
|
if grep -q "shellcheck" "$PRE_COMMIT"; then
|
|
pass "pre-commit hook contains shellcheck"
|
|
else
|
|
fail "pre-commit hook missing shellcheck"
|
|
fi
|
|
if grep -q "jq" "$PRE_COMMIT"; then
|
|
pass "pre-commit hook contains jq"
|
|
else
|
|
fail "pre-commit hook missing jq"
|
|
fi
|
|
if grep -q "SKILL.md" "$PRE_COMMIT"; then
|
|
pass "pre-commit hook contains SKILL.md frontmatter check"
|
|
else
|
|
fail "pre-commit hook missing SKILL.md frontmatter check"
|
|
fi
|
|
|
|
# --- 5. Creates pre-push hook ---
|
|
echo ""
|
|
echo "--- creates pre-push hook ---"
|
|
REPO4="$(make_repo)"
|
|
trap 'rm -rf "$REPO4"' EXIT
|
|
bash "$SCRIPT" "$REPO4" > /dev/null 2>&1
|
|
PUSH_HOOK="$REPO4/.git/hooks/pre-push"
|
|
if [[ -f "$PUSH_HOOK" ]]; then
|
|
pass "pre-push hook created"
|
|
else
|
|
fail "pre-push hook not created"
|
|
fi
|
|
if [[ -x "$PUSH_HOOK" ]]; then
|
|
pass "pre-push hook is executable"
|
|
else
|
|
fail "pre-push hook is not executable"
|
|
fi
|
|
if grep -q "check-manifests" "$PUSH_HOOK"; then
|
|
pass "pre-push hook calls check-manifests.sh"
|
|
else
|
|
fail "pre-push hook missing check-manifests.sh call"
|
|
fi
|
|
|
|
# --- 6. Idempotent: second run replaces each block exactly once ---
|
|
echo ""
|
|
echo "--- idempotent: second run does not duplicate blocks ---"
|
|
REPO5="$(make_repo)"
|
|
trap 'rm -rf "$REPO5"' EXIT
|
|
bash "$SCRIPT" "$REPO5" > /dev/null 2>&1
|
|
bash "$SCRIPT" "$REPO5" > /dev/null 2>&1
|
|
bash "$SCRIPT" "$REPO5" > /dev/null 2>&1
|
|
|
|
for hook_file in "$REPO5/.git/hooks/commit-msg" "$REPO5/.git/hooks/pre-commit" "$REPO5/.git/hooks/pre-push"; do
|
|
count=$(grep -c "# managed by setup-hooks.sh" "$hook_file" || true)
|
|
hook_name="$(basename "$hook_file")"
|
|
if [[ "$count" -eq 1 ]]; then
|
|
pass "idempotent: $hook_name marker appears exactly once after 3 runs"
|
|
else
|
|
fail "idempotent: $hook_name marker appears $count times — block duplicated"
|
|
fi
|
|
if [[ -x "$hook_file" ]]; then
|
|
pass "idempotent: $hook_name remains executable after 3 runs"
|
|
else
|
|
fail "idempotent: $hook_name lost executable bit after repeated runs"
|
|
fi
|
|
done
|
|
|
|
# --- 7. Setup installs tools; no "not installed" warnings when tools present ---
|
|
echo ""
|
|
echo "--- setup does not warn when tools are available ---"
|
|
REPO6="$(make_repo)"
|
|
trap 'rm -rf "$REPO6"' EXIT
|
|
output6="$(bash "$SCRIPT" "$REPO6" 2>&1)"
|
|
for tool in shellcheck jq yq; do
|
|
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
|
|
pass "setup emits no 'not installed' warning for: $tool (present or installed)"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
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 tools are present"
|
|
else
|
|
fail "setup exited non-zero unexpectedly"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|