chore: add post-push hook to auto-refresh kyberforge plugin cache
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P87CiC58Ru2PPWYTeXtjHT
This commit is contained in:
17
scripts/git-hooks/post-push
Executable file
17
scripts/git-hooks/post-push
Executable file
@@ -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
|
||||
@@ -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
|
||||
|
||||
119
tests/test-git-hooks-install.sh
Executable file
119
tests/test-git-hooks-install.sh
Executable file
@@ -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 ]]
|
||||
108
tests/test-post-push.sh
Executable file
108
tests/test-post-push.sh
Executable file
@@ -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 ]]
|
||||
Reference in New Issue
Block a user