ci: gate the executables.allow key against kyberforge's version
apm approves a package's hooks and bin by an exact dictionary lookup on a composed `name#version` key (apm_cli/security/executables.py, is_package_approved). There is no wildcard and no version-less form, so bumping plugins/kyberforge/apm.yml without editing root apm.yml's `kyberforge#<version>` key errors nowhere: the entry stops matching, the SessionStart hook stops deploying, and the install goes quietly stale. ADR-0019 already named that as a live failure mode, mitigated only by a comment in the executables block. This repo gates generated-content drift, marketplace mirror drift and vale style drift deterministically, and a silent-staleness failure is worse than any of them — a comment does not survive the release that breaks it. check-executables-allow-sync.sh parses the version out of the plugin manifest and asserts the matching key exists in the root manifest. It uses PyYAML where importable and falls back to a two-shape scan otherwise, so a missing pip package cannot become the thing that blocks every push; the test asserts both readers agree. 23 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
This commit is contained in:
@@ -89,6 +89,15 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
always_run: true
|
||||||
|
|
||||||
|
- id: check-executables-allow-sync
|
||||||
|
name: Check executables allow key sync
|
||||||
|
description: Verify root apm.yml's executables.allow key names kyberforge's actual version -- apm matches that key by exact "<package>#<version>" lookup, so a version bump on one side alone silently stops deploying kyberforge's hooks/ and bin/ and lets the apm install go stale (see ADR-0019)
|
||||||
|
entry: bash scripts/check-executables-allow-sync.sh
|
||||||
|
language: system
|
||||||
|
stages: [pre-push]
|
||||||
|
pass_filenames: false
|
||||||
|
always_run: true
|
||||||
|
|
||||||
- id: apm-marketplace-check
|
- id: apm-marketplace-check
|
||||||
name: apm marketplace check
|
name: apm marketplace check
|
||||||
description: Validate every marketplace.packages[] entry resolves, including network reachability of remote refs -- catches stale/unreachable remote package references that check-manifests.sh deliberately skips (local-source checks only)
|
description: Validate every marketplace.packages[] entry resolves, including network reachability of remote refs -- catches stale/unreachable remote package references that check-manifests.sh deliberately skips (local-source checks only)
|
||||||
|
|||||||
231
scripts/check-executables-allow-sync.sh
Executable file
231
scripts/check-executables-allow-sync.sh
Executable file
@@ -0,0 +1,231 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Fails the push when root apm.yml's executables.allow key stops naming
|
||||||
|
# kyberforge's actual version.
|
||||||
|
#
|
||||||
|
# apm gates a package's hooks/ and bin/ on an EXACT dict lookup of
|
||||||
|
# "<package>#<version>" in executables.allow (apm_cli/security/executables.py,
|
||||||
|
# `allow_executables.get(package_key)`) — there is no wildcard and no
|
||||||
|
# version-less form. So bumping plugins/kyberforge/apm.yml's `version:` without
|
||||||
|
# bumping the key in root apm.yml does not error anywhere: the entry simply
|
||||||
|
# stops matching, kyberforge's SessionStart hook stops deploying, and the apm
|
||||||
|
# install goes quietly stale — the exact failure ADR-0019 records as live and
|
||||||
|
# mitigates only with a comment. Nothing else in the pre-push gate compares the
|
||||||
|
# two files, which is why this exists.
|
||||||
|
#
|
||||||
|
# Run from repo root or pass REPO_ROOT as arg.
|
||||||
|
|
||||||
|
REPO_ROOT="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
||||||
|
# A nonexistent REPO_ROOT fails loudly rather than falling through to the
|
||||||
|
# "no kyberforge plugin here" no-op below: that no-op is for a repo that
|
||||||
|
# legitimately does not ship the plugin, not for a typo'd or stale path, and
|
||||||
|
# exit 0 would read as "checked, in sync" when nothing was compared at all.
|
||||||
|
if [[ ! -d "$REPO_ROOT" ]]; then
|
||||||
|
echo "FAIL: executables-allow sync check: REPO_ROOT '$REPO_ROOT' is not a directory." >&2
|
||||||
|
echo " Fix: run this from the repo root, or pass a real repo path as the first argument." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
|
||||||
|
|
||||||
|
PLUGIN_DIR="$REPO_ROOT/plugins/kyberforge"
|
||||||
|
PLUGIN_MANIFEST="$PLUGIN_DIR/apm.yml"
|
||||||
|
ROOT_MANIFEST="$REPO_ROOT/apm.yml"
|
||||||
|
|
||||||
|
# Only a repo with no kyberforge plugin at all is a legitimate no-op — there is
|
||||||
|
# no version to pin and no hook to deploy.
|
||||||
|
if [[ ! -d "$PLUGIN_DIR" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$PLUGIN_MANIFEST" ]]; then
|
||||||
|
echo "FAIL: plugins/kyberforge/ exists but has no apm.yml, so its version cannot be read." >&2
|
||||||
|
echo " Why: this gate compares that version against root apm.yml's executables.allow key;" >&2
|
||||||
|
echo " without the manifest it would exit 0 having compared nothing." >&2
|
||||||
|
echo " Fix: restore plugins/kyberforge/apm.yml, or remove plugins/kyberforge/ entirely." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$ROOT_MANIFEST" ]]; then
|
||||||
|
echo "FAIL: root apm.yml is missing, so kyberforge's executables allow key cannot be verified." >&2
|
||||||
|
echo " Why: apm reads executables.allow from the consuming package's manifest; with no root" >&2
|
||||||
|
echo " manifest nothing grants kyberforge's hooks/ and bin/ permission to deploy." >&2
|
||||||
|
echo " Fix: restore apm.yml at the repo root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Manifest facts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Both readers emit the same line protocol, so the rest of the script does not
|
||||||
|
# care which ran:
|
||||||
|
#
|
||||||
|
# v<TAB><kyberforge version> exactly once
|
||||||
|
# a<TAB>present|absent exactly once — is executables.allow a mapping?
|
||||||
|
# k<TAB><allow key> zero or more
|
||||||
|
#
|
||||||
|
# python3 + PyYAML is preferred where importable, because it is a real parser.
|
||||||
|
# It is deliberately NOT a hard requirement: no other hook in this repo's
|
||||||
|
# pre-push gate needs PyYAML, and making a version-pin check the one thing that
|
||||||
|
# can block every push on a missing pip package is a worse failure than reading
|
||||||
|
# two known shapes by hand. The fallback below is not a YAML parser — it
|
||||||
|
# recognises exactly the two shapes these manifests use and nothing else.
|
||||||
|
|
||||||
|
read_facts_python() {
|
||||||
|
python3 - "$ROOT_MANIFEST" "$PLUGIN_MANIFEST" << 'PY'
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
with open(path) as fh:
|
||||||
|
data = yaml.safe_load(fh)
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
root, plugin = load(sys.argv[1]), load(sys.argv[2])
|
||||||
|
|
||||||
|
version = plugin.get("version")
|
||||||
|
# A 2-component version parses as a YAML float and would render back as e.g.
|
||||||
|
# "1.5" from 1.50 — a mismatch invented by the reader rather than found in the
|
||||||
|
# files. Refuse instead of guessing; semver keys here are always strings.
|
||||||
|
if version is not None and not isinstance(version, str):
|
||||||
|
sys.stderr.write(
|
||||||
|
"kyberforge's version: is not a string (%r) — quote it so the "
|
||||||
|
"executables.allow key can be compared verbatim.\n" % (version,)
|
||||||
|
)
|
||||||
|
raise SystemExit(2)
|
||||||
|
print("v\t%s" % ("" if version is None else version))
|
||||||
|
|
||||||
|
executables = root.get("executables")
|
||||||
|
allow = executables.get("allow") if isinstance(executables, dict) else None
|
||||||
|
if isinstance(allow, dict):
|
||||||
|
print("a\tpresent")
|
||||||
|
for key in allow:
|
||||||
|
print("k\t%s" % key)
|
||||||
|
else:
|
||||||
|
print("a\tabsent")
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strips one layer of matching quotes plus surrounding whitespace from a scalar.
|
||||||
|
unquote() {
|
||||||
|
local value="$1"
|
||||||
|
read -r value <<< "$value"
|
||||||
|
case "$value" in
|
||||||
|
\"*\") value="${value#\"}"; value="${value%\"}" ;;
|
||||||
|
\'*\') value="${value#\'}"; value="${value%\'}" ;;
|
||||||
|
esac
|
||||||
|
printf '%s\n' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
read_facts_bash() {
|
||||||
|
local line version="" allow_state="absent" in_executables=0 in_allow=0 key
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
[[ "$line" == version:* ]] || continue
|
||||||
|
version="$(unquote "${line#version:}")"
|
||||||
|
break
|
||||||
|
done < "$PLUGIN_MANIFEST"
|
||||||
|
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
# Blank and full-line comments carry no structure at any depth.
|
||||||
|
case "$line" in
|
||||||
|
'' | '#'*) continue ;;
|
||||||
|
esac
|
||||||
|
# A top-level key (column 0) closes whatever block was open. Checked before
|
||||||
|
# anything else so `marketplace:` after `executables:` cannot leak keys in.
|
||||||
|
if [[ "$line" != [[:space:]]* ]]; then
|
||||||
|
if [[ "$line" == executables:* ]]; then
|
||||||
|
in_executables=1
|
||||||
|
else
|
||||||
|
in_executables=0
|
||||||
|
fi
|
||||||
|
in_allow=0
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[[ $in_executables -eq 1 ]] || continue
|
||||||
|
# 2-space indent: a key directly under executables:. `allow:` opens the
|
||||||
|
# mapping this gate reads; any sibling key closes it.
|
||||||
|
if [[ "$line" =~ ^\ \ [^[:space:]#] ]]; then
|
||||||
|
if [[ "$line" =~ ^\ \ allow: ]]; then
|
||||||
|
in_allow=1
|
||||||
|
allow_state="present"
|
||||||
|
else
|
||||||
|
in_allow=0
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# 4-space indent while inside allow: — an allow key. Keys contain '#' by
|
||||||
|
# construction ("kyberforge#1.5.0"), so a trailing-comment strip would eat
|
||||||
|
# them; there is none, and inline comments are not used on these lines.
|
||||||
|
if [[ $in_allow -eq 1 && "$line" =~ ^\ \ \ \ ([^[:space:]#][^:]*): ]]; then
|
||||||
|
key="$(unquote "${BASH_REMATCH[1]}")"
|
||||||
|
printf 'k\t%s\n' "$key"
|
||||||
|
fi
|
||||||
|
done < "$ROOT_MANIFEST"
|
||||||
|
|
||||||
|
printf 'v\t%s\n' "$version"
|
||||||
|
printf 'a\t%s\n' "$allow_state"
|
||||||
|
}
|
||||||
|
|
||||||
|
if command -v python3 > /dev/null 2>&1 && python3 -c 'import yaml' > /dev/null 2>&1; then
|
||||||
|
READER="python3 + PyYAML"
|
||||||
|
if ! FACTS="$(read_facts_python)"; then
|
||||||
|
echo "FAIL: could not read apm.yml / plugins/kyberforge/apm.yml (see the parser error above)." >&2
|
||||||
|
echo " Why: this gate compares kyberforge's version against root apm.yml's executables.allow" >&2
|
||||||
|
echo " key; an unreadable manifest means the comparison did not happen." >&2
|
||||||
|
echo " Fix: make both manifests valid YAML, then re-run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
READER="shape-scan fallback (PyYAML unavailable)"
|
||||||
|
FACTS="$(read_facts_bash)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$(printf '%s\n' "$FACTS" | sed -n 's/^v\t//p')"
|
||||||
|
ALLOW_STATE="$(printf '%s\n' "$FACTS" | sed -n 's/^a\t//p')"
|
||||||
|
ALLOW_KEYS="$(printf '%s\n' "$FACTS" | sed -n 's/^k\t//p')"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" ]]; then
|
||||||
|
echo "FAIL: plugins/kyberforge/apm.yml declares no version:, so no allow key can be checked against it." >&2
|
||||||
|
echo " Why: apm's executables.allow lookup is keyed on '<package>#<version>' exactly; with no" >&2
|
||||||
|
echo " version there is nothing for root apm.yml's key to stay in sync with." >&2
|
||||||
|
echo " Fix: give plugins/kyberforge/apm.yml a top-level version: (read by $READER)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXPECTED_KEY="kyberforge#$VERSION"
|
||||||
|
|
||||||
|
if [[ "$ALLOW_STATE" != "present" ]]; then
|
||||||
|
echo "FAIL: root apm.yml has no executables.allow mapping, but plugins/kyberforge is version $VERSION." >&2
|
||||||
|
echo " Why: without an allow entry apm refuses to deploy kyberforge's hooks/ and bin/, so the" >&2
|
||||||
|
echo " SessionStart hook that keeps this install level with the remote never runs and the" >&2
|
||||||
|
echo " deployed skills go stale silently (ADR-0019)." >&2
|
||||||
|
echo " Fix: add to root apm.yml:" >&2
|
||||||
|
echo " executables:" >&2
|
||||||
|
echo " allow:" >&2
|
||||||
|
echo " $EXPECTED_KEY:" >&2
|
||||||
|
echo " hooks: true" >&2
|
||||||
|
echo " bin: true" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s\n' "$ALLOW_KEYS" | grep -qxF "$EXPECTED_KEY"; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
STALE_KEYS="$(printf '%s\n' "$ALLOW_KEYS" | grep '^kyberforge#' || true)"
|
||||||
|
|
||||||
|
echo "FAIL: root apm.yml's executables.allow has no '$EXPECTED_KEY' key, but that is kyberforge's version." >&2
|
||||||
|
if [[ -n "$STALE_KEYS" ]]; then
|
||||||
|
echo " Found instead:" >&2
|
||||||
|
printf '%s\n' "$STALE_KEYS" | sed 's/^/ /' >&2
|
||||||
|
fi
|
||||||
|
echo " Why: apm matches this key by exact dict lookup — there is no wildcard and no version-less" >&2
|
||||||
|
echo " form — so a key naming any other version silently stops granting kyberforge's hooks/" >&2
|
||||||
|
echo " and bin/. The SessionStart hook then stops deploying and the apm install goes stale" >&2
|
||||||
|
echo " with no error anywhere (ADR-0019, 'The allow key is version-pinned')." >&2
|
||||||
|
echo " Fix: bump the key in root apm.yml to '$EXPECTED_KEY:' — the version bump in" >&2
|
||||||
|
echo " plugins/kyberforge/apm.yml is not complete without it." >&2
|
||||||
|
exit 1
|
||||||
243
tests/test-check-executables-allow-sync.sh
Normal file
243
tests/test-check-executables-allow-sync.sh
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tests for scripts/check-executables-allow-sync.sh — the pre-push gate that
|
||||||
|
# keeps root apm.yml's executables.allow key level with kyberforge's version.
|
||||||
|
#
|
||||||
|
# Fixtures are two-file skeletons (root apm.yml + plugins/kyberforge/apm.yml)
|
||||||
|
# rather than copies of the real repo: the gate reads exactly those two files,
|
||||||
|
# and a hand-built fixture is the only way to construct the drift it exists to
|
||||||
|
# catch without editing the real manifests.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
SCRIPT="$REPO_ROOT/scripts/check-executables-allow-sync.sh"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
# Same `exit 77` (automake convention; run-tests.sh renders it as SKIPPED) guard
|
||||||
|
# the vale suites use. The script itself runs fine without python3 — it falls
|
||||||
|
# back to a shape scan — but this suite asserts BOTH readers agree, and the
|
||||||
|
# PyYAML path cannot be exercised at all on a machine without it. Reporting
|
||||||
|
# those cases as failures would say "a regression landed" when the truth is
|
||||||
|
# "this machine is missing a dev dependency".
|
||||||
|
command -v python3 > /dev/null 2>&1 || { echo "SKIP: python3 is required to exercise the PyYAML reader"; exit 77; }
|
||||||
|
python3 -c 'import yaml' > /dev/null 2>&1 || { echo "SKIP: PyYAML is required to exercise the PyYAML reader"; exit 77; }
|
||||||
|
|
||||||
|
FIXTURES=()
|
||||||
|
cleanup() { [[ ${#FIXTURES[@]} -eq 0 ]] || rm -rf "${FIXTURES[@]}"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# make_fixture <plugin-version-line> <root-executables-block>
|
||||||
|
# The executables block is passed verbatim (may be empty) so a fixture can omit
|
||||||
|
# it entirely, which is one of the failure modes under test.
|
||||||
|
make_fixture() {
|
||||||
|
local version_line="$1" executables_block="$2" dir
|
||||||
|
dir="$(mktemp -d)"
|
||||||
|
FIXTURES+=("$dir")
|
||||||
|
mkdir -p "$dir/plugins/kyberforge"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "name: kyberforge"
|
||||||
|
echo "$version_line"
|
||||||
|
echo "description: fixture"
|
||||||
|
} > "$dir/plugins/kyberforge/apm.yml"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "name: ai-development"
|
||||||
|
echo "version: 0.0.1"
|
||||||
|
echo "dependencies:"
|
||||||
|
echo " apm:"
|
||||||
|
echo " - name: kyberforge"
|
||||||
|
[[ -n "$executables_block" ]] && printf '%s\n' "$executables_block"
|
||||||
|
# A top-level key after the block: the fallback reader must stop collecting
|
||||||
|
# allow keys here rather than reading on into the next section.
|
||||||
|
echo "marketplace:"
|
||||||
|
echo " owner:"
|
||||||
|
echo " name: fixture"
|
||||||
|
} > "$dir/apm.yml"
|
||||||
|
|
||||||
|
printf '%s\n' "$dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
MATCHING_BLOCK='executables:
|
||||||
|
allow:
|
||||||
|
kyberforge#1.5.0:
|
||||||
|
hooks: true
|
||||||
|
bin: true'
|
||||||
|
|
||||||
|
STALE_BLOCK='executables:
|
||||||
|
allow:
|
||||||
|
kyberforge#1.4.0:
|
||||||
|
hooks: true
|
||||||
|
bin: true'
|
||||||
|
|
||||||
|
OTHER_PACKAGE_BLOCK='executables:
|
||||||
|
allow:
|
||||||
|
git#1.0.0:
|
||||||
|
hooks: true'
|
||||||
|
|
||||||
|
# run_gate <fixture> — echoes combined output, sets GATE_RC.
|
||||||
|
GATE_RC=0
|
||||||
|
run_gate() {
|
||||||
|
GATE_RC=0
|
||||||
|
bash "$SCRIPT" "$1" > /dev/null 2>&1 || GATE_RC=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "--- the real repo passes ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# The gate's whole value is that it is green on a correct tree and red on drift;
|
||||||
|
# a version bump landing in only one of the two real manifests must show up here.
|
||||||
|
run_gate "$REPO_ROOT"
|
||||||
|
[[ $GATE_RC -eq 0 ]] && pass "current repo state passes" \
|
||||||
|
|| fail "current repo state should pass — the gate said: $(bash "$SCRIPT" "$REPO_ROOT" 2>&1 | head -3)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- matching version ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "$MATCHING_BLOCK")"
|
||||||
|
run_gate "$F"
|
||||||
|
[[ $GATE_RC -eq 0 ]] && pass "exits 0 when the allow key names the plugin's version" \
|
||||||
|
|| fail "should pass when key and version agree, got rc=$GATE_RC"
|
||||||
|
|
||||||
|
F="$(make_fixture 'version: "1.5.0"' "$MATCHING_BLOCK")"
|
||||||
|
run_gate "$F"
|
||||||
|
[[ $GATE_RC -eq 0 ]] && pass "exits 0 when the version is quoted" \
|
||||||
|
|| fail "a quoted version must compare the same as an unquoted one, got rc=$GATE_RC"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- mismatched version ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "$STALE_BLOCK")"
|
||||||
|
run_gate "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fails when the allow key names a different version" \
|
||||||
|
|| fail "a stale allow key must fail the push"
|
||||||
|
OUT="$(bash "$SCRIPT" "$F" 2>&1 || true)"
|
||||||
|
grep -q "kyberforge#1.5.0" <<< "$OUT" && pass "names the key that should be there" \
|
||||||
|
|| fail "the failure must state the expected key"
|
||||||
|
grep -q "kyberforge#1.4.0" <<< "$OUT" && pass "names the stale key it found instead" \
|
||||||
|
|| fail "the failure must quote back the stale key"
|
||||||
|
grep -q "Why:" <<< "$OUT" && grep -q "Fix:" <<< "$OUT" && pass "uses the FAIL/Why/Fix message block" \
|
||||||
|
|| fail "message must carry Why: and Fix: lines"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- missing executables.allow ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "")"
|
||||||
|
run_gate "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fails when there is no executables block at all" \
|
||||||
|
|| fail "a missing executables.allow must fail — apm deploys no hooks without it"
|
||||||
|
OUT="$(bash "$SCRIPT" "$F" 2>&1 || true)"
|
||||||
|
grep -q "executables:" <<< "$OUT" && grep -q "kyberforge#1.5.0" <<< "$OUT" \
|
||||||
|
&& pass "shows the block to add" || fail "the failure must show the block to add"
|
||||||
|
|
||||||
|
# An `executables:` key that is not a mapping is the same hole as no key at all.
|
||||||
|
F="$(make_fixture "version: 1.5.0" "executables:")"
|
||||||
|
run_gate "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fails when executables: exists but allow: does not" \
|
||||||
|
|| fail "an empty executables: block grants nothing and must fail"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- allow present, kyberforge key missing ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "$OTHER_PACKAGE_BLOCK")"
|
||||||
|
run_gate "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fails when allow: exists but names no kyberforge key" \
|
||||||
|
|| fail "an allow block covering only other packages must still fail"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- degenerate inputs fail loudly rather than passing silently ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
run_gate "$REPO_ROOT/definitely-not-a-directory"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fails on a nonexistent REPO_ROOT" \
|
||||||
|
|| fail "a bad path must not exit 0 — that reads as 'checked, in sync'"
|
||||||
|
|
||||||
|
# No kyberforge plugin at all is the one legitimate no-op: nothing to pin.
|
||||||
|
NO_PLUGIN="$(mktemp -d)"; FIXTURES+=("$NO_PLUGIN")
|
||||||
|
echo "name: someone-else" > "$NO_PLUGIN/apm.yml"
|
||||||
|
run_gate "$NO_PLUGIN"
|
||||||
|
[[ $GATE_RC -eq 0 ]] && pass "no-ops in a repo with no kyberforge plugin" \
|
||||||
|
|| fail "a repo without plugins/kyberforge/ has nothing to check"
|
||||||
|
|
||||||
|
# ...but a kyberforge directory with no manifest is drift, not a no-op.
|
||||||
|
mkdir -p "$NO_PLUGIN/plugins/kyberforge"
|
||||||
|
run_gate "$NO_PLUGIN"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fails when plugins/kyberforge/ has no apm.yml" \
|
||||||
|
|| fail "a plugin dir with no manifest must not silently pass"
|
||||||
|
|
||||||
|
F="$(make_fixture "description: no version here" "$MATCHING_BLOCK")"
|
||||||
|
run_gate "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fails when the plugin manifest declares no version" \
|
||||||
|
|| fail "no version means nothing to compare — must fail, not pass"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- the fallback reader agrees with the PyYAML reader ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# PyYAML is deliberately not a hard dependency of this gate (no other pre-push
|
||||||
|
# hook needs it), so the shape-scan fallback carries the same verdicts. Masking
|
||||||
|
# is done with a python3 stub whose `import yaml` fails, which is the exact
|
||||||
|
# condition on a machine that has python3 without PyYAML.
|
||||||
|
NO_YAML_BIN="$(mktemp -d)"; FIXTURES+=("$NO_YAML_BIN")
|
||||||
|
printf '#!/usr/bin/env bash\nexit 1\n' > "$NO_YAML_BIN/python3"
|
||||||
|
chmod +x "$NO_YAML_BIN/python3"
|
||||||
|
|
||||||
|
run_fallback() {
|
||||||
|
GATE_RC=0
|
||||||
|
PATH="$NO_YAML_BIN:$PATH" bash "$SCRIPT" "$1" > /dev/null 2>&1 || GATE_RC=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "$MATCHING_BLOCK")"
|
||||||
|
run_fallback "$F"
|
||||||
|
[[ $GATE_RC -eq 0 ]] && pass "fallback passes a matching fixture" || fail "fallback should pass when in sync"
|
||||||
|
|
||||||
|
F="$(make_fixture 'version: "1.5.0"' "$MATCHING_BLOCK")"
|
||||||
|
run_fallback "$F"
|
||||||
|
[[ $GATE_RC -eq 0 ]] && pass "fallback strips quotes from the version" || fail "fallback mishandled a quoted version"
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "$STALE_BLOCK")"
|
||||||
|
run_fallback "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fallback fails a stale key" || fail "fallback missed a stale key"
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "")"
|
||||||
|
run_fallback "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fallback fails a missing executables block" || fail "fallback missed a missing block"
|
||||||
|
|
||||||
|
F="$(make_fixture "version: 1.5.0" "$OTHER_PACKAGE_BLOCK")"
|
||||||
|
run_fallback "$F"
|
||||||
|
[[ $GATE_RC -ne 0 ]] && pass "fallback fails when no kyberforge key is present" || fail "fallback missed an absent key"
|
||||||
|
|
||||||
|
run_fallback "$REPO_ROOT"
|
||||||
|
[[ $GATE_RC -eq 0 ]] && pass "fallback passes the real repo" || fail "fallback disagrees with PyYAML on the real repo"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- wired into the pre-push gate ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# A gate nobody runs is not a gate; this is the only assertion that the script
|
||||||
|
# is actually reachable from `git push`.
|
||||||
|
CONFIG="$REPO_ROOT/.pre-commit-config.yaml"
|
||||||
|
grep -q "id: check-executables-allow-sync" "$CONFIG" \
|
||||||
|
&& pass ".pre-commit-config.yaml declares the hook" || fail "hook is not declared in .pre-commit-config.yaml"
|
||||||
|
grep -q "scripts/check-executables-allow-sync.sh" "$CONFIG" \
|
||||||
|
&& pass ".pre-commit-config.yaml points at the script" || fail "hook does not reference the script path"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
|
[[ $FAIL -eq 0 ]]
|
||||||
Reference in New Issue
Block a user