Coverage was derived from the worktree alone, so the -d guard on a hook's bundled assets/ tree meant deleting the whole tree removed it from the pathspec instead of flagging it — the gate stayed silent about a change that breaks every consumer at the next rev:. The path set is now derived twice, from the worktree manifest and from the manifest at $LAST_TAG, then unioned. A path the tag exposed but HEAD no longer does is a removal pinned consumers must be told about; a path only HEAD exposes is new contract surface. Both need flagging. Fails closed on an unreadable tagged tree (shallow clone), and treats a readable root tree with no manifest as "added since the tag". tokens[0] needed no exit-code fix — it carries no existence guard, so both deletion cases already exited non-zero. What was wrong was the reporting: a fully retired hook could no longer be named in the failure message. The tagged manifest fixes that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
274 lines
13 KiB
Bash
Executable File
274 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
SCRIPT="$REPO_ROOT/scripts/check-release-needed.sh"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
|
|
# Both entry shapes the real .pre-commit-hooks.yaml ships: a bare script with no
|
|
# bundled data, and a bare script whose sibling assets/ tree it self-locates at
|
|
# runtime. Neither carries arguments — pre-commit only rewrites entry[0] to the
|
|
# hook-repo clone path, so an argument path would resolve against the consuming
|
|
# repo. RELEASE_PATHS is derived from the manifest rather than hand-maintained,
|
|
# so it has to cope with both.
|
|
HOOK_DIR="plugins/demo/skills/demo-audit"
|
|
|
|
write_manifest() {
|
|
local dir="$1"
|
|
cat > "$dir/.pre-commit-hooks.yaml" <<EOF
|
|
- id: fake-size-check
|
|
entry: scripts/skill-size-check.sh
|
|
language: script
|
|
- id: fake-vale-check
|
|
entry: $HOOK_DIR/scripts/vale-wrap.sh
|
|
language: script
|
|
EOF
|
|
}
|
|
|
|
# Helper: writes the files both manifest entries expose — the two hook scripts
|
|
# plus the bundled Vale config and style rule the second one self-locates.
|
|
write_release_paths() {
|
|
local dir="$1"
|
|
mkdir -p "$dir/scripts" "$dir/$HOOK_DIR/scripts" "$dir/$HOOK_DIR/assets/vale/styles/Kyberforge"
|
|
echo "v1" > "$dir/scripts/skill-size-check.sh"
|
|
echo "v1" > "$dir/$HOOK_DIR/scripts/vale-wrap.sh"
|
|
echo "cfg" > "$dir/$HOOK_DIR/assets/vale/.vale.ini"
|
|
echo "rule: v1" > "$dir/$HOOK_DIR/assets/vale/styles/Kyberforge/DemoRule.yml"
|
|
}
|
|
|
|
# Helper: a fixture repo with a manifest and every release-relevant path it
|
|
# exposes, committed and tagged v1.0.0.
|
|
make_tagged_fixture() {
|
|
local dir
|
|
dir="$(mktemp -d)"
|
|
(cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t)
|
|
write_manifest "$dir"
|
|
write_release_paths "$dir"
|
|
(cd "$dir" && git add -A && git commit -q -m "initial" && git tag v1.0.0)
|
|
echo "$dir"
|
|
}
|
|
|
|
run_check() {
|
|
local dir="$1" branch="$2"
|
|
(cd "$dir" && PRE_COMMIT_REMOTE_BRANCH="$branch" bash "$SCRIPT" 2>&1)
|
|
}
|
|
|
|
CLEANUP_DIRS=()
|
|
trap 'rm -rf "${CLEANUP_DIRS[@]}"' EXIT
|
|
track() { CLEANUP_DIRS+=("$1"); }
|
|
|
|
# --- 1. Not targeting main: silent no-op regardless of state ---
|
|
echo ""
|
|
echo "--- exits 0 when not pushing to main, even with no tags ---"
|
|
FIXTURE1="$(mktemp -d)"; track "$FIXTURE1"
|
|
(cd "$FIXTURE1" && git init -q)
|
|
if run_check "$FIXTURE1" "refs/heads/feature-branch" > /dev/null; then
|
|
pass "exits 0 when target branch isn't main"
|
|
else
|
|
fail "exited non-zero on a non-main target branch"
|
|
fi
|
|
|
|
# --- 2. Targeting main, no tag exists at all: hard fail ---
|
|
echo ""
|
|
echo "--- exits 1 when targeting main and no tag exists ---"
|
|
FIXTURE2="$(mktemp -d)"; track "$FIXTURE2"
|
|
(cd "$FIXTURE2" && git init -q && git config user.email t@t.t && git config user.name t)
|
|
write_manifest "$FIXTURE2"
|
|
write_release_paths "$FIXTURE2"
|
|
(cd "$FIXTURE2" && git add -A && git commit -q -m "initial")
|
|
if run_check "$FIXTURE2" "refs/heads/main" > /dev/null; then
|
|
fail "exited 0 when targeting main with no tag — expected exit 1"
|
|
else
|
|
pass "exits non-zero when targeting main and no tag exists yet"
|
|
fi
|
|
|
|
# --- 3. Targeting main, tag exists, no release-relevant changes since: passes ---
|
|
echo ""
|
|
echo "--- exits 0 when targeting main and nothing release-relevant changed since the tag ---"
|
|
FIXTURE3="$(make_tagged_fixture)"; track "$FIXTURE3"
|
|
echo "unrelated" > "$FIXTURE3/README.md"
|
|
(cd "$FIXTURE3" && git add -A && git commit -q -m "unrelated change")
|
|
if run_check "$FIXTURE3" "refs/heads/main" > /dev/null; then
|
|
pass "exits 0 when only unrelated files changed since the tag"
|
|
else
|
|
fail "exited non-zero despite no release-relevant changes since the tag"
|
|
fi
|
|
|
|
# --- 4. Targeting main, tag exists, a release-relevant file changed since: hard fail ---
|
|
echo ""
|
|
echo "--- exits 1 when a release-relevant file changed since the tag ---"
|
|
FIXTURE4="$(make_tagged_fixture)"; track "$FIXTURE4"
|
|
echo "v2" > "$FIXTURE4/scripts/skill-size-check.sh"
|
|
(cd "$FIXTURE4" && git add -A && git commit -q -m "update release-relevant script")
|
|
OUT4=$(run_check "$FIXTURE4" "refs/heads/main" || true)
|
|
if echo "$OUT4" | grep -q "skill-size-check.sh"; then
|
|
pass "exits non-zero and names the changed file when a release-relevant path changed since the tag"
|
|
else
|
|
fail "did not flag the release-relevant file that changed since the tag"
|
|
fi
|
|
|
|
# --- 5. Not targeting main even with release-relevant changes and a tag: still a no-op ---
|
|
echo ""
|
|
echo "--- exits 0 on a feature branch even with release-relevant changes since the tag ---"
|
|
FIXTURE5="$(make_tagged_fixture)"; track "$FIXTURE5"
|
|
echo "v2" > "$FIXTURE5/scripts/skill-size-check.sh"
|
|
(cd "$FIXTURE5" && git add -A && git commit -q -m "update release-relevant script")
|
|
if run_check "$FIXTURE5" "refs/heads/some-feature" > /dev/null; then
|
|
pass "exits 0 on a feature branch regardless of un-tagged release-relevant changes"
|
|
else
|
|
fail "hard-failed on a feature branch — should only ever fail when targeting main"
|
|
fi
|
|
|
|
# --- 6. A release-relevant path deleted since the tag is still flagged ---
|
|
echo ""
|
|
echo "--- exits 1 when a release-relevant path was deleted since the tag, not just modified ---"
|
|
FIXTURE6="$(make_tagged_fixture)"; track "$FIXTURE6"
|
|
rm -f "$FIXTURE6/$HOOK_DIR/assets/vale/.vale.ini"
|
|
(cd "$FIXTURE6" && git add -A && git commit -q -m "delete the bundled vale config")
|
|
OUT6=$(run_check "$FIXTURE6" "refs/heads/main" || true)
|
|
if echo "$OUT6" | grep -q "assets/vale/.vale.ini"; then
|
|
pass "flags a deleted release-relevant path instead of silently dropping it from the diff"
|
|
else
|
|
fail "did not flag deletion of a release-relevant path since the tag"
|
|
fi
|
|
|
|
# --- 7. A git diff failure hard-fails instead of reading as a clean pass ---
|
|
echo ""
|
|
echo "--- exits 1 (not a silent pass) when the underlying git diff errors out ---"
|
|
FIXTURE7="$(make_tagged_fixture)"; track "$FIXTURE7"
|
|
TAG_TREE="$(cd "$FIXTURE7" && git rev-parse 'v1.0.0^{tree}')"
|
|
echo "v2" > "$FIXTURE7/scripts/skill-size-check.sh"
|
|
(cd "$FIXTURE7" && git add -A && git commit -q -m "advance past the tag")
|
|
rm -f "$FIXTURE7/.git/objects/${TAG_TREE:0:2}/${TAG_TREE:2}"
|
|
if run_check "$FIXTURE7" "refs/heads/main" > /dev/null; then
|
|
fail "silently exited 0 when the underlying git diff failed"
|
|
else
|
|
pass "hard-fails instead of silently passing when git diff can't be computed"
|
|
fi
|
|
|
|
# --- 8. A non-version tag reachable from HEAD does not become the diff baseline ---
|
|
echo ""
|
|
echo "--- ignores a non-vX.Y.Z tag and still flags a change since the real release tag ---"
|
|
FIXTURE8="$(make_tagged_fixture)"; track "$FIXTURE8"
|
|
echo "checkpoint" > "$FIXTURE8/scripts/skill-size-check.sh"
|
|
(cd "$FIXTURE8" && git add -A && git commit -q -m "checkpoint work" && git tag checkpoint-1)
|
|
echo "v2" > "$FIXTURE8/scripts/skill-size-check.sh"
|
|
(cd "$FIXTURE8" && git add -A && git commit -q -m "real release-relevant change")
|
|
OUT8=$(run_check "$FIXTURE8" "refs/heads/main" || true)
|
|
if echo "$OUT8" | grep -q "skill-size-check.sh"; then
|
|
pass "still flags the release-relevant change since v1.0.0, ignoring the non-version checkpoint tag"
|
|
else
|
|
fail "an incidental non-version tag shifted the baseline and hid a real release-relevant change"
|
|
fi
|
|
|
|
# --- 9. A file outside every manifest entry does not trigger a fail ---
|
|
echo ""
|
|
echo "--- exits 0 when a changed file sits near, but isn't referenced by, a manifest entry ---"
|
|
FIXTURE9="$(make_tagged_fixture)"; track "$FIXTURE9"
|
|
echo "irrelevant" > "$FIXTURE9/scripts/unrelated-helper.sh"
|
|
(cd "$FIXTURE9" && git add -A && git commit -q -m "add an unrelated script alongside the exposed one")
|
|
if run_check "$FIXTURE9" "refs/heads/main" > /dev/null; then
|
|
pass "exits 0 for a file that lives alongside, but isn't referenced by, any manifest entry"
|
|
else
|
|
fail "flagged a file that no .pre-commit-hooks.yaml entry actually exposes"
|
|
fi
|
|
|
|
# --- 10. A change confined to a hook's bundled styles/ tree is release-relevant ---
|
|
# The manifest entry names only the wrapper script; the Vale rules it enforces
|
|
# live in the sibling assets/ tree it self-locates at runtime. If that tree is
|
|
# not covered, editing a rule and landing it on main demands no new tag, and a
|
|
# consumer pinned to the old rev keeps the stale rules forever.
|
|
echo ""
|
|
echo "--- exits 1 when only a bundled Vale style rule changed since the tag ---"
|
|
FIXTURE10="$(make_tagged_fixture)"; track "$FIXTURE10"
|
|
echo "rule: v2" > "$FIXTURE10/$HOOK_DIR/assets/vale/styles/Kyberforge/DemoRule.yml"
|
|
(cd "$FIXTURE10" && git add -A && git commit -q -m "tighten a vale rule")
|
|
OUT10=$(run_check "$FIXTURE10" "refs/heads/main" || true)
|
|
if echo "$OUT10" | grep -q "assets/vale/styles/Kyberforge/DemoRule.yml"; then
|
|
pass "flags a change confined to a hook's bundled assets/vale/styles/ tree"
|
|
else
|
|
fail "a bundled Vale style rule changed since the tag without demanding a release"
|
|
fi
|
|
|
|
# --- 11. The assets/ derivation must not invent a path for a bundle-less hook ---
|
|
# scripts/skill-size-check.sh has no sibling assets/ tree, so its derived
|
|
# candidate normalises to a bare top-level assets/ — a directory this repo does
|
|
# not ship. Adding it unconditionally would make any unrelated repo-root
|
|
# assets/ file falsely demand a release.
|
|
echo ""
|
|
echo "--- exits 0 when a top-level assets/ file changed but no hook bundles one ---"
|
|
FIXTURE11="$(make_tagged_fixture)"; track "$FIXTURE11"
|
|
mkdir -p "$FIXTURE11/assets"
|
|
echo "unrelated" > "$FIXTURE11/assets/logo.txt"
|
|
(cd "$FIXTURE11" && git add -A && git commit -q -m "add an unrelated top-level assets file")
|
|
if run_check "$FIXTURE11" "refs/heads/main" > /dev/null; then
|
|
pass "exits 0 for a top-level assets/ file that no manifest entry bundles"
|
|
else
|
|
fail "invented a bogus assets/ path for a hook script with no bundled tree"
|
|
fi
|
|
|
|
# --- 12. Deleting a hook's entire bundled assets/ tree is release-relevant ---
|
|
# The worktree-only derivation guarded the assets/ path on the directory still
|
|
# existing, so wiping the whole tree removed the path from the pathspec instead
|
|
# of diffing it: the single most consumer-breaking change possible diffed clean.
|
|
# The path list therefore has to be unioned with what $LAST_TAG exposed.
|
|
echo ""
|
|
echo "--- exits 1 when a hook's entire bundled assets/ tree was deleted since the tag ---"
|
|
FIXTURE12="$(make_tagged_fixture)"; track "$FIXTURE12"
|
|
rm -rf "${FIXTURE12:?}/$HOOK_DIR/assets"
|
|
(cd "$FIXTURE12" && git add -A && git commit -q -m "delete the whole bundled assets tree")
|
|
OUT12=$(run_check "$FIXTURE12" "refs/heads/main" || true)
|
|
if echo "$OUT12" | grep -q "assets/vale/.vale.ini"; then
|
|
pass "flags a wholesale deletion of a hook's bundled assets/ tree"
|
|
else
|
|
fail "a hook's entire bundled assets/ tree vanished since the tag without demanding a release"
|
|
fi
|
|
|
|
# --- 13. A hook script deleted while its manifest entry survives is flagged ---
|
|
# Characterisation test, not a bug fix: tokens[0] is added to the pathspec
|
|
# unconditionally (no existence guard), so this case was already covered. It is
|
|
# pinned here so the tagged-tree union can't accidentally introduce an existence
|
|
# guard on tokens[0] and reopen the hole its assets/ sibling had.
|
|
echo ""
|
|
echo "--- exits 1 when a hook script was deleted but its manifest entry remains ---"
|
|
FIXTURE13="$(make_tagged_fixture)"; track "$FIXTURE13"
|
|
rm -f "$FIXTURE13/$HOOK_DIR/scripts/vale-wrap.sh"
|
|
(cd "$FIXTURE13" && git add -A && git commit -q -m "delete a hook script, keep its manifest entry")
|
|
OUT13=$(run_check "$FIXTURE13" "refs/heads/main" || true)
|
|
if echo "$OUT13" | grep -q "vale-wrap.sh"; then
|
|
pass "flags a hook script deleted out from under a surviving manifest entry"
|
|
else
|
|
fail "a manifest entry's script vanished since the tag without demanding a release"
|
|
fi
|
|
|
|
# --- 14. Retiring a whole hook names what the tag exposed, not just the manifest ---
|
|
# Removing the entry and everything it shipped changes $HOOKS_MANIFEST, so the
|
|
# gate fires either way — but a derivation that only reads the current manifest
|
|
# can no longer name the retired script or its assets, and the failure message
|
|
# understates the breakage to consumers pinned at the old rev. The tagged
|
|
# manifest is what makes those paths reportable.
|
|
echo ""
|
|
echo "--- names the retired hook's own paths when an entry and its files are removed together ---"
|
|
FIXTURE14="$(make_tagged_fixture)"; track "$FIXTURE14"
|
|
cat > "$FIXTURE14/.pre-commit-hooks.yaml" <<'EOF'
|
|
- id: fake-size-check
|
|
entry: scripts/skill-size-check.sh
|
|
language: script
|
|
EOF
|
|
rm -rf "${FIXTURE14:?}/$HOOK_DIR"
|
|
(cd "$FIXTURE14" && git add -A && git commit -q -m "retire the vale hook entirely")
|
|
OUT14=$(run_check "$FIXTURE14" "refs/heads/main" || true)
|
|
if echo "$OUT14" | grep -q "vale-wrap.sh" && echo "$OUT14" | grep -q "assets/vale/.vale.ini"; then
|
|
pass "names the retired hook's script and bundled assets, not just the manifest edit"
|
|
else
|
|
fail "reported only the manifest change and hid which shipped paths the retirement removed"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|