Why: .pre-commit-hooks.yaml and its release-tag gate served external consumers that do not exist. No repo on the Gitea instance pins these hooks, and the README names apm as the only supported install path. The mechanism was also already failing: skill-size-check.sh changed after v2.0.1 with no tag cut, and the gate cannot fire through Gitea's merge button. (Simplification audit finding 36.) Implementation Notes: - Delete .pre-commit-hooks.yaml, scripts/check-release-needed.sh, tests/test-check-release-needed.sh and tests/test-vale-hooks-consumer.sh, and remove the check-release-needed pre-push hook. The repo: local skill-size-check and vale-audit-prefilter-* hooks are unchanged. - ADR-0014 is amended, not retired: its runtime decision to bundle Vale inside factory-audit stands. The amendment keeps the entry[0]-only constraint (LESSONS.md:101,105) in case the export returns. ADR-0025 gets a pointer. - test-vale-wrap.sh: drop case 33 (the cross-manifest drift check) and case 28's hook-scope half, which read the published manifest. Case 32 now also requires each hook to select every tracked file of its class, which keeps case 33's one-plugin-narrowing guard, with a mutation test. - test-skill-size-check.sh and test-adr0020-contract.sh now assert the hook contract and verbose: true on .pre-commit-config.yaml only. - gates.md: pre-push count goes from 9 to 8 authored hooks (11 to 10 reported), and the Release table, the External consumers section and the two-manifest scope table are removed. README and script/test comments no longer describe the export as live. The resolver comment is edited identically in both copies. - The v1.0.0/v2.0.0/v2.0.1 tags are left in place; they are inert. ADR: 0014 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
573 lines
24 KiB
Bash
Executable File
573 lines
24 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 docs/spec/gates.md, Tests).
|
||
|
||
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)
|
||
echo "unrelated" > "$F/README.md"; commit "$F"
|
||
CLEAN="$(cd "$F" && git rev-parse HEAD)"
|
||
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" "$CLEAN"
|
||
|
||
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. pushed version must also exceed 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_fail "bump over the merge-base fails when main's tip is higher" \
|
||
"alpha: 1\.0\.5 -> 1\.0\.1 \(not above main tip\)" "$F"
|
||
OUT="$(run_check "$F" || true)"
|
||
if grep -q "not above merge-base" <<< "$OUT"; then
|
||
fail "merge-base reported as failed although 1.0.1 > 1.0.0: $OUT"
|
||
else
|
||
pass "only the baseline actually failed is named"
|
||
fi
|
||
write_skill "$F" demo alpha 'version: "1.0.6"' "branch change 2"; commit "$F"
|
||
expect_pass "bump above both the merge-base and main's tip passes" "$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"
|
||
expect_fail "the report names alpha and the versions read from origin/main's merge-base" \
|
||
"demo/\.apm/skills/alpha: 1\.0\.1 -> 1\.0\.1 \(not above merge-base\)" "$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 "--- 20. non-ASCII paths are not hidden by core.quotePath ---"
|
||
F="$(make_fixture)"
|
||
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
|
||
echo "ref" > "$F/plugins/demo/.apm/skills/alpha/references/résumé.md"; commit "$F"
|
||
expect_fail "non-ASCII file under references/ without bump fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q main)
|
||
write_skill "$F" demo "café" 'version: "2.0.0"'; commit "$F" "add café"
|
||
(cd "$F" && git checkout -q feature && git merge -q main)
|
||
write_skill "$F" demo "café" 'version: "2.0.0"' "changed"; commit "$F"
|
||
expect_fail "non-ASCII skill dir without bump fails" "skills/café: 2\.0\.0 -> 2\.0\.0" "$F"
|
||
write_skill "$F" demo "café" 'version: "2.0.1"' "changed again"; commit "$F"
|
||
expect_pass "non-ASCII skill dir with bump passes" "$F"
|
||
|
||
echo ""
|
||
echo "--- 21. local main fallback: pushed commit already in main fails closed ---"
|
||
F="$(make_fixture)"
|
||
MAIN_SHA="$(cd "$F" && git rev-parse main)"
|
||
expect_fail "pushing main's sha without origin/main fails" \
|
||
"origin/main does not resolve.*already contained in local main" "$F" "$MAIN_SHA"
|
||
(cd "$F" && git checkout -q main)
|
||
expect_fail "HEAD on main without origin/main fails" \
|
||
"origin/main does not resolve and HEAD is already contained in local main" "$F"
|
||
|
||
echo ""
|
||
echo "--- 22. pushing main itself with origin/main present ---"
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q main && git update-ref refs/remotes/origin/main main)
|
||
expect_pass "main equal to origin/main passes (nothing changed vs main)" "$F"
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "x"; commit "$F"
|
||
expect_pass "main ahead of origin/main with a bump passes" "$F"
|
||
write_skill "$F" demo beta "version: 1.0.9" "x"; commit "$F"
|
||
expect_fail "main ahead of origin/main without a bump fails" "beta: 1\.0\.9 -> 1\.0\.9" "$F"
|
||
(cd "$F" && git update-ref refs/remotes/origin/main main)
|
||
expect_pass "already-merged content (merge-base == pushed) passes against origin/main" "$F" \
|
||
"$(cd "$F" && git rev-parse main~1)"
|
||
|
||
echo ""
|
||
echo "--- 23. version shape is ASCII-only, bounded, and has no leading zeros ---"
|
||
for v in 'version: "1.0.1"' 'version: "1.0.1"' 'version: "1.0.9999999999"' \
|
||
'version: "99999999999999999999.0.0"' 'version: "1.0.08"' 'version: "01.0.1"' \
|
||
'version: "1.00.1"'; do
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha "$v" "new body"; commit "$F"
|
||
OUT="$(run_check "$F" || true)"
|
||
if grep -q "alpha: metadata\.version missing or not" <<< "$OUT" \
|
||
&& ! grep -qiE "integer|syntax error|value too great" <<< "$OUT"; then
|
||
pass "'$v' is rejected as invalid without a bash arithmetic error"
|
||
else
|
||
fail "'$v' not cleanly rejected: $OUT"
|
||
fi
|
||
done
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha 'version: "1.0.999999999"' "new body"; commit "$F"
|
||
expect_pass "nine-digit part is accepted and compared" "$F"
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha 'version: "1.0.10"' "new body"; commit "$F"
|
||
expect_pass "a zero inside a part (1.0.10) is not a leading zero" "$F"
|
||
|
||
echo ""
|
||
echo "--- 24. python3 / PyYAML failures are never reported as a missing version ---"
|
||
REAL_PYTHON="$(command -v python3)"
|
||
SHIM_ROOT="$(mktemp -d)"; CLEANUP_DIRS+=("$SHIM_ROOT")
|
||
mkdir -p "$SHIM_ROOT/shadow" "$SHIM_ROOT/noyaml" "$SHIM_ROOT/crash"
|
||
printf 'raise ImportError("PyYAML deliberately unavailable in this fixture")\n' \
|
||
> "$SHIM_ROOT/shadow/yaml.py"
|
||
cat > "$SHIM_ROOT/noyaml/python3" <<EOF
|
||
#!/bin/sh
|
||
PYTHONPATH="$SHIM_ROOT/shadow\${PYTHONPATH:+:\$PYTHONPATH}" exec "$REAL_PYTHON" "\$@"
|
||
EOF
|
||
# Passes the up-front import probe, crashes on the real read.
|
||
cat > "$SHIM_ROOT/crash/python3" <<EOF
|
||
#!/bin/sh
|
||
[ "\$1" = "-c" ] && [ "\$2" = "import yaml" ] && exec "$REAL_PYTHON" "\$@"
|
||
echo "Traceback: simulated interpreter failure" >&2
|
||
exit 1
|
||
EOF
|
||
chmod +x "$SHIM_ROOT/noyaml/python3" "$SHIM_ROOT/crash/python3"
|
||
if PATH="$SHIM_ROOT/noyaml:$PATH" python3 -c 'import yaml' 2>/dev/null; then
|
||
fail "fixture check: the no-PyYAML shim still imports yaml — the next assertion would be vacuous"
|
||
else
|
||
pass "fixture check: the no-PyYAML shim makes 'import yaml' fail"
|
||
fi
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "new body"; commit "$F"
|
||
OUT="$(cd "$F" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF PRE_COMMIT_REMOTE_BRANCH \
|
||
&& PATH="$SHIM_ROOT/noyaml:$PATH" bash "$SCRIPT" 2>&1)" && RC=0 || RC=$?
|
||
if [[ $RC -ne 0 ]] && grep -q "PyYAML is required" <<< "$OUT" && grep -q "Fix: python3 -m pip install PyYAML" <<< "$OUT" \
|
||
&& ! grep -q "Traceback" <<< "$OUT"; then
|
||
pass "missing PyYAML fails with FAIL + Fix, no traceback"
|
||
else
|
||
fail "missing PyYAML not reported cleanly (rc=$RC): $OUT"
|
||
fi
|
||
OUT="$(cd "$F" && unset PRE_COMMIT_FROM_REF PRE_COMMIT_TO_REF PRE_COMMIT_REMOTE_BRANCH \
|
||
&& PATH="$SHIM_ROOT/crash:$PATH" bash "$SCRIPT" 2>&1)" && RC=0 || RC=$?
|
||
if [[ $RC -ne 0 ]] && grep -q "could not read metadata.version" <<< "$OUT" \
|
||
&& ! grep -q "missing or not" <<< "$OUT"; then
|
||
pass "python3 crash during the read is a read failure, not a missing version"
|
||
else
|
||
fail "python3 crash misreported (rc=$RC): $OUT"
|
||
fi
|
||
|
||
echo ""
|
||
echo "--- 25. SKILL.md deleted but skill dir kept ---"
|
||
F="$(make_fixture)"
|
||
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
|
||
echo "ref" > "$F/plugins/demo/.apm/skills/alpha/references/x.md"
|
||
rm "$F/plugins/demo/.apm/skills/alpha/SKILL.md"; commit "$F"
|
||
expect_fail "missing SKILL.md is reported as such" "alpha: SKILL\.md missing at HEAD \(baseline: 1\.0\.0\)" "$F"
|
||
|
||
echo ""
|
||
echo "--- 26. baseline without a valid version accepts any valid version ---"
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q main)
|
||
write_skill "$F" demo legacy ""; commit "$F" "add legacy skill"
|
||
(cd "$F" && git checkout -q feature && git merge -q main)
|
||
write_skill "$F" demo legacy 'version: "0.0.1"' "changed"; commit "$F"
|
||
expect_pass "invalid baseline + valid current passes" "$F"
|
||
|
||
echo ""
|
||
echo "--- 27. no merge-base (unrelated histories) fails closed ---"
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q --orphan unrelated && git rm -rq --cached . && rm -rf plugins)
|
||
write_skill "$F" demo alpha 'version: "1.0.0"' "orphan"; commit "$F" "orphan root"
|
||
expect_fail "unrelated history fails at the merge-base check" "no merge-base between main and HEAD" "$F"
|
||
|
||
echo ""
|
||
echo "--- 28. annotated tag objects as PRE_COMMIT_TO_REF ---"
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
|
||
(cd "$F" && git tag -a v9 -m "tag" && git checkout -q main)
|
||
TAG_OBJ="$(cd "$F" && git rev-parse v9)"
|
||
if [[ "$(cd "$F" && git cat-file -t "$TAG_OBJ")" == "tag" ]]; then
|
||
pass "fixture check: TO_REF is a tag object, not a commit"
|
||
else
|
||
fail "fixture check: v9 is not an annotated tag object"
|
||
fi
|
||
expect_fail "unbumped change behind an annotated tag fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F" "$TAG_OBJ"
|
||
# Peeling is what makes the local-main fallback's "pushed commit is the
|
||
# merge-base" test see through a tag: compared unpeeled, the tag's own sha
|
||
# never equals the merge-base and the empty diff would pass.
|
||
(cd "$F" && git tag -a on-main -m "tag" main)
|
||
expect_fail "a tag on local main's commit fails closed like the commit itself" \
|
||
"origin/main does not resolve and [0-9a-f]+ is already contained in local main" \
|
||
"$F" "$(cd "$F" && git rev-parse on-main)"
|
||
(cd "$F" && git tag -a tree-tag -m "tag" "main^{tree}")
|
||
expect_fail "a tag on a tree fails closed" "pushed ref [0-9a-f]+ does not resolve to a commit" \
|
||
"$F" "$(cd "$F" && git rev-parse tree-tag)"
|
||
|
||
echo ""
|
||
echo "--- 29. CRLF frontmatter is parsed ---"
|
||
# crlf <repo> <skill>: rewrite that skill's SKILL.md with CRLF line endings.
|
||
crlf() {
|
||
local f="$1/plugins/demo/.apm/skills/$2/SKILL.md"
|
||
sed 's/$/\r/' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
|
||
}
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git config core.autocrlf false && git checkout -q main)
|
||
crlf "$F" alpha; commit "$F" "alpha to CRLF"
|
||
(cd "$F" && git checkout -q feature && git merge -q main)
|
||
write_skill "$F" demo alpha 'version: "1.0.0"' "crlf body"; crlf "$F" alpha; commit "$F"
|
||
if grep -q $'\r' "$F/plugins/demo/.apm/skills/alpha/SKILL.md"; then
|
||
pass "fixture check: SKILL.md carries CRLF"
|
||
else
|
||
fail "fixture check: SKILL.md has no CRLF"
|
||
fi
|
||
expect_fail "unbumped CRLF skill reports both parsed versions" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "crlf body 2"; crlf "$F" alpha; commit "$F"
|
||
expect_pass "bumped CRLF skill passes" "$F"
|
||
|
||
echo ""
|
||
echo "--- 30. identical bump already merged to main fails ---"
|
||
# Branches A and B both bump alpha 1.0.0 -> 1.0.1 with different content. The
|
||
# bumps do not conflict at merge, so without the tip rule main would ship two
|
||
# changes under one version.
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q -b branch-a main)
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "change A"; commit "$F"
|
||
(cd "$F" && git checkout -q main && git merge -q --no-ff -m "merge A" branch-a \
|
||
&& git update-ref refs/remotes/origin/main main && git checkout -q feature)
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "change B"; commit "$F"
|
||
expect_fail "B's 1.0.1 fails against A's 1.0.1 on origin/main" \
|
||
"alpha: 1\.0\.1 -> 1\.0\.1 \(not above origin/main tip\)" "$F"
|
||
write_skill "$F" demo alpha 'version: "1.0.2"' "change B 2"; commit "$F"
|
||
expect_pass "B at 1.0.2 passes" "$F"
|
||
|
||
echo ""
|
||
echo "--- 31. skill deleted on main's tip: only the merge-base rule applies ---"
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q main)
|
||
rm -rf "$F/plugins/demo/.apm/skills/alpha"; commit "$F" "drop alpha on main"
|
||
(cd "$F" && git checkout -q feature)
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "branch change"; commit "$F"
|
||
expect_pass "bump over the merge-base passes when main's tip lacks the skill" "$F"
|
||
(cd "$F" && git checkout -q -b unbumped main~1)
|
||
write_skill "$F" demo alpha 'version: "1.0.0"' "unbumped"; commit "$F"
|
||
expect_fail "unbumped change still fails against the merge-base" \
|
||
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
|
||
|
||
echo ""
|
||
echo "--- 32. UTF-8 BOM before the frontmatter is parsed ---"
|
||
# bom <repo> <skill>: prefix that skill's SKILL.md with a UTF-8 byte-order mark.
|
||
bom() {
|
||
local f="$1/plugins/demo/.apm/skills/$2/SKILL.md"
|
||
{ printf '\xef\xbb\xbf'; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
|
||
}
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha 'version: "1.0.0"' "bom body"; bom "$F" alpha; commit "$F"
|
||
if [[ "$(head -c 3 "$F/plugins/demo/.apm/skills/alpha/SKILL.md" | od -An -tx1 | tr -d ' ')" == "efbbbf" ]]; then
|
||
pass "fixture check: SKILL.md starts with a BOM"
|
||
else
|
||
fail "fixture check: SKILL.md has no BOM"
|
||
fi
|
||
expect_fail "unbumped BOM skill reports its parsed version, not a missing one" \
|
||
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "bom body 2"; bom "$F" alpha; commit "$F"
|
||
expect_pass "bumped BOM skill passes" "$F"
|
||
|
||
echo ""
|
||
echo "--- 33. a file moved from one skill to another flags both ---"
|
||
# --no-renames: with rename detection, --name-only lists only the new path and
|
||
# alpha would lose a file without anyone noticing.
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q main)
|
||
mkdir -p "$F/plugins/demo/.apm/skills/alpha/references"
|
||
printf 'ref line %s\n' 1 2 3 4 5 > "$F/plugins/demo/.apm/skills/alpha/references/x.md"
|
||
commit "$F" "alpha reference"
|
||
(cd "$F" && git checkout -q feature && git merge -q main)
|
||
mkdir -p "$F/plugins/demo/.apm/skills/beta/references"
|
||
(cd "$F" && git mv plugins/demo/.apm/skills/alpha/references/x.md plugins/demo/.apm/skills/beta/references/x.md)
|
||
write_skill "$F" demo beta "version: 1.0.10"; commit "$F"
|
||
expect_fail "alpha is flagged although only beta was bumped" \
|
||
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
|
||
|
||
echo ""
|
||
echo "--- 34. comparison is ordered major first ---"
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git checkout -q main)
|
||
write_skill "$F" demo alpha 'version: "2.0.0"'; commit "$F" "alpha 2.0.0"
|
||
(cd "$F" && git checkout -q feature && git merge -q main)
|
||
write_skill "$F" demo alpha 'version: "1.9.0"' "new body"; commit "$F"
|
||
expect_fail "2.0.0 -> 1.9.0 fails although minor rose" "alpha: 2\.0\.0 -> 1\.9\.0" "$F"
|
||
|
||
echo ""
|
||
echo "--- 35. unresolvable PRE_COMMIT_TO_REF fails closed ---"
|
||
F="$(make_fixture)"
|
||
expect_fail "a sha absent from the repo fails" \
|
||
"pushed ref 1234567890abcdef1234567890abcdef12345678 does not resolve to a commit" \
|
||
"$F" "1234567890abcdef1234567890abcdef12345678"
|
||
|
||
echo ""
|
||
echo "--- 36. a SKILL.md git cannot read fails closed ---"
|
||
# drop_blob <repo> <rev:path>: delete that blob's loose object, as a corrupt or
|
||
# partial clone would lack it. The tree still names the file.
|
||
drop_blob() {
|
||
local sha
|
||
sha="$(cd "$1" && git rev-parse "$2")"
|
||
rm -f "$1/.git/objects/${sha:0:2}/${sha:2}"
|
||
}
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "new body"; commit "$F"
|
||
drop_blob "$F" "HEAD:plugins/demo/.apm/skills/alpha/SKILL.md"
|
||
OUT="$(run_check "$F")" && RC=0 || RC=$?
|
||
if [[ $RC -ne 0 ]] && grep -q "could not read metadata.version from [0-9a-f]*:plugins/demo/.apm/skills/alpha/SKILL.md" <<< "$OUT" \
|
||
&& grep -q "fatal: bad object" <<< "$OUT" && ! grep -qE "missing or not|SKILL\.md missing" <<< "$OUT"; then
|
||
pass "an unreadable pushed SKILL.md is a read failure carrying git's error"
|
||
else
|
||
fail "unreadable pushed SKILL.md misreported (rc=$RC): $OUT"
|
||
fi
|
||
F="$(make_fixture)"
|
||
write_skill "$F" demo alpha 'version: "1.0.1"' "new body"; commit "$F"
|
||
drop_blob "$F" "main:plugins/demo/.apm/skills/alpha/SKILL.md"
|
||
OUT="$(run_check "$F")" && RC=0 || RC=$?
|
||
if [[ $RC -ne 0 ]] && grep -q "could not read metadata.version" <<< "$OUT"; then
|
||
pass "an unreadable merge-base SKILL.md fails closed instead of exempting the skill"
|
||
else
|
||
fail "unreadable merge-base SKILL.md not caught (rc=$RC): $OUT"
|
||
fi
|
||
|
||
echo ""
|
||
echo "--- 37. a mode-only change counts as a change ---"
|
||
F="$(make_fixture)"
|
||
(cd "$F" && git config core.fileMode true)
|
||
chmod +x "$F/plugins/demo/.apm/skills/alpha/SKILL.md"; commit "$F"
|
||
if [[ "$(cd "$F" && git diff --summary main HEAD)" == *"mode change 100644 => 100755"* ]]; then
|
||
pass "fixture check: the commit changes only the file mode"
|
||
else
|
||
fail "fixture check: no mode change recorded"
|
||
fi
|
||
expect_fail "chmod +x without a bump fails" "alpha: 1\.0\.0 -> 1\.0\.0" "$F"
|
||
|
||
echo ""
|
||
echo "--- 38. skill directory replaced by a symlink ---"
|
||
# Current behaviour, pinned: the path is no longer a tree at the pushed commit,
|
||
# so the skill is exempt as deleted. apm drops symlinks under .apm/ (ADR-0017),
|
||
# so readers do lose the skill.
|
||
F="$(make_fixture)"
|
||
rm -rf "$F/plugins/demo/.apm/skills/alpha"
|
||
ln -s beta "$F/plugins/demo/.apm/skills/alpha"; commit "$F"
|
||
if [[ "$(cd "$F" && git ls-tree HEAD plugins/demo/.apm/skills/alpha)" == 120000* ]]; then
|
||
pass "fixture check: alpha is committed as a symlink"
|
||
else
|
||
fail "fixture check: alpha is not a symlink in the commit"
|
||
fi
|
||
expect_pass "a skill replaced by a symlink is exempt as deleted" "$F"
|
||
|
||
echo ""
|
||
echo "Results: $PASS passed, $FAIL failed"
|
||
[[ $FAIL -eq 0 ]]
|