check-skill-version-bump fails a push when a skill directory changed against its merge-base with main (tests/ excluded) without a strictly higher metadata.version than main. New, renamed and deleted skills are exempt; every plugin is covered. Recorded as a dated section in ADR-0022 and documented in gates.md. Patch-bumps the 17 skills that changed on this branch without a bump, so the branch passes its own gate. Simplification audit finding 33. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
253 lines
8.6 KiB
Bash
Executable File
253 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
SCRIPT="$REPO_ROOT/scripts/check-skill-version-bump.sh"
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
|
|
# The gate reads versions with python3 + PyYAML, the same hard requirement
|
|
# skill-size-check carries. Without them this suite cannot run: exit 77 so
|
|
# run-tests reports SKIPPED (and --strict fails the push).
|
|
if ! python3 -c 'import yaml' 2>/dev/null; then
|
|
echo "SKIP: python3 with PyYAML is required" >&2
|
|
exit 77
|
|
fi
|
|
|
|
# Output assertions use here-strings, never `echo | grep -q` (pipefail race;
|
|
# see tests/test-check-release-needed.sh).
|
|
|
|
CLEANUP_DIRS=()
|
|
trap 'rm -rf ${CLEANUP_DIRS[@]+"${CLEANUP_DIRS[@]}"}' EXIT
|
|
|
|
# write_skill <repo> <plugin> <skill> <version-line|""> [body]
|
|
# An empty version line writes a SKILL.md with no metadata block at all.
|
|
write_skill() {
|
|
local repo="$1" plugin="$2" skill="$3" vline="$4" body="${5:-body}"
|
|
local d="$repo/plugins/$plugin/.apm/skills/$skill"
|
|
mkdir -p "$d"
|
|
{
|
|
echo "---"
|
|
echo "name: $skill"
|
|
echo "description: Use when testing."
|
|
if [[ -n "$vline" ]]; then
|
|
echo "metadata:"
|
|
echo " $vline"
|
|
fi
|
|
echo "---"
|
|
echo "$body"
|
|
} > "$d/SKILL.md"
|
|
}
|
|
|
|
commit() { (cd "$1" && git add -A && git commit -q -m "${2:-change}"); }
|
|
|
|
# A fixture with skills alpha (1.0.0) and beta (1.0.9) on main, then checked out
|
|
# onto a feature branch.
|
|
make_fixture() {
|
|
local dir
|
|
dir="$(mktemp -d)"
|
|
CLEANUP_DIRS+=("$dir")
|
|
(cd "$dir" && git init -q -b main && git config user.email t@t.t && git config user.name t)
|
|
write_skill "$dir" demo alpha 'version: "1.0.0"'
|
|
write_skill "$dir" demo beta "version: 1.0.9"
|
|
commit "$dir" initial
|
|
(cd "$dir" && git checkout -q -b feature)
|
|
echo "$dir"
|
|
}
|
|
|
|
# Every PRE_COMMIT_* input comes from here: a value inherited from the pre-push
|
|
# hook running this suite names a sha of the real repo, not the fixture.
|
|
run_check() {
|
|
local dir="$1"
|
|
if [[ $# -ge 2 ]]; then
|
|
(cd "$dir" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_REMOTE_BRANCH \
|
|
&& PRE_COMMIT_TO_REF="$2" bash "$SCRIPT" 2>&1)
|
|
else
|
|
(cd "$dir" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF PRE_COMMIT_REMOTE_BRANCH \
|
|
&& bash "$SCRIPT" 2>&1)
|
|
fi
|
|
}
|
|
|
|
# expect_pass <desc> <dir> [to-ref]
|
|
expect_pass() {
|
|
local desc="$1"; shift
|
|
local out
|
|
if out="$(run_check "$@")" && [[ -z "$out" ]]; then
|
|
pass "$desc"
|
|
else
|
|
fail "$desc — expected silent exit 0, got: $out"
|
|
fi
|
|
}
|
|
|
|
# expect_fail <desc> <pattern> <dir> [to-ref]
|
|
expect_fail() {
|
|
local desc="$1" pattern="$2"; shift 2
|
|
local out
|
|
if out="$(run_check "$@")"; then
|
|
fail "$desc — expected non-zero exit, got 0"
|
|
elif grep -qE "$pattern" <<< "$out"; then
|
|
pass "$desc"
|
|
else
|
|
fail "$desc — output did not match /$pattern/: $out"
|
|
fi
|
|
}
|
|
|
|
echo ""
|
|
echo "--- 1. unchanged skill passes ---"
|
|
F="$(make_fixture)"
|
|
echo "unrelated" > "$F/README.md"; commit "$F"
|
|
expect_pass "no skill change passes silently" "$F"
|
|
|
|
echo ""
|
|
echo "--- 2. changed without bump fails ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha 'version: "1.0.0"' "new body"; commit "$F"
|
|
expect_fail "unbumped change fails and names baseline and current" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
|
|
|
|
echo ""
|
|
echo "--- 3. changed with patch / minor / major bump passes ---"
|
|
for v in 1.0.1 1.1.0 2.0.0; do
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha "version: \"$v\"" "new body"; commit "$F"
|
|
expect_pass "bump to $v passes" "$F"
|
|
done
|
|
|
|
echo ""
|
|
echo "--- 4. version decreased fails ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo beta "version: 1.0.8" "new body"; commit "$F"
|
|
expect_fail "decrease fails" "beta: 1\.0\.9 -> 1\.0\.8" "$F"
|
|
|
|
echo ""
|
|
echo "--- 5. change to a non-SKILL.md file still counts ---"
|
|
F="$(make_fixture)"
|
|
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
|
|
echo "ref" > "$F/plugins/demo/.apm/skills/alpha/references/x.md"; commit "$F"
|
|
expect_fail "references/ change without bump fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
|
|
|
|
echo ""
|
|
echo "--- 6. tests/-only change passes without bump ---"
|
|
F="$(make_fixture)"
|
|
mkdir -p "$F/plugins/demo/.apm/skills/alpha/tests"
|
|
echo "t" > "$F/plugins/demo/.apm/skills/alpha/tests/test-x.sh"; commit "$F"
|
|
expect_pass "tests/-only change is exempt" "$F"
|
|
|
|
echo ""
|
|
echo "--- 7. new skill exempt ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo gamma 'version: "0.1.0"'; commit "$F"
|
|
expect_pass "new skill passes" "$F"
|
|
|
|
echo ""
|
|
echo "--- 8. deleted skill exempt ---"
|
|
F="$(make_fixture)"
|
|
rm -rf "$F/plugins/demo/.apm/skills/alpha"; commit "$F"
|
|
expect_pass "deleted skill passes" "$F"
|
|
|
|
echo ""
|
|
echo "--- 9. renamed skill exempt ---"
|
|
F="$(make_fixture)"
|
|
(cd "$F" && git mv plugins/demo/.apm/skills/alpha plugins/demo/.apm/skills/alpha2)
|
|
commit "$F"
|
|
expect_pass "renamed skill passes (old absent at pushed, new absent at baseline)" "$F"
|
|
|
|
echo ""
|
|
echo "--- 10. missing version on changed skill fails ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha "" "new body"; commit "$F"
|
|
expect_fail "missing version fails" "alpha: metadata\.version missing" "$F"
|
|
|
|
echo ""
|
|
echo "--- 11. malformed / prerelease version fails ---"
|
|
for v in 'version: 1.1' 'version: "1.0.1-rc1"'; do
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha "$v" "new body"; commit "$F"
|
|
expect_fail "'$v' is rejected like skill-size-check rejects it" "alpha: metadata\.version missing or not" "$F"
|
|
done
|
|
|
|
echo ""
|
|
echo "--- 12. multiple offenders all reported ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha 'version: "1.0.0"' "x"
|
|
write_skill "$F" demo beta "version: 1.0.9" "x"
|
|
commit "$F"
|
|
OUT="$(run_check "$F" || true)"
|
|
if grep -q "alpha: 1.0.0 -> 1.0.0" <<< "$OUT" && grep -q "beta: 1.0.9 -> 1.0.9" <<< "$OUT" \
|
|
&& grep -q "bump PATCH at minimum" <<< "$OUT"; then
|
|
pass "both offenders and the fix are reported"
|
|
else
|
|
fail "not every offender reported: $OUT"
|
|
fi
|
|
|
|
echo ""
|
|
echo "--- 13. PRE_COMMIT_TO_REF respected over HEAD ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
|
|
BAD="$(cd "$F" && git rev-parse HEAD)"
|
|
(cd "$F" && git checkout -q -b clean main)
|
|
expect_fail "unbumped TO_REF fails while HEAD is clean" "alpha: 1\.0\.0 -> 1\.0\.0" "$F" "$BAD"
|
|
(cd "$F" && git checkout -q "$BAD")
|
|
expect_pass "clean TO_REF passes while HEAD is unbumped" "$F" "$(cd "$F" && git rev-parse main)"
|
|
|
|
echo ""
|
|
echo "--- 14. all-zeros delete sha no-ops ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
|
|
expect_pass "40-zero sha exits 0" "$F" "0000000000000000000000000000000000000000"
|
|
expect_pass "64-zero sha exits 0" "$F" "$(printf '0%.0s' {1..64})"
|
|
|
|
echo ""
|
|
echo "--- 15. bin plugin covered ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" bin tool 'version: "1.0.0"'
|
|
(cd "$F" && git checkout -q main); commit "$F" "add bin skill"
|
|
(cd "$F" && git checkout -q feature && git merge -q main)
|
|
write_skill "$F" bin tool 'version: "1.0.0"' "changed"; commit "$F"
|
|
expect_fail "bin skill change without bump fails" "plugins/bin/\.apm/skills/tool: 1\.0\.0 -> 1\.0\.0" "$F"
|
|
|
|
echo ""
|
|
echo "--- 16. multi-digit semver compare ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo beta "version: 1.0.10" "x"; commit "$F"
|
|
expect_pass "1.0.10 > 1.0.9 passes" "$F"
|
|
|
|
echo ""
|
|
echo "--- 17. baseline is the merge-base, not main's tip ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha 'version: "1.0.1"' "branch change"; commit "$F"
|
|
(cd "$F" && git checkout -q main)
|
|
write_skill "$F" demo alpha 'version: "1.0.5"' "main moved on"; commit "$F"
|
|
(cd "$F" && git checkout -q feature)
|
|
expect_pass "bump over the merge-base passes even though main is ahead" "$F"
|
|
|
|
echo ""
|
|
echo "--- 18. origin/main preferred over local main ---"
|
|
F="$(make_fixture)"
|
|
write_skill "$F" demo alpha 'version: "1.0.1"' "x"; commit "$F"
|
|
# A stale local main pointing at the feature tip would make the diff empty;
|
|
# origin/main at the original commit must win and still see the change.
|
|
(cd "$F" && git update-ref refs/remotes/origin/main main && git branch -f main feature)
|
|
write_skill "$F" demo alpha 'version: "1.0.1"' "y"; commit "$F"
|
|
if OUT="$(run_check "$F")" && [[ -z "$OUT" ]]; then
|
|
pass "origin/main used as baseline (1.0.0 -> 1.0.1 counted as a bump)"
|
|
else
|
|
fail "unexpected output: $OUT"
|
|
fi
|
|
(cd "$F" && git update-ref refs/remotes/origin/main feature~1)
|
|
expect_fail "origin/main at the bumped commit flags the further unbumped change" \
|
|
"merge-base with origin/main" "$F"
|
|
|
|
echo ""
|
|
echo "--- 19. no main ref fails closed ---"
|
|
F="$(mktemp -d)"; CLEANUP_DIRS+=("$F")
|
|
(cd "$F" && git init -q -b trunk && git config user.email t@t.t && git config user.name t)
|
|
write_skill "$F" demo alpha 'version: "1.0.0"'; commit "$F"
|
|
expect_fail "missing main fails with a clear message" "neither origin/main nor main resolves" "$F"
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|