fix(gates): hold skill versions above main's tip as well as the merge-base
Why: two branches that both bump a skill 1.0.0 -> 1.0.1 with different content merge without a conflict, and each passed the gate against its own merge-base, so main could ship two changes under one version. Implementation Notes: - check-skill-version-bump requires the pushed version to exceed both the merge-base and the main tip; failures name the baseline they missed. - Presence is read from the tree, so a blob missing from a partial clone is a read failure instead of a silently exempt "new" skill. - A leading UTF-8 BOM no longer reads as a missing version. - Version parts reject leading zeros in all three validators (check-skill-version-bump, skill-size-check, factory-audit). - New tests cover equal bumps, moved files, major/minor ordering, bad refs, unreadable blobs, mode-only changes, symlinks and tag peeling. Impact: ADR-0022 amended (reverses "not main's current tip"); gates.md updated to match, including pre-commit 4.6.1's exact ref selection. ADR: 0022 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -235,7 +235,9 @@ else:
|
||||
# --- ADR-0022: metadata.version is mandatory -------------------------------
|
||||
# FAIL, not SUGGESTION, and the tier is set by the gate rather than by taste.
|
||||
# `.pre-commit-config.yaml`'s `skill-size-check` hook REJECTS a SKILL.md with
|
||||
# no `metadata.version`, and rejects a value that is not three-part semver.
|
||||
# no `metadata.version`, and rejects a value that is not three-part semver:
|
||||
# ASCII digits only, no leading zero (semver 2.0.0 item 2), at most nine digits
|
||||
# per part (bash arithmetic in check-skill-version-bump.sh), whole-value match.
|
||||
# skill-author's Step 4 says to run this audit and "resolve every FAIL", so any
|
||||
# tier below FAIL lets that step report done on a skill the commit gate then
|
||||
# refuses — the same audit-disagrees-with-the-gate failure the MAX_LINES note
|
||||
@@ -246,8 +248,10 @@ else:
|
||||
# The rule is DUPLICATED from that hook for the same cache-isolation reason as
|
||||
# every other constant here — an installed plugin's scripts cannot read the
|
||||
# repo-root config. Keep the two in step: this check must accept exactly what
|
||||
# the hook accepts.
|
||||
SEMVER_RE = re.compile(r'^\d+\.\d+\.\d+$')
|
||||
# the hook accepts. Used with fullmatch(), never match() with ^...$ anchors:
|
||||
# `$` also matches before a trailing newline, and `\d` also matches non-ASCII
|
||||
# Unicode digits — both of which the hook rejects.
|
||||
SEMVER_RE = re.compile(r'(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})')
|
||||
|
||||
try:
|
||||
fm_data = yaml.safe_load(fm)
|
||||
@@ -268,13 +272,15 @@ else:
|
||||
# spelling is exactly the two-part value the hook rejects — coercing and
|
||||
# then matching keeps this check and the hook agreeing on that case.
|
||||
version_text = version_value if isinstance(version_value, str) else str(version_value)
|
||||
version_text = version_text.strip()
|
||||
if SEMVER_RE.match(version_text):
|
||||
# Same normalisation as the hook: surrounding whitespace, then quotes.
|
||||
version_text = version_text.strip().strip('\'"')
|
||||
if SEMVER_RE.fullmatch(version_text):
|
||||
ok(f"metadata.version present: '{version_text}' (ADR-0022)")
|
||||
else:
|
||||
fail(f"metadata.version '{version_text}' is not three-part semver — the "
|
||||
f"skill-size-check pre-commit hook rejects it. Use MAJOR.MINOR.PATCH, "
|
||||
f"e.g. \"1.0.0\"")
|
||||
f"skill-size-check pre-commit hook rejects it. Use MAJOR.MINOR.PATCH "
|
||||
f"with ASCII digits, no leading zeros and at most nine digits per "
|
||||
f"part, e.g. \"1.0.0\"")
|
||||
|
||||
# SKILL.md size ceilings (agentskills.io skill-authoring.md: 500 lines,
|
||||
# ~5,000 tokens). Both constants are DUPLICATED from the repo-root pre-commit
|
||||
|
||||
@@ -377,7 +377,7 @@ SH
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0022 — metadata.version is mandatory. FAIL tier, matching the
|
||||
# skill-frontmatter pre-commit hook: an audit that graded this lower would
|
||||
# skill-size-check pre-commit hook: an audit that graded this lower would
|
||||
# report ready-to-ship on a file the commit gate rejects.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -437,6 +437,62 @@ PY
|
||||
assert_output --partial "metadata.version present: '0.1.3'"
|
||||
}
|
||||
|
||||
@test "ADR-0022: a leading zero in the patch part FAILs (1.0.08)" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_valid_skill "$skill"
|
||||
python3 - "$skill/SKILL.md" <<'PY'
|
||||
import sys
|
||||
p = sys.argv[1]
|
||||
s = open(p).read().replace(' version: "1.0.0"\n', ' version: "1.0.08"\n')
|
||||
open(p, 'w').write(s)
|
||||
PY
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "three-part semver"
|
||||
}
|
||||
|
||||
@test "ADR-0022: a leading zero in the major part FAILs (01.0.1)" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_valid_skill "$skill"
|
||||
python3 - "$skill/SKILL.md" <<'PY'
|
||||
import sys
|
||||
p = sys.argv[1]
|
||||
s = open(p).read().replace(' version: "1.0.0"\n', ' version: "01.0.1"\n')
|
||||
open(p, 'w').write(s)
|
||||
PY
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "three-part semver"
|
||||
}
|
||||
|
||||
@test "ADR-0022: a multi-digit part with no leading zero passes (1.0.10)" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_valid_skill "$skill"
|
||||
python3 - "$skill/SKILL.md" <<'PY'
|
||||
import sys
|
||||
p = sys.argv[1]
|
||||
s = open(p).read().replace(' version: "1.0.0"\n', ' version: "1.0.10"\n')
|
||||
open(p, 'w').write(s)
|
||||
PY
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "metadata.version present: '1.0.10'"
|
||||
}
|
||||
|
||||
@test "ADR-0022: a zero major part passes (0.1.0)" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_valid_skill "$skill"
|
||||
python3 - "$skill/SKILL.md" <<'PY'
|
||||
import sys
|
||||
p = sys.argv[1]
|
||||
s = open(p).read().replace(' version: "1.0.0"\n', ' version: "0.1.0"\n')
|
||||
open(p, 'w').write(s)
|
||||
PY
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "metadata.version present: '0.1.0'"
|
||||
}
|
||||
|
||||
@test "fails when name contains consecutive hyphens" {
|
||||
local skill="$TMPDIR/my--skill"
|
||||
make_valid_skill "$skill"
|
||||
|
||||
Reference in New Issue
Block a user