#!/usr/bin/env bash # The delta-inventory guard (#200, enforcing #197 D3's standing constraint). # # Forge-specific behaviour lives in the files docs/UPSTREAM-SYNC.md names, # and nowhere else. # # WHY. This tree carries upstream's content plus a forge delta, forever — the # sync is recurring and upstream is read-only (#197 D3). What makes that # tractable is not luck: the 0.6.0 sync cost 18 conflict hunks across 10 files # because the delta sits in six files upstream never touches. Scatter # `forge_detect` into a seventh and every future sync pays for it, in a place # nobody chose. # # So this fails on the PR that scatters it, rather than on the sync after next # — the same reason changelog-monotonic guards a property no single tree can # be asked about. # # IT RUNS OFFLINE, AND IT DOES NOT PASS BY ABSENCE. Tests must not need the # network, and a guard that goes green when it cannot see its input is the # blind-sweep shape this repo keeps writing issues about. So the inventory is # read from the document and the tree is scanned for forge markers — both local # — and a missing or empty `.upstream-ref` is a REFUSAL, not a skip. set -u ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=test/harness.sh . "$ROOT/test/harness.sh" DOC="$ROOT/docs/UPSTREAM-SYNC.md" REF="$ROOT/.upstream-ref" # --- the recorded upstream ref ------------------------------------------------ check "the sync document exists" 0 "" test -f "$DOC" check "the upstream ref is recorded" 0 "" test -f "$REF" recorded_ref() { grep -vE '^[[:space:]]*(#|$)' "$REF" | head -n1; } ref_is_recorded() { [ -n "$(recorded_ref)" ]; } check "...and is not blank — an unrecorded ref is a refusal, not a skip" 0 "" \ ref_is_recorded ref_looks_like_sha() { printf '%s' "$(recorded_ref)" | grep -qE '^[0-9a-f]{7,40}$'; } check "...and looks like a commit SHA" 0 "" ref_looks_like_sha # The ancestry half needs upstream's objects, which a CI clone of THIS repo # does not have. It is therefore conditional — but it says so out loud rather # than passing quietly, because "could not check" and "checked and fine" are # the two states this repo exists to keep apart. ancestry_state() { if git -C "$ROOT" cat-file -e "$(recorded_ref)^{commit}" 2>/dev/null; then git -C "$ROOT" merge-base --is-ancestor "$(recorded_ref)" HEAD 2>/dev/null \ && echo ANCESTOR || echo NOT-ANCESTOR else echo UNVERIFIABLE-HERE fi } state="$(ancestry_state)" printf 'upstream-delta: recorded ref %s is %s\n' "$(recorded_ref)" "$state" ancestry_acceptable() { [ "$state" = ANCESTOR ] || [ "$state" = UNVERIFIABLE-HERE ]; } check "the recorded ref is an ancestor of HEAD, or is honestly reported as unverifiable" 0 "" \ ancestry_acceptable # --- the inventory ------------------------------------------------------------ # The document's inventory table: the first `code` span of each table row in # the "Where the forge delta lives" section. inventory() { awk '/^## Where the forge delta lives/ { on = 1; next } on && /^## / { on = 0 } on && /^\| `/ { gsub(/^\| `/, ""); sub(/`.*$/, ""); print }' "$DOC" } inventory_size() { inventory | wc -l; } inventory_is_populated() { [ "$(inventory_size)" -ge 6 ]; } check "the inventory names at least the six known delta files" 0 "" \ inventory_is_populated in_inventory() { # $1 = repo-relative path local entry while IFS= read -r entry; do [ -n "$entry" ] || continue case "$1" in "$entry" | "$entry"*) return 0 ;; esac done < <(inventory) return 1 } check "a listed file is recognised" 0 "" in_inventory lib/forge.sh check "...including one listed as a directory" 0 "" in_inventory drills/0.4.1.md check "an unlisted file is not" 1 "" in_inventory lib/version.sh # --- the scan ----------------------------------------------------------------- # What "forge-specific" means, mechanically: the selector's own verbs and the # environment override that drives them. Comments are stripped — this tree # documents the forge split at length, and a guard that read prose as evidence # would flag every file that merely explains the design. FORGE_MARKERS='forge_detect|forge_select|forge_preflight|forge_client|CEREMONY_FORGE\b|CEREMONY_FORGE_CLIENT' forge_specific_files() { local f rel for f in "$ROOT"/lib/*.sh "$ROOT"/actions/*/*.sh "$ROOT"/bin/* \ "$ROOT"/.github/scripts/*.sh; do [ -f "$f" ] || continue rel="${f#"$ROOT"/}" sed 's/[[:space:]]#.*$//; s/^[[:space:]]*#.*$//' "$f" \ | grep -qE "$FORGE_MARKERS" && printf '%s\n' "$rel" done return 0 } # A call site that merely USES the shim is not forge-specific — every # reconciler calls forge_preflight and that is the point of the shim. What the # inventory is about is where the forge is DECIDED or BRANCHED ON. So the scan # reports files carrying the markers, and the assertion below allows the shim's # own consumers explicitly, by name, so adding a seventh consumer is silent but # adding a seventh DECIDER is not. SHIM_CONSUMERS='actions/issueflow-reconcile/issueflow-reconcile.sh actions/labels-reconcile/labels-reconcile.sh actions/labels-scope/labels-scope.sh actions/refs-not-closing/run.sh lib/facts.sh lib/ruling.sh lib/attention.sh' unlisted_deciders() { local rel while IFS= read -r rel; do [ -n "$rel" ] || continue grep -qxF "$rel" <<<"$SHIM_CONSUMERS" && continue in_inventory "$rel" && continue printf '%s\n' "$rel" done < <(forge_specific_files) } no_unlisted() { local found found="$(unlisted_deciders)" [ -z "$found" ] || { printf 'forge-specific but not in docs/UPSTREAM-SYNC.md:\n' >&2 printf ' %s\n' "$found" >&2 return 1 } } check "every forge-deciding file is named in the inventory" 0 "" no_unlisted # --- teeth -------------------------------------------------------------------- # The sweep above is a property of the whole tree and cannot be made to fail # without editing it, so the predicates are driven directly. TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT # shellcheck disable=SC2016 # fixture CONTENT: the literal text a scanned file would hold printf '%s\n' '#!/usr/bin/env bash' 'case "$(forge_detect)" in forgejo) : ;; esac' \ >"$TMP/scattered.sh" # Byte-identical to the strip forge_specific_files uses: a helper that # normalised differently would be testing a predicate the scan does not have. marker_seen() { sed 's/[[:space:]]#.*$//; s/^[[:space:]]*#.*$//' "$1" | grep -qE "$FORGE_MARKERS" } check "a new file branching on forge_detect is seen" 0 "" marker_seen "$TMP/scattered.sh" printf '%s\n' '#!/usr/bin/env bash' '# forge_detect used to live here' >"$TMP/prose.sh" check "prose about forge_detect is not" 1 "" marker_seen "$TMP/prose.sh" check "...and such a file would not be in the inventory" 1 "" in_inventory scattered.sh summary