ceremony/test/upstream-delta.test.sh

165 lines
6.7 KiB
Bash
Raw Normal View History

docs: the recurring upstream sync, its standing resolutions, and a guard on where the delta lives (#200) The third child of #197, written immediately after performing the sync it describes, while the findings are still first-hand. docs/UPSTREAM-SYNC.md carries the procedure and the six standing resolutions, each with the issue that decided it, so they are not re-argued every sync. The parts that are not obvious from the outside, and that the 0.6.0 sync paid to learn: * THE AUDIT STEP. `git merge` takes upstream's side wherever only upstream moved a region, so a function upstream ADDED to a file this tree owns arrives with no conflict and no question. Reviewing the hunks cannot find it — four reviewers read the same diff and each found a different subset. That was eight runtime `gh` call sites in three files and two file types. * THE SAME MECHANIC APPLIES TO STATE. A resolved region can remove a producer whose consumers auto-merged, and those consumers degrade to empty rather than erroring, so nothing goes red. Three such seams in one sync. * VERIFY WHERE IT RUNS. "Green locally" was wrong three times, for three different reasons: shellcheck-all lints TRACKED files so a new file's first lint is meaningless; CI pins shellcheck 0.10.0; and the runner's jq 1.6 exits 0 where 1.7 exits 4 on `jq -e` with empty input — which was not a test problem but a guard accepting an unreadable read. * TEST THE MERGE RESULT. Forgejo tests heads, never what two branches produce together, and two green PRs did produce a red tree in this sync. * AFTER MERGING, CHECK THE SWEEP RECONCILED SOMETHING. The first post-merge run was green and had done nothing. .upstream-ref records the carried commit in machine-readable form beside the CHANGELOG's prose. test/upstream-delta.test.sh asserts every forge-DECIDING file is named in the inventory — offline, comment-aware, and refusing rather than skipping when the ref is missing. Shim CONSUMERS are allowed by name, so a seventh consumer is silent and a seventh decider is not. docs/CONSUMERS.md now states that two ceremonies answer to the same version number and how a consumer says which one it pinned. Must-fail, both from the issue's test plan: scattering a forge_detect branch into an unlisted file reds the guard; blanking .upstream-ref reds it too. test/run.sh 29 files 0 failed under jq 1.7 and jq 1.6; shellcheck 0.10.0, actionlint, self-ref, marker, vendored and changelog-armed all clean. Refs #200
2026-08-05 13:36:17 +00:00
#!/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