Files
holocron/tests/test-check-release-needed.sh
Defame1297 14c2c91521 fix(lint): flag release-relevant paths retired since the last tag
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
2026-08-09 13:32:30 +00:00

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 ]]