diff --git a/.tangled/workflows/check.yml b/.tangled/workflows/check.yml index 81a2d38f..5982ee20 100644 --- a/.tangled/workflows/check.yml +++ b/.tangled/workflows/check.yml @@ -21,6 +21,14 @@ clone: dependencies: nixpkgs: + # Shell gates are intentionally explicit: Nixery's base image does not + # provide the normal Unix text/file utilities. + - bash + - coreutils + - findutils + - gawk + - gnugrep + - gnused - rustc - cargo - rustfmt diff --git a/tools/check.sh b/tools/check.sh index 14becb6c..6c04fc5b 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -152,6 +152,27 @@ for script in tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh tools/seed- bash -n "$script" || { echo "FAIL: shell syntax: $script"; fail=1; } done +step "gate dependency preflight" +bash_bin=$(command -v bash) +partial_path=$(mktemp -d) +for available_command in comm dirname head mktemp rm sort tr wc; do + command_path=$(command -v "$available_command" || true) + [ -n "$command_path" ] || continue + ln -s "$command_path" "$partial_path/$available_command" +done +for gate in tools/corpus_gate.sh tools/wiki_gate.sh; do + if gate_output=$(PATH="$partial_path" "$bash_bin" "$gate" 2>&1); then + echo "FAIL: $gate returned success without its required commands" + fail=1 + elif ! grep -q '^FAIL: required command unavailable:' <<< "$gate_output"; then + echo "FAIL: $gate did not fail through its command preflight" + fail=1 + else + echo "$gate: rejects missing dependencies" + fi +done +rm -rf "$partial_path" + # Docs gates run in parallel with each other (and with the Rust gate when # both are needed). They never take the rust flock. docs_pids=() @@ -173,6 +194,8 @@ step "docs gates (parallel)" start_docs_gate "corpus" "bash tools/corpus_gate.sh" start_docs_gate "wiki" "bash tools/wiki_gate.sh" start_docs_gate "ledger-index" "bash tools/ledger_index.sh --check" +# The single-quoted program is evaluated inside start_docs_gate, not here. +# shellcheck disable=SC2016 start_docs_gate "env-registry" ' env_reg=wiki/engineering/env.md env_missing=0 diff --git a/tools/corpus_gate.sh b/tools/corpus_gate.sh index 71bba06a..426c9211 100755 --- a/tools/corpus_gate.sh +++ b/tools/corpus_gate.sh @@ -3,7 +3,17 @@ # Scans the wiki tree once per concern instead of re-grepping every file # for every field (the previous form was a fork storm under concurrent agents). set -uo pipefail -cd "$(dirname "$0")/.." + +missing_commands=0 +for required_command in awk dirname find grep head mktemp rm sed sort tr wc; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf 'FAIL: required command unavailable: %s\n' "$required_command" + missing_commands=1 + fi +done +[ "$missing_commands" -eq 0 ] || exit 1 + +cd "$(dirname "$0")/.." || exit 1 fail=0 @@ -30,7 +40,8 @@ tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT heading_slugs_file() { - local file="$1" cache="$tmp/headings.$(printf '%s' "$file" | tr '/ ' '__')" + local file="$1" cache + cache="$tmp/headings.$(printf '%s' "$file" | tr '/ ' '__')" if [ ! -f "$cache" ]; then # slugify_heading prints without a trailing newline; add one per heading # so the cache is line-oriented for grep -qxF. diff --git a/tools/wiki_gate.sh b/tools/wiki_gate.sh index df283639..1e50cab7 100755 --- a/tools/wiki_gate.sh +++ b/tools/wiki_gate.sh @@ -13,7 +13,17 @@ # writing that exact bracket-paren shape in prose; describe the syntax in # words instead. set -uo pipefail -cd "$(dirname "$0")/.." + +missing_commands=0 +for required_command in comm dirname find grep mktemp rm sed sort; do + if ! command -v "$required_command" >/dev/null 2>&1; then + printf 'FAIL: required command unavailable: %s\n' "$required_command" + missing_commands=1 + fi +done +[ "$missing_commands" -eq 0 ] || exit 1 + +cd "$(dirname "$0")/.." || exit 1 fail=0 tmp=$(mktemp -d) diff --git a/wiki/log/2026-07-10-ci-gates-fail-closed.md b/wiki/log/2026-07-10-ci-gates-fail-closed.md new file mode 100644 index 00000000..ddef7fd2 --- /dev/null +++ b/wiki/log/2026-07-10-ci-gates-fail-closed.md @@ -0,0 +1,49 @@ +# CI corpus gates fail closed + +``` +Type: log +``` + +## Intent + +Make remote corpus enforcement real: missing audit utilities must turn the +Tangled job red instead of letting an empty scan report success. + +## Finding + +The Tangled run for the corpus-hardening landing displayed green even though +its corpus and wiki steps printed `command not found` for `find`, `grep`, and +`sed`. Nixery did not include the ordinary shell utilities implicitly. The +scripts used `set -uo pipefail`, but the missing commands ran inside process +substitutions and loops whose failures did not reach the final exit status, so +both audits could scan nothing and report success. + +This was a process insecurity: the local gate was real, but the remote copy of +the same gate could fail open. + +## Fix + +- Tangled's check image now explicitly installs Bash, coreutils, findutils, + gawk, GNU grep, and GNU sed. +- `tools/corpus_gate.sh` and `tools/wiki_gate.sh` preflight every external + command they use and exit nonzero before scanning when one is unavailable. +- `tools/check.sh --docs` constructs the CI-like partial `PATH` that caused the + false green and requires both scripts to reject it through that preflight. +- The workflow knowledge page now states that a missing audit dependency is a + hard failure. + +## Verification + +- Reproduction before the fix: both gates exited 0 while printing missing + `find`/`grep`/`sed` errors. +- Regression after the fix: both gates exit 1 and name the missing commands + under the same partial `PATH`. +- `./tools/check.sh --docs` passes, including the new fail-closed regression + step and the normal corpus, wiki, and environment gates. +- `shellcheck tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh` passes. + +## Design impact + +No game or corpus rule changed. This restores +`wiki/process/meta.md` acceptance criterion 9 in the environment that was not +actually enforcing it. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 543cc8f8..ceae84cb 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -30,6 +30,11 @@ add or amend a session log, then re-run the generator. - Intent: Cameron: mode change should be hover over a rack and press 1-4 immediately — not enter + enter + select dial row + enter. - Log: [wiki/log/2026-07-10-hover-mode-keys.md](2026-07-10-hover-mode-keys.md) +## 2026-07-10 - CI corpus gates fail closed + +- Intent: Make remote corpus enforcement real: missing audit utilities must turn the Tangled job red instead of letting an empty scan report success. +- Log: [wiki/log/2026-07-10-ci-gates-fail-closed.md](2026-07-10-ci-gates-fail-closed.md) + ## 2026-07-10 - Bevy performance pass - Intent: The material frontend was paying full restyle/UI cost every display frame: `advance_sim` held `ResMut` and ticked a timer on `Game` every frame, so `game.is_changed()` stayed true and `restyle_3d` / `render_map` / `render_ui` rebuilt continuously. diff --git a/wiki/process/workflows.md b/wiki/process/workflows.md index 5ce77186..68d61dcc 100644 --- a/wiki/process/workflows.md +++ b/wiki/process/workflows.md @@ -165,7 +165,11 @@ rebase of every place that teaches, checks, or renders the authority model. The doorway is protected mechanically: `tools/corpus_gate.sh` bounds its size, forbids binding-page structure, validates every page role and spec header, and resolves every structured Design target and heading. Local checks and CI call -this same script so the two implementations cannot drift. +this same script so the two implementations cannot drift. Both corpus scripts +preflight their external commands and fail before auditing if any are absent; +the Tangled Nixery image explicitly supplies Bash, GNU text utilities, +findutils, and coreutils. A missing audit dependency must make CI red, never +turn an empty scan into a green result. ## Browsing the wiki