fix(lint): restore release-gate coverage of bundled Vale assets
The gate derived release-relevant paths from the dirname of each entry's --config target. Dropping --config from .pre-commit-hooks.yaml left that loop dead, silently removing both assets/vale/ trees from coverage — a Vale rule change could land on main without demanding a release tag, leaving consumers pinned to an old rev: with stale rules. Coverage now derives from tokens[0] instead: double-dirname for the .. normalization, guarded on the tree existing and on the bundle root not resolving to "." so skill-size-check.sh cannot invent a bogus path. The --config branch is removed rather than kept as dead code. Since pre-commit rewrites only entry[0], no argument in any entry can ever name a file this repo ships, so that shape is broken by design. Known gap: deleting a hook's entire assets/ tree is not flagged, as the candidate path stops existing. Deletions within a surviving tree are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
This commit is contained in:
@@ -34,19 +34,29 @@ fi
|
|||||||
# instead of hand-maintaining a parallel list — the manifest is the single
|
# instead of hand-maintaining a parallel list — the manifest is the single
|
||||||
# source of truth for what external consumers actually pull at a pinned rev,
|
# source of truth for what external consumers actually pull at a pinned rev,
|
||||||
# so a hook added/removed/renamed there can't silently drift out of sync here.
|
# so a hook added/removed/renamed there can't silently drift out of sync here.
|
||||||
# Each entry is "<script> [--config <path>] [...]"; the script itself and the
|
# Everything is derived from tokens[0], the hook's script: pre-commit prefixes
|
||||||
# directory holding any --config target (vale-wrap.sh needs its .vale.ini's
|
# only entry[0] with the hook-repo clone path, so any later token that looks
|
||||||
# sibling styles/ tree, not just the ini file) are release-relevant.
|
# like a path resolves against the *consuming* repo and can never name a file
|
||||||
|
# this repo ships. A hook's bundled data therefore has to be self-located
|
||||||
|
# relative to the script — vale-wrap.sh reads its own
|
||||||
|
# <script-dir>/../assets/vale/.vale.ini plus the sibling styles/ tree — which
|
||||||
|
# makes <script-dir>/../assets release-relevant alongside the script itself.
|
||||||
|
# The ../ is normalised by stripping a path component rather than with
|
||||||
|
# `realpath -m`, which is a GNU-only extension. Two guards keep the derivation
|
||||||
|
# from inventing paths: a bundle root of "." is skipped, because a script in a
|
||||||
|
# top-level directory (scripts/skill-size-check.sh) would derive the repo's own
|
||||||
|
# shared assets/, which no hook owns and whose churn must not demand a release;
|
||||||
|
# and the directory is added only when it exists, since a hook that bundles
|
||||||
|
# nothing must not contribute a pathspec matching nothing.
|
||||||
RELEASE_PATHS=("$HOOKS_MANIFEST")
|
RELEASE_PATHS=("$HOOKS_MANIFEST")
|
||||||
while IFS= read -r entry; do
|
while IFS= read -r entry; do
|
||||||
read -ra tokens <<< "$entry"
|
read -ra tokens <<< "$entry"
|
||||||
[[ ${#tokens[@]} -eq 0 ]] && continue
|
[[ ${#tokens[@]} -eq 0 ]] && continue
|
||||||
RELEASE_PATHS+=("${tokens[0]}")
|
RELEASE_PATHS+=("${tokens[0]}")
|
||||||
for ((i = 1; i < ${#tokens[@]}; i++)); do
|
bundle_root="$(dirname "$(dirname "${tokens[0]}")")"
|
||||||
if [[ "${tokens[i]}" == "--config" && -n "${tokens[i + 1]:-}" ]]; then
|
if [[ "$bundle_root" != "." && -d "$bundle_root/assets" ]]; then
|
||||||
RELEASE_PATHS+=("$(dirname "${tokens[i + 1]}")")
|
RELEASE_PATHS+=("$bundle_root/assets")
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
done < <(sed -n 's/^[[:space:]]*entry:[[:space:]]*//p' "$HOOKS_MANIFEST")
|
done < <(sed -n 's/^[[:space:]]*entry:[[:space:]]*//p' "$HOOKS_MANIFEST")
|
||||||
|
|
||||||
# Only vX.Y.Z release tags count as a baseline — an incidental checkpoint or
|
# Only vX.Y.Z release tags count as a baseline — an incidental checkpoint or
|
||||||
|
|||||||
@@ -9,32 +9,45 @@ FAIL=0
|
|||||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
# Helper: write a minimal .pre-commit-hooks.yaml exposing one script entry and
|
# Both entry shapes the real .pre-commit-hooks.yaml ships: a bare script with no
|
||||||
# one --config-bearing entry, so RELEASE_PATHS (derived from the manifest, not
|
# bundled data, and a bare script whose sibling assets/ tree it self-locates at
|
||||||
# hand-maintained) has both shapes to parse.
|
# 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() {
|
write_manifest() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
mkdir -p "$dir/vale"
|
cat > "$dir/.pre-commit-hooks.yaml" <<EOF
|
||||||
cat > "$dir/.pre-commit-hooks.yaml" <<'EOF'
|
|
||||||
- id: fake-size-check
|
- id: fake-size-check
|
||||||
entry: scripts/skill-size-check.sh
|
entry: scripts/skill-size-check.sh
|
||||||
language: script
|
language: script
|
||||||
- id: fake-vale-check
|
- id: fake-vale-check
|
||||||
entry: scripts/vale-wrap.sh --config vale/.vale.ini
|
entry: $HOOK_DIR/scripts/vale-wrap.sh
|
||||||
language: script
|
language: script
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: a fixture repo with a manifest and its two referenced release-relevant
|
# Helper: writes the files both manifest entries expose — the two hook scripts
|
||||||
# paths, committed and tagged v1.0.0.
|
# plus the bundled Vale config and style rule the second one self-locates.
|
||||||
|
write_release_paths() {
|
||||||
|
local dir="$1"
|
||||||
|
mkdir -p "$dir/scripts" "$dir/$HOOK_DIR/scripts" "$dir/$HOOK_DIR/assets/vale/styles/Kyberforge"
|
||||||
|
echo "v1" > "$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() {
|
make_tagged_fixture() {
|
||||||
local dir
|
local dir
|
||||||
dir="$(mktemp -d)"
|
dir="$(mktemp -d)"
|
||||||
(cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t)
|
(cd "$dir" && git init -q && git config user.email t@t.t && git config user.name t)
|
||||||
write_manifest "$dir"
|
write_manifest "$dir"
|
||||||
mkdir -p "$dir/scripts"
|
write_release_paths "$dir"
|
||||||
echo "v1" > "$dir/scripts/skill-size-check.sh"
|
|
||||||
echo "cfg" > "$dir/vale/.vale.ini"
|
|
||||||
(cd "$dir" && git add -A && git commit -q -m "initial" && git tag v1.0.0)
|
(cd "$dir" && git add -A && git commit -q -m "initial" && git tag v1.0.0)
|
||||||
echo "$dir"
|
echo "$dir"
|
||||||
}
|
}
|
||||||
@@ -65,9 +78,7 @@ echo "--- exits 1 when targeting main and no tag exists ---"
|
|||||||
FIXTURE2="$(mktemp -d)"; track "$FIXTURE2"
|
FIXTURE2="$(mktemp -d)"; track "$FIXTURE2"
|
||||||
(cd "$FIXTURE2" && git init -q && git config user.email t@t.t && git config user.name t)
|
(cd "$FIXTURE2" && git init -q && git config user.email t@t.t && git config user.name t)
|
||||||
write_manifest "$FIXTURE2"
|
write_manifest "$FIXTURE2"
|
||||||
mkdir -p "$FIXTURE2/scripts"
|
write_release_paths "$FIXTURE2"
|
||||||
echo "v1" > "$FIXTURE2/scripts/skill-size-check.sh"
|
|
||||||
echo "cfg" > "$FIXTURE2/vale/.vale.ini"
|
|
||||||
(cd "$FIXTURE2" && git add -A && git commit -q -m "initial")
|
(cd "$FIXTURE2" && git add -A && git commit -q -m "initial")
|
||||||
if run_check "$FIXTURE2" "refs/heads/main" > /dev/null; then
|
if run_check "$FIXTURE2" "refs/heads/main" > /dev/null; then
|
||||||
fail "exited 0 when targeting main with no tag — expected exit 1"
|
fail "exited 0 when targeting main with no tag — expected exit 1"
|
||||||
@@ -116,10 +127,10 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "--- exits 1 when a release-relevant path was deleted since the tag, not just modified ---"
|
echo "--- exits 1 when a release-relevant path was deleted since the tag, not just modified ---"
|
||||||
FIXTURE6="$(make_tagged_fixture)"; track "$FIXTURE6"
|
FIXTURE6="$(make_tagged_fixture)"; track "$FIXTURE6"
|
||||||
rm -rf "$FIXTURE6/vale"
|
rm -f "$FIXTURE6/$HOOK_DIR/assets/vale/.vale.ini"
|
||||||
(cd "$FIXTURE6" && git add -A && git commit -q -m "delete the vale config dir")
|
(cd "$FIXTURE6" && git add -A && git commit -q -m "delete the bundled vale config")
|
||||||
OUT6=$(run_check "$FIXTURE6" "refs/heads/main" || true)
|
OUT6=$(run_check "$FIXTURE6" "refs/heads/main" || true)
|
||||||
if echo "$OUT6" | grep -q "vale/.vale.ini"; then
|
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"
|
pass "flags a deleted release-relevant path instead of silently dropping it from the diff"
|
||||||
else
|
else
|
||||||
fail "did not flag deletion of a release-relevant path since the tag"
|
fail "did not flag deletion of a release-relevant path since the tag"
|
||||||
@@ -166,6 +177,40 @@ else
|
|||||||
fail "flagged a file that no .pre-commit-hooks.yaml entry actually exposes"
|
fail "flagged a file that no .pre-commit-hooks.yaml entry actually exposes"
|
||||||
fi
|
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
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[[ $FAIL -eq 0 ]]
|
[[ $FAIL -eq 0 ]]
|
||||||
|
|||||||
Reference in New Issue
Block a user