What a clone lacked: the gates went green with a thousand skipped tests and said nothing, every login script assumed a worktree-pool slot and its probe account, the client defaulted to the maintainer's account, a docker path and a home directory were in the code, the temp files broke on GNU coreutils, and two generators could only run against a sibling repo. Now the test gates print how many data-gated tests skipped and why; a scripted run logs in as the account its checkout declares in `.probe-identity` or as the three variables name, never a default, and the client refuses a scripted login on any other account; the event and message tables regenerate from vendored inputs, byte for byte; the docs state the real prerequisites. Out of the tree, into the maintainer's own tooling: the worktree pool, its hooks, the perf leg runner, the cross-platform compile and the Vulkan container leg — a way of working, and setups not every machine has. One hook stays: no writes into the install. Two tests that could never run are gone with their subject: the quest-timer geometry diff compared a copy of the window that no longer exists (and joined a chain path onto a folder, so it always skipped), and the FrameXML flag gate now reads the stock files off the player's own patch chain instead of an extracted folder only one machine had. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
267 lines
17 KiB
Bash
Executable File
267 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# The commit gates (docs/METHOD.md "Gates"), one vetted runner: fmt-check →
|
||
# clippy -D warnings → workspace tests (skips refused where the data is) → doc-links →
|
||
# pass-span-lint → the player build and its own tests → the enforcer. Each is introduced where it
|
||
# runs, below. Fail-fast, nonzero exit on ANY failure, the failing log's tail printed.
|
||
#
|
||
# Exists because ad-hoc gate chains keep masking failures: `cargo … | tail` reports the PIPE's
|
||
# exit code, and `grep -c` exits nonzero on a zero count — both bit real sessions (phase-1a of
|
||
# the social arc, then again at the 6a land). Run this instead of composing pipelines.
|
||
set -uo pipefail
|
||
|
||
# Gate the tree you are STANDING IN, not the one this file happens to live in. A machine with
|
||
# several checkouts reaches for the gates by absolute path — `scripts/gates.sh` resolved from
|
||
# `$0`, which is another checkout. That silently gated the wrong one: fmt-red work in one
|
||
# worktree reported ALL GATES GREEN, because the other was clean.
|
||
#
|
||
# So: the git toplevel of $PWD, as long as it is a checkout of THIS repo (it has this script) — a
|
||
# stray invocation from a sibling repo like wow-5875-re falls back rather than gating that repo with
|
||
# benilla's gates. And print the tree either way: the silence is what made the old bug invisible.
|
||
here="$(cd "$(dirname "$0")/.." && pwd)"
|
||
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||
[ -n "$root" ] && [ -f "$root/scripts/gates.sh" ] || root="$here"
|
||
cd "$root" || exit 1
|
||
echo "gating: $root"
|
||
|
||
# ── Green-stamp memoization (decision 1822) ──────────────────────────────────────────────────────
|
||
# A green chain stamps target/.gates-green with a key of exactly what determined the verdict: the
|
||
# WORKING TREE's true content hash (untracked files included — hashed through a throwaway index, the
|
||
# real one untouched, so `target/` and `WoW` stay excluded by the ignore rules), the
|
||
# toolchain, this script itself, and where the install resolver points. Re-running on an unchanged
|
||
# tree is then instant instead of ~5–8 min — which is what a land after an already-gated sync, or a
|
||
# "once more to be sure", actually costs the machine. GATES_FORCE=1 runs the chain regardless.
|
||
# The install path is compared lazily (its resolver is a `cargo run`, instant only on a warm
|
||
# target): a tree that doesn't match skips that probe entirely.
|
||
stamp="target/.gates-green"
|
||
tree_key() {
|
||
local tmpidx t
|
||
tmpidx="$(mktemp "${TMPDIR:-/tmp}/benilla-gates-idx.XXXXXX")" && rm -f "$tmpidx" || return 1
|
||
t="$( (export GIT_INDEX_FILE="$tmpidx"
|
||
git read-tree HEAD && git add -A . && git write-tree) 2>/dev/null )"
|
||
rm -f "$tmpidx"
|
||
[ -n "$t" ] || return 1
|
||
printf '%s|%s|%s' "$t" "$(rustc -V 2>/dev/null)" \
|
||
"$(shasum "$root/scripts/gates.sh" 2>/dev/null | cut -d' ' -f1)"
|
||
}
|
||
resolve_wow() { cargo run -q -p benilla-formats --example where 2>/dev/null || true; }
|
||
# **A docs-only delta keeps the verdict** (decision 2049). The stamped tree and the current one are
|
||
# both real tree objects (write-tree puts them in the object store), so git can say exactly what
|
||
# differs. If every differing path is under `docs/` or a top-level `*.md` — nothing compiles,
|
||
# tests, formats or `include_str!`s those, checked — then the chain's outcome on this tree IS the
|
||
# stamped one, by construction rather than by optimism. This is what lets a landing that only
|
||
# regenerated the map after a rebase go through without paying the chain again.
|
||
# A stamped tree that the object store has since pruned simply fails the check, and the chain runs.
|
||
docs_only_delta() { # $1 = stamped tree, $2 = current tree
|
||
git cat-file -e "$1^{tree}" 2>/dev/null && git cat-file -e "$2^{tree}" 2>/dev/null || return 1
|
||
[ -z "$(git diff-tree -r --name-only "$1" "$2" 2>/dev/null | grep -vE '^(docs/|[^/]*\.md$)' || true)" ]
|
||
}
|
||
key_start="$(tree_key || true)"
|
||
if [ "${GATES_FORCE:-}" != "1" ] && [ -n "$key_start" ] && [ -f "$stamp" ]; then
|
||
old="$(cat "$stamp" 2>/dev/null)"
|
||
old_key="${old%|*}" # tree|rustc|script — the part that must match (bar the tree, see below)
|
||
memo=""
|
||
if [ "$old_key" = "$key_start" ]; then
|
||
memo="this exact tree already passed"
|
||
elif [ "${old_key#*|}" = "${key_start#*|}" ] && docs_only_delta "${old_key%%|*}" "${key_start%%|*}"; then
|
||
memo="this tree differs from one that passed only in top-level *.md, which no gate reads (0979/2049)"
|
||
fi
|
||
if [ -n "$memo" ] && [ "${old##*|}" = "$(resolve_wow)" ]; then
|
||
echo "ALL GATES GREEN (memoized — $memo; GATES_FORCE=1 re-runs)"
|
||
[ -d target ] && printf '%s\t%s\t%s\t%s\t%s\n' "$(date -u +%FT%TZ)" \
|
||
"$(git rev-parse --short HEAD 2>/dev/null || echo '-')" chain 0 memo >>target/.gates-timing 2>/dev/null
|
||
echo " (a clean run is the fourth gate: scripts/smoke.sh — live logout/re-login round trip)"
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
log="$(mktemp "${TMPDIR:-/tmp}/benilla-gates.XXXXXX")"
|
||
skips="$(mktemp "${TMPDIR:-/tmp}/benilla-gates-skips.XXXXXX")"
|
||
trap 'rm -f "$log" "$skips"' EXIT
|
||
|
||
# **How hollow was that green?** The data-gated tests pass without asserting on a machine that
|
||
# lacks the install or the addon corpus, and libtest swallows the skip line — so `install.rs`'s
|
||
# `skipped` appends one to `$BENILLA_SKIP_LOG` instead, and this reads the file back after each
|
||
# test rung. A clone without the data sees the number rather than a green it cannot weigh
|
||
# (docs/CONTRIBUTING.md, "Setting up"). Where the data is, `BENILLA_REQUIRE_DATA` has already
|
||
# made every skip a failure and the file stays empty.
|
||
report_skips() { # $1 = rung name
|
||
[ -s "$skips" ] || return 0
|
||
echo " $1: $(wc -l <"$skips" | tr -d ' ') data-gated tests SKIPPED on this machine —"
|
||
sort "$skips" | uniq -c | sort -rn | sed 's/^ *\([0-9]*\) \(.*\)/ \1 × \2/'
|
||
echo " (they run where the data is: docs/CONTRIBUTING.md, \"Setting up\")"
|
||
: >"$skips"
|
||
}
|
||
|
||
# ── Timing (decision 2265 §C1) ──────────────────────────────────────────────────────────────────
|
||
# Every gate's wall seconds, printed beside its verdict and appended to `target/.gates-timing`
|
||
# (gitignored with the rest of `target/`; one tab-separated line per gate per run: UTC time, tree
|
||
# short-sha, gate, seconds, verdict). 1822 measured the chain as the machine's single biggest load
|
||
# and scoped the per-round verify; its successor question — WHICH gate is the eleven minutes a
|
||
# land costs, and how that moves as the tree grows — could not be asked, because the only log was
|
||
# `rm -f`'d on exit and `run()` printed no duration. A memo hit and a skipped gate are recorded
|
||
# too, at 0 s, so a run's shape reads off the file whole.
|
||
timing="target/.gates-timing"
|
||
chain_t0=$SECONDS
|
||
tree_sha="$(git rev-parse --short HEAD 2>/dev/null || echo '-')"
|
||
note_timing() { # $1 = gate, $2 = seconds, $3 = verdict
|
||
[ -d target ] || return 0
|
||
printf '%s\t%s\t%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$tree_sha" "$1" "$2" "$3" >>"$timing" 2>/dev/null || true
|
||
}
|
||
|
||
run() {
|
||
local name="$1"
|
||
shift
|
||
local t0=$SECONDS
|
||
if ! "$@" >"$log" 2>&1; then
|
||
echo "GATE FAILED: $name ($((SECONDS - t0)) s)"
|
||
note_timing "$name" "$((SECONDS - t0))" "FAILED"
|
||
tail -30 "$log"
|
||
exit 1
|
||
fi
|
||
echo "gate ok: $name ($((SECONDS - t0)) s)"
|
||
note_timing "$name" "$((SECONDS - t0))" "ok"
|
||
}
|
||
|
||
run fmt cargo fmt --all -- --check
|
||
run clippy cargo clippy --workspace --all-targets -- -D warnings
|
||
|
||
# **Skips are refused where the data is** (decision 2329). `wow_data_or_skip!` and
|
||
# `addon_corpus_or_skip!` pass without asserting on a machine that lacks the install or the addon
|
||
# corpus, and libtest swallows a passing test's stderr — so on THIS machine a resolver that has
|
||
# drifted reads as green, which is how thirty corpus tests skipped at every land for three weeks
|
||
# after the pool moved drives. Where both resolve, `BENILLA_REQUIRE_DATA=1` turns a skip into the
|
||
# failure it is. Both, not either: with the install alone every corpus test would fail for the
|
||
# honest reason. Ask the resolver for the install (the same `where` the player-tests rung and the
|
||
# enforcer use); the corpus is the `wow-addons-vanilla` link at the tree's root.
|
||
wow_data="$(resolve_wow)"
|
||
require_data=""
|
||
if [ -n "$wow_data" ] && [ -d "$root/wow-addons-vanilla" ]; then
|
||
require_data=1
|
||
else
|
||
echo "gates: NOTE — install or addon corpus not found here (install='${wow_data:-none}'," \
|
||
"corpus=$([ -d "$root/wow-addons-vanilla" ] && echo yes || echo no)):" \
|
||
"data-gated tests skip silently on this machine"
|
||
fi
|
||
run test env ${require_data:+BENILLA_REQUIRE_DATA=1} BENILLA_SKIP_LOG="$skips" cargo test --workspace
|
||
report_skips test
|
||
|
||
# **doc-links** (decision 1925) — the docs are the knowledge base, so a doc link pointing at a
|
||
# DELETED item is rot, and nothing else here runs rustdoc. Deliberately narrow: it fails only on a
|
||
# `crate::`/`super::`/`self::`/`Self::` path whose leaf exists nowhere in the workspace. The broad
|
||
# `-D rustdoc::broken_intra_doc_links` was measured and rejected — 818 of its warnings are
|
||
# conventions this codebase writes on purpose (docs naming private internals by path, one crate
|
||
# naming another's module). The script's own header carries that measurement. ~22 s warm.
|
||
run doc-links scripts/doc-links.py
|
||
|
||
# **pass-span-lint** (decision 2258, bug B390) — one `pass_span` per render pass at a time. bevy's
|
||
# pass span is a pipeline-statistics query as well as a timestamp pair, wgpu allows one active at
|
||
# a time, and only Vulkan exposes the feature: Metal (this machine) and DX12 (the lab laptop's
|
||
# pinned backend) run a nested span as a silent no-op, which is how the 09-15 sync aborted every
|
||
# Linux and Vulkan-on-Windows player on their first world frame with every gate green. The runtime
|
||
# fact no compile or macOS run can reach, read off the source in a tenth of a second.
|
||
run pass-span-lint scripts/pass-span-lint.py
|
||
|
||
# **The player build** (decision 1173's deliverable; built in 1174). `benilla` is the binary a
|
||
# player would run, and `--no-default-features` drops the `dev` feature — so the debug panel, the
|
||
# perf HUD, the inspector, the capture harness and the probe fleet are not compiled at all. It
|
||
# fails the moment anything outside those roots names one of them.
|
||
#
|
||
# This line IS the mechanism, not the module boundary above it. 0026 adopted "add no new
|
||
# gameplay→dev coupling" in June 2026 with nothing to fail; by August there were 24 references
|
||
# across 12 files, because a rule with no failing build is a wish (1173). Same lesson as the
|
||
# enforcer below: a tripwire nobody trips is not a tripwire.
|
||
#
|
||
# `build`, not `test`: the seam's claim is that the player binary LINKS without the instruments.
|
||
# The tests are dev-side by construction (they drive fixtures and probes) and stay on the default
|
||
# feature set, where `cargo test --workspace` above already runs them.
|
||
run player-build cargo build -p benilla --no-default-features
|
||
|
||
# **The player build's own unit tests** (decision 1175). A build proves the seam LINKS; it cannot
|
||
# prove the player configuration BEHAVES. The two facts 1175 rests on are both `#[cfg(not(feature =
|
||
# "dev"))]` — the install resolver looks nowhere inside the source tree, and the state folder lands
|
||
# beside the binary — so `cargo test --workspace` above, which runs with default features, never
|
||
# executes either of them. A falsifier that never runs is 1173's wish with a `#[test]` on it.
|
||
#
|
||
# `--lib` only: the integration tests and examples are dev-side by construction. Measured at ~7 s
|
||
# warm, because these two crates' unit tests are pure.
|
||
#
|
||
# **`$WOW_DATA` is handed in, and that is not a loophole.** Decision 1751 began sourcing FrameXML
|
||
# off the player's own patch chain, so the shipped-UI tests in this same `--lib` set now need an
|
||
# install — while the whole point of `--no-default-features` is that the resolver's *source-tree*
|
||
# rung is compiled out. The two collided silently: 27 tests here failed for a missing
|
||
# `Interface\FrameXML\ContainerFrame.xml` on every machine whose shell did not happen to export
|
||
# the variable, and the gate had been red for that reason rather than for anything a commit did.
|
||
# `$WOW_DATA` is rung ONE of the ladder, not the dev rung, so both facts 1175 asks this gate to
|
||
# falsify still hold: nothing here reads the source tree, and `install.rs`'s own tests take their
|
||
# environment as arguments (that is why they do) and cannot see this at all.
|
||
#
|
||
# Ask the resolver, don't re-derive it — the same rule, and the same `where`, as the enforcer
|
||
# below; `$wow_data` was resolved once, ahead of the test gate. No install found: run it bare,
|
||
# and let the client-data tests say so themselves (`BENILLA_REQUIRE_DATA` is deliberately NOT
|
||
# handed down: rung 2 is compiled out here, and what this rung falsifies is the player ladder,
|
||
# not the resolver — a skip here would be the honest one).
|
||
run player-tests env ${wow_data:+WOW_DATA="$wow_data"} BENILLA_SKIP_LOG="$skips" \
|
||
cargo test -p benilla-formats -p benilla-app --no-default-features --lib
|
||
report_skips player-tests
|
||
|
||
# **The enforcer** (decision 1160's second binary, made into a gate by 1164). `benilla-worldview`
|
||
# boots the engine plugin set with no server, no login, no UI and no player, and any system whose
|
||
# parameters need a gameplay resource reports itself. It is the only check for coupling that
|
||
# crosses NO SYMBOL — a `Res<player::Player>` read is invisible to the compiler and to the API wall
|
||
# test, and shows up only at runtime.
|
||
#
|
||
# It is here because it rotted: the binary sat broken on main, failing on frame one, for as long as
|
||
# nobody happened to run it. A tripwire nobody trips is not a tripwire.
|
||
#
|
||
# ~15 s, and it opens a small cornered window (`bgwin`'s no-pixel rule) — it reads the error log,
|
||
# not the framebuffer. Skipped, loudly, without the 1.12.1 install: the gate cannot demand an asset
|
||
# tree the repo is forbidden to contain.
|
||
# **Ask the resolver, don't re-derive it** (decision 1175). This used to read
|
||
# `[ -n "$WOW_DATA" ] || [ -d WoW/Data ]` — a second, hand-rolled copy of the rule for where the
|
||
# install is, which is the exact duplication that record exists to end. A gate that disagrees with
|
||
# the client skips on a machine where the client would have run, or runs where it cannot.
|
||
if cargo run -q -p benilla-formats --example where >/dev/null 2>&1; then
|
||
run enforcer env WOW_WORLDVIEW_CHECK=10 cargo run -q -p benilla-worldview
|
||
else
|
||
echo "gate SKIPPED: enforcer (no WoW install found — the engine boot check needs one;"
|
||
echo " \`cargo run -p benilla-formats --example where\` says where it looked)"
|
||
note_timing enforcer 0 SKIPPED
|
||
fi
|
||
|
||
# **The enforcer again, with NO install** (decision 1451) — the boot every player who unzips
|
||
# benilla into the wrong folder takes, and the one nothing on this machine could take: a dev build
|
||
# finds the project folder, so `wow_data()` never returned `None` in any run we make. Nobody had
|
||
# to *break* that path; it simply drifted, and by August a `Startup` that inserts a resource only
|
||
# when there is client data faced an `Update` that took it as a hard `Res`. Bevy's default handler
|
||
# panics on a parameter that cannot validate, so the client died on frame one with a system name it
|
||
# could not even print. `WOW_DATA=` (set, empty) is `benilla_formats::install`'s spelling of "there
|
||
# is no install", and the check's error handler collects every such fault instead of dying on the
|
||
# first — so this run names them all, the day they land.
|
||
#
|
||
# No `if`: this gate is the one that needs no install. ~5 s (nothing to stream — every system is
|
||
# offered to the executor, which is what validates its parameters, within the first frames), and
|
||
# the same small cornered window as above. It covers the engine crate, which is where the
|
||
# world-shaped resources live; the client's own no-install boot is still eyes-on.
|
||
run enforcer-no-install env WOW_DATA= WOW_WORLDVIEW_CHECK=5 cargo run -q -p benilla-worldview
|
||
|
||
echo "ALL GATES GREEN ($((SECONDS - chain_t0)) s; per-gate seconds in $timing)"
|
||
note_timing chain "$((SECONDS - chain_t0))" ok
|
||
|
||
# Stamp the verdict — but only if the tree is still the one the chain actually ran on: parallel
|
||
# agents share this working tree (docs/METHOD.md, "Who types what"), and a stamp for a half-edited tree
|
||
# would memoize a green nothing verified. A changed key just means no stamp; nothing fails.
|
||
key_end="$(tree_key || true)"
|
||
if [ -n "$key_end" ] && [ "$key_end" = "$key_start" ] && [ -d target ]; then
|
||
printf '%s|%s\n' "$key_end" "$wow_data" > "$stamp" 2>/dev/null || true
|
||
fi
|
||
|
||
# **The fourth gate has a runner too, and it is not this script** (2277). docs/METHOD.md asks for
|
||
# `fmt` · `clippy` · `test` · *a clean run*, and for a long time only the first three were a
|
||
# command — so "a clean run" was a paragraph in docs/METHOD.md, which a session can simply not have
|
||
# read. One did, decided it could not reach the server at all, and said so to the director while
|
||
# shipping a change whose entire subject was a session boundary.
|
||
#
|
||
# Not a rung: it wants the local vmangos, opens a window, and costs ~45 s, so paying it per commit
|
||
# is the director's call. A pointer costs nothing and makes the verb impossible to not know about.
|
||
echo " (a clean run is the fourth gate: scripts/smoke.sh — live logout/re-login round trip)"
|