Files
holocron/scripts/setup-hooks.sh
Defame1297 ce673ca5e2 feat(hooks): add deterministic validation layer via git hooks
Adds setup-hooks.sh and check-manifests.sh as the deterministic
enforcement layer described in docs/research/governance_principles/CONTROLS.md.

- commit-msg: conventional commits pattern check (hard block)
- pre-commit: shellcheck on .sh, jq on .json, yq on .yaml/.yml,
  SKILL.md frontmatter validation; optional tools degrade gracefully
- pre-push: full test suite + manifest cross-reference check
- check-manifests.sh: validates marketplace.json plugin sources,
  plugin.json skill/hooks/mcpServers path references
- Marker-based blocks (idempotent, composable with gitleaks)
- 33 integration tests across two test scripts

Run scripts/setup-hooks.sh to install into any repo's .git/hooks/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TP4EGbBg3XMcyF28Lx78XJ
2026-06-20 21:50:08 +00:00

156 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Installs git hook blocks for validation into a target repository.
# Usage: setup-hooks.sh [TARGET_REPO]
# 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.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET="${1:-$(pwd)}"
MARKER="# managed by setup-hooks.sh"
END_MARKER="# end setup-hooks"
if [[ ! -d "$TARGET/.git" ]]; then
echo "Error: $TARGET is not a git repository" >&2
exit 1
fi
# --- Tool checks (optional — warn, don't fail) ---
warn_if_missing() {
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
fi
}
warn_if_missing shellcheck
warn_if_missing jq
warn_if_missing yq
# --- Marker-block helpers ---
write_block() {
local hook_file="$1"
local block_content="$2"
if grep -qF "$MARKER" "$hook_file"; then
awk -v start="$MARKER" -v end="$END_MARKER" '
$0 == start { skip=1; next }
skip && $0 == end { skip=0; next }
!skip { print }
' "$hook_file" > "${hook_file}.tmp" && mv "${hook_file}.tmp" "$hook_file"
fi
printf '\n%s\n%s\n%s\n' "$MARKER" "$block_content" "$END_MARKER" >> "$hook_file"
}
ensure_hook() {
local hook_file="$1"
if [[ ! -f "$hook_file" ]]; then
printf '#!/usr/bin/env bash\nset -euo pipefail\n' > "$hook_file"
chmod +x "$hook_file"
fi
}
# --- commit-msg: conventional commits ---
COMMIT_MSG_HOOK="$TARGET/.git/hooks/commit-msg"
ensure_hook "$COMMIT_MSG_HOOK"
commit_msg_block() {
cat <<'BLOCK'
msg=$(cat "$1")
pattern='^(feat|fix|docs|chore|refactor|test|perf|ci|build|revert)(\(.+\))?!?: .+'
if ! echo "$msg" | grep -qE "$pattern"; then
echo "ERROR: Commit message must follow Conventional Commits format." >&2
echo " Examples: feat: add login, fix(auth): correct token expiry, chore!: drop python dep" >&2
exit 1
fi
BLOCK
}
write_block "$COMMIT_MSG_HOOK" "$(commit_msg_block)"
echo "Updated: $COMMIT_MSG_HOOK (conventional commits check)"
# --- pre-commit: shellcheck + jq/yq + SKILL.md frontmatter ---
PRE_COMMIT_HOOK="$TARGET/.git/hooks/pre-commit"
ensure_hook "$PRE_COMMIT_HOOK"
pre_commit_block() {
cat <<'BLOCK'
staged=$(git diff --cached --name-only --diff-filter=ACM)
# shellcheck on staged .sh files
if command -v shellcheck &>/dev/null; then
echo "$staged" | grep '\.sh$' | while IFS= read -r f; do
[[ -f "$f" ]] && shellcheck "$f"
done
else
echo "Warning: shellcheck not installed — shell script linting skipped" >&2
fi
# jq validation on staged .json files
if command -v jq &>/dev/null; then
echo "$staged" | grep '\.json$' | while IFS= read -r f; do
[[ -f "$f" ]] && jq . "$f" > /dev/null
done
else
echo "Warning: jq not installed — JSON validation skipped" >&2
fi
# yq validation on staged .yaml/.yml files
if command -v yq &>/dev/null; then
echo "$staged" | grep -E '\.(yaml|yml)$' | while IFS= read -r f; do
[[ -f "$f" ]] && yq eval '.' "$f" > /dev/null
done
else
echo "Warning: yq not installed — YAML validation skipped" >&2
fi
# SKILL.md frontmatter: must have name: and description:
echo "$staged" | grep 'SKILL\.md$' | while IFS= read -r f; do
if [[ -f "$f" ]]; then
if ! grep -q '^name:' "$f" || ! grep -q '^description' "$f"; then
echo "ERROR: $f is missing required frontmatter fields (name: and description:)" >&2
exit 1
fi
fi
done
BLOCK
}
write_block "$PRE_COMMIT_HOOK" "$(pre_commit_block)"
echo "Updated: $PRE_COMMIT_HOOK (shellcheck + jq/yq + SKILL.md validation)"
# --- pre-push: test suite + manifest cross-reference ---
PRE_PUSH_HOOK="$TARGET/.git/hooks/pre-push"
ensure_hook "$PRE_PUSH_HOOK"
pre_push_block() {
local script_dir="$SCRIPT_DIR"
cat <<BLOCK
HOOKS_SCRIPT_DIR="$script_dir"
REPO_ROOT="\$(git rev-parse --show-toplevel)"
echo "Running test suite..."
bash "\$REPO_ROOT/tests/test-install.sh"
bash "\$REPO_ROOT/tests/test-governance-layer.sh"
bash "\$REPO_ROOT/tests/test-check-manifests.sh"
bash "\$REPO_ROOT/tests/test-setup-hooks.sh"
echo "Checking manifests..."
bash "\$HOOKS_SCRIPT_DIR/check-manifests.sh" "\$REPO_ROOT"
BLOCK
}
write_block "$PRE_PUSH_HOOK" "$(pre_push_block)"
echo "Updated: $PRE_PUSH_HOOK (test suite + manifest check)"
echo ""
echo "Done. Hooks installed in $TARGET/.git/hooks/"
echo "To skip on a single push: git push --no-verify"