fix(gates): read leading-whitespace frontmatter in check-skill-version-bump

read_version required --- at byte 0 while skill-size-check accepts
leading blank lines, so a file one gate passed the other reported as
unversioned, and an unversioned merge-base side let an unbumped change
through. Match FRONTMATTER_RE, add case 39, and describe the main-tip
check and fail-closed cases in the hook entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 15:27:17 +00:00
parent 3a9d257225
commit 614a0d5efa
3 changed files with 33 additions and 7 deletions

View File

@@ -189,16 +189,19 @@ repos:
- id: check-skill-version-bump - id: check-skill-version-bump
name: Check changed skills bump metadata.version name: Check changed skills bump metadata.version
description: On every push, fail if a skill directory changed (tests/ excluded) since the merge-base with main without its SKILL.md metadata.version rising (ADR-0022) description: On every push, fail if a skill directory changed (tests/ excluded) since the merge-base with main without its SKILL.md metadata.version rising above both that merge-base's and main's tip's (ADR-0022)
entry: bash scripts/check-skill-version-bump.sh entry: bash scripts/check-skill-version-bump.sh
language: system language: system
stages: [pre-push] stages: [pre-push]
pass_filenames: false pass_filenames: false
always_run: true always_run: true
# Baseline is the merge-base with origin/main (falling back to main), # "Changed" is measured from the merge-base with origin/main (falling
# not the remote branch tip: readers install from main. Fails closed # back to main), not the remote branch tip: readers install from main.
# when no main ref resolves. Merges through Gitea's merge button run no # The version must also beat main's tip, so two branches making the same
# local hook, so they bypass this. # bump cannot both land. Fails closed when no main ref resolves, when
# there is no merge-base, or when only local main resolves and already
# contains the pushed commit. Merges through Gitea's merge button run no
# local hook, so they bypass this. See docs/spec/gates.md.
- id: validate-marketplace - id: validate-marketplace
name: Validate marketplace manifest name: Validate marketplace manifest

View File

@@ -175,12 +175,14 @@ done < "$CHANGED_FILE"
# python3 crashing, PyYAML failing to import — is a non-zero exit with no OK / # python3 crashing, PyYAML failing to import — is a non-zero exit with no OK /
# INVALID line, which the caller reports as a read failure, never as a missing # INVALID line, which the caller reports as a read failure, never as a missing
# version. Bytes are decoded explicitly so the caller's locale cannot turn a # version. Bytes are decoded explicitly so the caller's locale cannot turn a
# non-ASCII SKILL.md into a crash; `\s*` before each `\n` absorbs CRLF. # non-ASCII SKILL.md into a crash; `[ \t\r]*` before each `\n` absorbs CRLF.
# Whitespace before the opening `---` is accepted, matching FRONTMATTER_RE in
# factory-audit's lib-boundary-resolver.sh, which skill-size-check applies.
read_version() { read_version() {
python3 -c ' python3 -c '
import re, sys, yaml import re, sys, yaml
text = sys.stdin.buffer.read().decode("utf-8-sig", errors="replace") text = sys.stdin.buffer.read().decode("utf-8-sig", errors="replace")
m = re.match(r"---[ \t\r]*\n(.*?)\n---[ \t\r]*(\n|\Z)", text, re.S) m = re.match(r"[ \t\r\n]*---[ \t\r]*\n(.*?)\n---[ \t\r]*(\n|\Z)", text, re.S)
data = None data = None
if m: if m:
try: try:

View File

@@ -567,6 +567,27 @@ else
fi fi
expect_pass "a skill replaced by a symlink is exempt as deleted" "$F" expect_pass "a skill replaced by a symlink is exempt as deleted" "$F"
echo ""
echo "--- 39. leading blank lines before the frontmatter are parsed ---"
# skill-size-check's FRONTMATTER_RE accepts whitespace before the opening
# `---`; this gate must read the same shape. Otherwise the pushed side reports
# "missing" for a file the commit hook accepted, and a baseline shaped this way
# reads as "no valid version", which lets any version pass.
# blank_lead <repo> <skill>: prefix that skill's SKILL.md with blank lines.
blank_lead() {
local f="$1/plugins/demo/.apm/skills/$2/SKILL.md"
{ printf '\n \t\n'; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f"
}
F="$(make_fixture)"
(cd "$F" && git checkout -q main)
blank_lead "$F" alpha; commit "$F" "alpha leading blank"
(cd "$F" && git checkout -q feature && git merge -q main)
write_skill "$F" demo alpha 'version: "1.0.0"' "lead body"; blank_lead "$F" alpha; commit "$F"
expect_fail "unbumped skill with leading blank lines is held to its baseline version" \
"alpha: 1\.0\.0 -> 1\.0\.0 \(not above merge-base\)" "$F"
write_skill "$F" demo alpha 'version: "1.0.1"' "lead body 2"; blank_lead "$F" alpha; commit "$F"
expect_pass "bumped skill with leading blank lines passes" "$F"
echo "" echo ""
echo "Results: $PASS passed, $FAIL failed" echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]] [[ $FAIL -eq 0 ]]