Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/scripts/detect-contract-changes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
# Exercised by test-detect-contract-changes.sh, which runs on every PR (gate-self-test).
set -euo pipefail

# The two decision messages below say "paths matching the <label> gate", not
# "contract-path". They said contract-path until the x402-node gate became the first
# non-contract caller of this script, at which point the log of a gate whose only job is
# to report whether it applies started naming a file class that had not changed. Every
# other message here already used ${GATE_LABEL}; these two were the exception.
GATE_LABEL="${1:?usage: detect-contract-changes.sh <gate-label> <extended-regex>}"
PATH_REGEX="${2:?usage: detect-contract-changes.sh <gate-label> <extended-regex>}"

Expand Down Expand Up @@ -82,7 +87,7 @@ fi
# pipeline's status under `pipefail`, which silently inverts the answer on a large diff
# (defect 2). A herestring is not a pipeline, so there is nothing for pipefail to promote.
if grep -qE "$PATH_REGEX" <<< "$CHANGED"; then
apply "Contract-path changes detected — running the full ${GATE_LABEL} gate."
apply "Paths matching the ${GATE_LABEL} gate changed — running it in full."
fi

skip "No contract-path changes — ${GATE_LABEL} cannot be affected by this PR."
skip "No paths matching the ${GATE_LABEL} gate changed — it cannot be affected by this PR."
46 changes: 46 additions & 0 deletions .github/scripts/test-detect-contract-changes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,52 @@ HEAD_WF="$(git rev-parse HEAD)"
GATE_REGEX="$X402_REGEX" \
expect_gate "workflow change → apply for x402" true EVENT=pull_request BASE_SHA="$BASE" HEAD_SHA="$HEAD_WF"

# A gate must not name a file class it did not look at. Every decision message already
# interpolates ${GATE_LABEL}, so "does it mention the label" would NOT have caught the
# defect this pins: the old lines said "Contract-path changes detected — running the full
# ${GATE_LABEL} gate", label and all, while the gate had looked at packages/. What was
# wrong was the hardcoded noun, so that is what is asserted.
#
# Without this the wording had no gate at all: replacing an entire decision message left
# the suite at 19/19, because it asserts the ANSWER and not the prose -- correct by design,
# and the reason a false sentence could ride in behind a green run.
#
# The script's own filename contains "contract" and appears in usage errors, so it is
# removed before looking.
expect_no_foreign_noun() {
local desc="$1"; shift
local out log
out="$WORK/gh_output"; : > "$out"
log="$(env "$@" GITHUB_OUTPUT="$out" bash "$UNDER_TEST" "x402-node" '^packages/x402-facilitator-node/' 2>&1 || true)"
# A live-instrument check BEFORE the absence check. `grep -q` on an empty string finds
# nothing, so "the message does not say contract" and "there is no message" are the same
# reading -- and the second one passes. Verified: emptying every decision message left
# all four rows green and the suite at 23/23, i.e. a script that said nothing scored
# perfectly on a guard about what it says.
#
# The label is what makes the instrument live: it must appear, which cannot be satisfied
# by silence, and cannot be satisfied by a message about some other gate either.
if ! printf '%s' "$log" | grep -q 'x402-node'; then
echo " FAIL $desc — no decision message naming the gate; nothing to check."
echo " stdout+stderr was: [$log]"
fail=$((fail + 1)); return
fi
local stripped; stripped="$(printf '%s' "$log" | sed 's/detect-contract-changes\.sh//g')"
if printf '%s' "$stripped" | grep -qi contract; then
echo " FAIL $desc — a gate labelled x402-node named 'contract' in its own decision:"
printf '%s\n' "$log" | sed 's/^/ /'
fail=$((fail + 1)); return
fi
echo " ok $desc → names the gate, not a file class"
pass=$((pass + 1))
}

echo "— a gate never names a file class it did not look at"
expect_no_foreign_noun "apply (paths matched)" EVENT=pull_request BASE_SHA="$BASE" HEAD_SHA="$HEAD_X402"
expect_no_foreign_noun "skip (nothing matched)" EVENT=pull_request BASE_SHA="$BASE" HEAD_SHA="$HEAD_DOCS"
expect_no_foreign_noun "apply (non-PR event)" EVENT=push
expect_no_foreign_noun "apply (undecidable)" EVENT=pull_request BASE_SHA="$GONE" HEAD_SHA="$HEAD_DOCS"

echo "— defect 1: the diff cannot be computed (round-10 answered 'skip' here)"
expect_gate "base unreachable → apply" true EVENT=pull_request BASE_SHA="$GONE" HEAD_SHA="$HEAD_DOCS"
expect_gate "head unreachable → apply" true EVENT=pull_request BASE_SHA="$BASE" HEAD_SHA="$GONE"
Expand Down
Loading