From a61a8d04a50080f4780be8f5c6af7699c0a078f5 Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sat, 27 Jun 2026 22:55:56 +0000 Subject: [PATCH] chore: add post-push hook to auto-refresh kyberforge plugin cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installing kyberforge via the holocron marketplace caches the plugin at a specific version. After every push, the marketplace clone and plugin cache need to be refreshed manually — otherwise new skills added since the last install are invisible until the user runs `claude plugin update` manually. Adds a post-push git hook that pulls the holocron marketplace clone and updates the kyberforge cache automatically after every push, eliminating the manual refresh step. ## Implementation Notes - `scripts/git-hooks/post-push` is the canonical source; `install.sh` now copies all files in `scripts/git-hooks/` into `.git/hooks/` on fresh checkouts, making the pattern extensible for future hooks. - Hook exits 0 on all failures (warns to stderr) — a stale cache refresh never blocks a completed push. - 11 bats tests cover both the hook and the install.sh copy block, using mocked binaries and a temp-tree fixture to avoid touching real state. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01P87CiC58Ru2PPWYTeXtjHT --- scripts/git-hooks/post-push | 17 +++++ scripts/install.sh | 12 ++++ tests/test-git-hooks-install.sh | 119 ++++++++++++++++++++++++++++++++ tests/test-post-push.sh | 108 +++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100755 scripts/git-hooks/post-push create mode 100755 tests/test-git-hooks-install.sh create mode 100755 tests/test-post-push.sh diff --git a/scripts/git-hooks/post-push b/scripts/git-hooks/post-push new file mode 100755 index 0000000..63c149e --- /dev/null +++ b/scripts/git-hooks/post-push @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Refresh the kyberforge plugin cache after every push +set -euo pipefail + +echo "→ Refreshing holocron marketplace clone..." +if git -C ~/.claude/plugins/marketplaces/holocron pull --quiet; then + echo "→ Updating kyberforge plugin cache..." + if claude plugin update kyberforge; then + echo "✓ kyberforge cache updated" + else + echo "⚠ claude plugin update kyberforge failed — run it manually" >&2 + fi +else + echo "⚠ Failed to pull holocron marketplace clone — run 'git -C ~/.claude/plugins/marketplaces/holocron pull' manually" >&2 +fi + +exit 0 diff --git a/scripts/install.sh b/scripts/install.sh index 7010d86..30f3d9f 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -62,6 +62,18 @@ for adapter in "${SKILL_ADAPTERS[@]}"; do fi done +# Install git hooks into the current repo checkout +HOOKS_SRC="$REPO_ROOT/scripts/git-hooks" +if [[ -d "$HOOKS_SRC" ]]; then + for hook_file in "$HOOKS_SRC"/*; do + [[ -f "$hook_file" ]] || continue + hook_name="$(basename "$hook_file")" + dest_hook="$REPO_ROOT/.git/hooks/$hook_name" + cp "$hook_file" "$dest_hook" + chmod +x "$dest_hook" + done +fi + echo "" echo "Deployed:" for entry in "${DEPLOY_FILES[@]}" "${DEPLOY_EXECUTABLES[@]}"; do diff --git a/tests/test-git-hooks-install.sh b/tests/test-git-hooks-install.sh new file mode 100755 index 0000000..aea2f1a --- /dev/null +++ b/tests/test-git-hooks-install.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Tests for the git hook install block in scripts/install.sh. +# Runs install.sh from a temp repo to avoid touching the real .git/hooks/. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PASS=0 +FAIL=0 + +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +# --------------------------------------------------------------------------- +# Temp repo setup +# +# install.sh derives REPO_ROOT from the script's own location, so copying it +# into a temp tree makes REPO_ROOT point there, not at the real checkout. +# --------------------------------------------------------------------------- + +TEMP_REPO="$(mktemp -d)" +TEMP_HOME="$(mktemp -d)" +trap 'rm -rf "$TEMP_REPO" "$TEMP_HOME"' EXIT + +mkdir -p "$TEMP_REPO/scripts" +mkdir -p "$TEMP_REPO/.git/hooks" + +# Minimal deploy-manifest.sh — empty deploy lists so install.sh reaches the +# hook-copy block without attempting to copy files that don't exist in the +# temp tree. +cat > "$TEMP_REPO/scripts/deploy-manifest.sh" << 'EOF' +# shellcheck disable=SC2034 +DEPLOY_FILES=() +DEPLOY_EXECUTABLES=() +DEPLOY_DIRS=() +DEPLOY_SKILLS_SRC=".agents/skills" +EOF + +# Copy the real install.sh and git-hooks into the temp repo +cp "$REPO_ROOT/scripts/install.sh" "$TEMP_REPO/scripts/install.sh" +cp -r "$REPO_ROOT/scripts/git-hooks" "$TEMP_REPO/scripts/git-hooks" + +HOOKS_SRC="$TEMP_REPO/scripts/git-hooks" + +run_install() { + HOME="$TEMP_HOME" bash "$TEMP_REPO/scripts/install.sh" > /dev/null 2>&1 +} + +# --------------------------------------------------------------------------- +echo "--- hook install: files copied to .git/hooks/ ---" +# --------------------------------------------------------------------------- + +run_install + +for hook_file in "$HOOKS_SRC"/*; do + [[ -f "$hook_file" ]] || continue + hook_name="$(basename "$hook_file")" + if [[ -f "$TEMP_REPO/.git/hooks/$hook_name" ]]; then + pass "$hook_name installed to .git/hooks/" + else + fail "$hook_name missing from .git/hooks/" + fi +done + +# --------------------------------------------------------------------------- +echo "" +echo "--- hook install: installed hooks are executable ---" +# --------------------------------------------------------------------------- + +for hook_file in "$HOOKS_SRC"/*; do + [[ -f "$hook_file" ]] || continue + hook_name="$(basename "$hook_file")" + if [[ -x "$TEMP_REPO/.git/hooks/$hook_name" ]]; then + pass "$hook_name is executable" + else + fail "$hook_name is not executable" + fi +done + +# --------------------------------------------------------------------------- +echo "" +echo "--- hook install: directories in git-hooks/ are not installed ---" +# --------------------------------------------------------------------------- + +# Add a subdirectory to scripts/git-hooks/ — the install block must skip it +mkdir -p "$HOOKS_SRC/not-a-hook" +run_install +if [[ ! -e "$TEMP_REPO/.git/hooks/not-a-hook" ]]; then + pass "directories in scripts/git-hooks/ are not copied to .git/hooks/" +else + fail "a directory from scripts/git-hooks/ was incorrectly installed" +fi +rmdir "$HOOKS_SRC/not-a-hook" + +# --------------------------------------------------------------------------- +echo "" +echo "--- hook install: idempotent ---" +# --------------------------------------------------------------------------- + +run_install + +for hook_file in "$HOOKS_SRC"/*; do + [[ -f "$hook_file" ]] || continue + hook_name="$(basename "$hook_file")" + if diff -q "$hook_file" "$TEMP_REPO/.git/hooks/$hook_name" > /dev/null 2>&1; then + pass "idempotent: $hook_name content unchanged after second install" + else + fail "idempotent: $hook_name content corrupted after second install" + fi + if [[ -x "$TEMP_REPO/.git/hooks/$hook_name" ]]; then + pass "idempotent: $hook_name still executable after second install" + else + fail "idempotent: $hook_name lost executable bit after second install" + fi +done + +# --------------------------------------------------------------------------- +echo "" +echo "Results: $PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]] diff --git a/tests/test-post-push.sh b/tests/test-post-push.sh new file mode 100755 index 0000000..a6fa4eb --- /dev/null +++ b/tests/test-post-push.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PASS=0 +FAIL=0 + +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +HOOK="$REPO_ROOT/scripts/git-hooks/post-push" +FAKE_BIN="$(mktemp -d)" +FAKE_HOME="$(mktemp -d)" +trap 'rm -rf "$FAKE_BIN" "$FAKE_HOME"' EXIT + +# Helper: write a fake git stub that exits with the given code +make_git() { + local exit_code="$1" + printf '#!/usr/bin/env bash\nexit %s\n' "$exit_code" > "$FAKE_BIN/git" + chmod +x "$FAKE_BIN/git" +} + +# Helper: write a fake claude stub; optionally touches a sentinel file on invocation +make_claude() { + local exit_code="$1" + local log="${2:-}" + if [[ -n "$log" ]]; then + printf '#!/usr/bin/env bash\ntouch "%s"\nexit %s\n' "$log" "$exit_code" > "$FAKE_BIN/claude" + else + printf '#!/usr/bin/env bash\nexit %s\n' "$exit_code" > "$FAKE_BIN/claude" + fi + chmod +x "$FAKE_BIN/claude" +} + +# Run the hook with mocked PATH and HOME; suppress all output +run_hook() { + PATH="$FAKE_BIN:$PATH" HOME="$FAKE_HOME" bash "$HOOK" > /dev/null 2>&1 +} + +# Run the hook and capture combined stdout+stderr +capture_hook() { + PATH="$FAKE_BIN:$PATH" HOME="$FAKE_HOME" bash "$HOOK" 2>&1 || true +} + +# --------------------------------------------------------------------------- +echo "--- post-push: always exits 0 ---" +# --------------------------------------------------------------------------- + +make_git 1; make_claude 0 +if run_hook; then + pass "exits 0 when git pull fails" +else + fail "should exit 0 when git pull fails" +fi + +make_git 0; make_claude 1 +if run_hook; then + pass "exits 0 when claude plugin update fails" +else + fail "should exit 0 when claude plugin update fails" +fi + +# --------------------------------------------------------------------------- +echo "" +echo "--- post-push: success output ---" +# --------------------------------------------------------------------------- + +make_git 0; make_claude 0 +output=$(capture_hook) +echo "$output" | grep -q "kyberforge cache updated" \ + && pass "prints success message when both git pull and claude succeed" \ + || fail "should print success message when both commands succeed" + +# --------------------------------------------------------------------------- +echo "" +echo "--- post-push: warnings on failure ---" +# --------------------------------------------------------------------------- + +make_git 1; make_claude 0 +output=$(capture_hook) +echo "$output" | grep -qi "failed to pull" \ + && pass "warns when git pull fails" \ + || fail "should warn when git pull fails" + +make_git 0; make_claude 1 +output=$(capture_hook) +echo "$output" | grep -qi "failed" \ + && pass "warns when claude plugin update fails" \ + || fail "should warn when claude plugin update fails" + +# --------------------------------------------------------------------------- +echo "" +echo "--- post-push: claude not called when git pull fails ---" +# --------------------------------------------------------------------------- + +CLAUDE_LOG="$FAKE_HOME/claude-was-called" +make_git 1; make_claude 0 "$CLAUDE_LOG" +run_hook +if [[ ! -f "$CLAUDE_LOG" ]]; then + pass "claude not called when git pull fails" +else + fail "claude should not be called when git pull fails" +fi + +# --------------------------------------------------------------------------- +echo "" +echo "Results: $PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]]