Files
state-hub/scripts/install_hooks.sh
tegwick 5e7a72e144 feat(CUST-WP-0014): repo sync automation & Gitea inventory
- Migration e2f3a4b5c6d7: add last_state_synced_at to managed_repos
- consistency_check.py: PATCH last_state_synced_at after fix run;
  fix ~ treated as non-empty state_hub_task_id (C-03 vs C-11);
  fix _inject_task_id_into_block skipping injection when field exists
  with null value
- install_hooks.sh: idempotent post-commit hook installer for all
  registered repos (make install-hooks REPO= / install-hooks-all)
- gitea_inventory.py: compare coulomb Gitea org against state-hub
  registered repos — registered / unregistered / hub-only sections
- infra/README.md: document systemd user timer + crontab fallback
- systemd user timer: custodian-sync.{service,timer} runs
  fix-consistency-all every 15 min (enabled)
- dashboard/src/repo-sync.md: Repo Sync Health page — sync age table,
  unregistered Gitea repos, hub-only repos
- api/routers/repos.py: GET /repos/{slug}/dispatch endpoint returning
  active goal, pending tasks per workstream, human interventions
- mcp_server/server.py: get_repo_dispatch() MCP tool

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 01:41:16 +01:00

150 lines
4.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# install_hooks.sh — install a custodian post-commit sync hook into registered repos.
#
# Usage:
# ./install_hooks.sh --repo <slug> # install into one repo
# ./install_hooks.sh --all # install into all registered repos
# ./install_hooks.sh --repo <slug> --remove # remove hook from one repo
# ./install_hooks.sh --all --remove # remove hook from all repos
#
# The hook runs `make fix-consistency REPO=<slug>` in the state-hub after each
# commit, keeping the hub in sync with workplan file changes automatically.
#
# Idempotent: the hook block is guarded by a marker comment. Running twice is safe.
set -euo pipefail
STATEHUB_DIR="$(cd "$(dirname "$0")/.." && pwd)"
API_BASE="${STATE_HUB_API:-http://127.0.0.1:8000}"
MARKER="# custodian-sync-hook"
usage() {
echo "Usage: $0 --repo <slug> | --all [--remove]"
exit 1
}
# ── Arg parsing ───────────────────────────────────────────────────────────────
REPO_SLUG=""
DO_ALL=false
REMOVE=false
while [[ $# -gt 0 ]]; do
case "$1" in
--repo) REPO_SLUG="$2"; shift 2 ;;
--all) DO_ALL=true; shift ;;
--remove) REMOVE=true; shift ;;
-h|--help) usage ;;
*) echo "Unknown argument: $1"; usage ;;
esac
done
if [[ -z "$REPO_SLUG" && "$DO_ALL" == false ]]; then usage; fi
# ── Helper: resolve local path for a repo slug ───────────────────────────────
resolve_path() {
local slug="$1"
# Try the registered local_path first
local api_path
api_path=$(curl -sf "${API_BASE}/repos/${slug}/" | python3 -c \
"import json,sys; d=json.load(sys.stdin); print(d.get('local_path') or '')" 2>/dev/null || true)
if [[ -n "$api_path" && -d "$api_path" ]]; then
echo "$api_path"
return
fi
# Fall back to convention: /home/<user>/<slug>
local conventional="/home/$(whoami)/${slug}"
if [[ -d "$conventional" ]]; then
echo "$conventional"
return
fi
echo ""
}
# ── Helper: install hook into one repo ───────────────────────────────────────
install_hook() {
local slug="$1"
local repo_path
repo_path=$(resolve_path "$slug")
if [[ -z "$repo_path" ]]; then
echo "$slug: no local path found — skipping"
return
fi
if [[ ! -d "$repo_path/.git" ]]; then
echo "$slug: $repo_path is not a git repo — skipping"
return
fi
local hook_file="$repo_path/.git/hooks/post-commit"
local hook_block
hook_block=$(cat <<BLOCK
${MARKER} — managed by custodian, do not edit this block
if curl -sf ${API_BASE}/state/health >/dev/null 2>&1; then
(cd "${STATEHUB_DIR}" && make fix-consistency REPO=${slug} >/dev/null 2>&1 &)
fi
${MARKER}-end
BLOCK
)
if [[ -f "$hook_file" ]] && grep -q "$MARKER" "$hook_file"; then
echo "$slug: hook already present at $hook_file"
return
fi
if [[ -f "$hook_file" ]]; then
# Prepend to existing hook
local existing
existing=$(cat "$hook_file")
printf '#!/usr/bin/env bash\n%s\n\n%s\n' "$hook_block" "$existing" > "$hook_file"
else
printf '#!/usr/bin/env bash\n%s\n' "$hook_block" > "$hook_file"
fi
chmod +x "$hook_file"
echo "$slug: hook installed at $hook_file"
}
# ── Helper: remove hook from one repo ────────────────────────────────────────
remove_hook() {
local slug="$1"
local repo_path
repo_path=$(resolve_path "$slug")
if [[ -z "$repo_path" || ! -f "$repo_path/.git/hooks/post-commit" ]]; then
echo " $slug: no hook file found — skipping"
return
fi
local hook_file="$repo_path/.git/hooks/post-commit"
if ! grep -q "$MARKER" "$hook_file"; then
echo " $slug: custodian marker not found in hook — skipping"
return
fi
# Remove the marked block (between MARKER and MARKER-end inclusive)
python3 - "$hook_file" <<'PY'
import sys, re
path = sys.argv[1]
text = open(path).read()
cleaned = re.sub(
r'# custodian-sync-hook.*?# custodian-sync-hook-end\n?',
'',
text,
flags=re.DOTALL,
)
open(path, 'w').write(cleaned)
PY
echo " 🗑 $slug: hook block removed from $hook_file"
}
# ── Collect repo slugs ────────────────────────────────────────────────────────
if $DO_ALL; then
mapfile -t SLUGS < <(curl -sf "${API_BASE}/repos/" | python3 -c \
"import json,sys; [print(r['slug']) for r in json.load(sys.stdin) if r.get('status') == 'active']")
else
SLUGS=("$REPO_SLUG")
fi
echo "Custodian sync hook — $( $REMOVE && echo 'removing' || echo 'installing' ) for ${#SLUGS[@]} repo(s)"
for slug in "${SLUGS[@]}"; do
if $REMOVE; then remove_hook "$slug"; else install_hook "$slug"; fi
done
echo "Done."