#!/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" < "$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" } # Helper: a fixture whose manifest carries one malformed entry: at the tag *and* # at HEAD, plus a post-tag change to the file that entry was meant to cover. # Committing the bad entry before the tag is what makes the assertion sharp — an # edited manifest is itself release-relevant, so the gate would fail for the # wrong reason and hide a parser that degrades silently. make_malformed_fixture() { local entry="$1" dir dir="$(mktemp -d)" (cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t) write_release_paths "$dir" cat > "$dir/.pre-commit-hooks.yaml" < "$dir/scripts/skill-size-check.sh" (cd "$dir" && git add -A && git commit -q -m "change the file the malformed entry should cover") echo "$dir" } # $3 is optional: pre-commit's PRE_COMMIT_TO_REF, the local sha being pushed. # Left off entirely, the variable stays unset and the script falls back to HEAD, # exactly as a plain `git push ` behaves. # The fixture repo is the subject under test, so every PRE_COMMIT_* input must # come from this function and nowhere else. Any such variable already in the # environment belongs to the *caller's* repo: run under the pre-push hook this # suite guards, PRE_COMMIT_TO_REF holds a sha of the real repo, which does not # exist in the fixture, and the script resolves against the wrong rev. Clearing # them is what makes a standalone run and a pre-push run the same test — this # suite passed everywhere except under the hook it exists to protect. run_check() { local dir="$1" branch="$2" if [[ $# -ge 3 ]]; then (cd "$dir" && unset PRE_COMMIT_FROM_REF \ && PRE_COMMIT_REMOTE_BRANCH="$branch" PRE_COMMIT_TO_REF="$3" bash "$SCRIPT" 2>&1) else (cd "$dir" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF \ && PRE_COMMIT_REMOTE_BRANCH="$branch" bash "$SCRIPT" 2>&1) fi } 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 # --- 15. A multi-token entry: is rejected loudly, not silently mis-parsed --- # ADR-0014 binds entries to a bare script path, but nothing enforced it, and the # sibling .pre-commit-config.yaml already ships `entry: bash