From 3d85d91cae2d0403dde1b004970a9445944b990c Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:23:23 +0000 Subject: [PATCH] The gate restored and given a manifest (7 October 2026, 22:2x UK): tools/ci/checks.txt names every check and a push is red when a name has no run line or a run line no name At 21:43 UK a merge on the box mirror (b7abee81, site-land-dn3) resolved its conflict in tools/ci/pre-push.sh by taking an old copy: 88 lines gone, among them the hook's CI rule (master_ci_ok, the remote and exception logic), the branch red line, the overlap wall clock and the self-tests of the timeouts check, retry-once, ci-state, the public ledger and the gh-account check; every landing for 40 minutes ran the weaker gate and nothing said so. This commit restores the file (the one real addition of the other side, the inline-rm check, kept) and adds tools/ci/gate-manifest-check.sh in the never-push set: every push on every branch, and the merge's own push, compares pre-push.sh's run lines with tools/ci/checks.txt both ways; known-failed first (a dropped check and an unlisted check each red and named). Adding or removing a check edits checks.txt in the same commit; --write regenerates it. Co-Authored-By: Claude Fable 5.1 --- tools/ci/checks.txt | 71 +++++++++++++++++++++++++++++++++ tools/ci/gate-manifest-check.sh | 39 ++++++++++++++++++ tools/ci/pre-push.sh | 69 +++++++++++++++++++++++++++----- 3 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 tools/ci/checks.txt create mode 100755 tools/ci/gate-manifest-check.sh diff --git a/tools/ci/checks.txt b/tools/ci/checks.txt new file mode 100644 index 000000000..1d2a9c528 --- /dev/null +++ b/tools/ci/checks.txt @@ -0,0 +1,71 @@ +# every check tools/ci/pre-push.sh runs, one name per line (tools/ci/gate-manifest-check.sh --write regenerates it; commit it with the gate change) +no lateral scroll: scrollWidth equals clientWidth on every route at five widths, both themes (self-test, then the site; skipped where there is no Playwright) +no conflict markers in tracked files +every tracked path is valid on Windows (colon, trailing dot, reserved names, length) +identity grep of the public export list and the served site +no secret file names and no 64-hex secrets in the tree +the gate's manifest: every listed check has its run line and every run line is listed (never-push: a conflict resolution cannot drop a check) +no founder name, personal login, earlier business or personal address in any tracked text file (the pre-public scrub; self-test first, encoded list) +every check in tools/ci/checks.txt has its run line here and every run line is listed (a conflict resolution cannot drop a check unseen; self-test first) +site build (in a temporary copy here, in place only inside GitHub Actions) +internal link check of site/*.html +every served page carries the slim bar (mark, Mine, Network, Learn, Download) with every route in its panels and the sheet (self-test, then the tree) +vendor marks: site/lib/marks.mjs is brand/marks/vendor-marks.mjs byte for byte (the app and the site draw one set) +the phone menu opens and is seen at 390 px on every page (self-test first; needs the box or CI browser, says so without one) +padding and visuals: text 24 px from every band, card and section edge, section padding on the scale, no touching controls, media inside its frame, no heading under the bar, at 390 to 1600 px both themes (known-failed fixture first) +ledger sentences present verbatim on their public pages +shell inside .github/workflows parses (bash -n, the PowerShell 5.1 rule) +PowerShell drive-reference check (\$name: in a double-quoted string) +copied sources are re-stamped before a build +override params files parse with no duplicate key +second-engine playbooks log to a file and end their tree (C35) +no playbook quits, pauses or resumes the installed app +no script writes into another worktree or walks Projects +the signer is never piped into head +bash bodies in PowerShell job scripts pass bash -n +run jobs test their fetched kit before use +every Windows spawn of the app runs with a hidden console +pinned guest programs match their manifest +root prover playbooks kill the GPU server and unlink its socket +commit-string gate self-test +engine gate self-test (igneumd and igneum-miner must carry igneum-pow/src/ paths) +build server remote checkout self-test +a slot holder keeps its own line for the whole run (the watcher-trust rule) +the remote checkout resets the mirror's tree before the branch checkout (the stale-overlay class) +the remote checkout's clean spares a lane's scratch (.igneum-scratch-spare, the fixed prefixes, never -x; the lost-scratch class) +long-running tools keep their body in one parsed block (the edited-while-running class) +build-remote without a priority flag bounds suites and benches (nice 10, 32 cores); a gate runs unbounded +the class router is a preference with spill-over (a held or overloaded box hands the job to the other one) +per-core leases, the quiet class and the reaper pass on the box (lease.sh and remote-run.sh self-tests over ssh; skipped on a runner with no box; one retry) +the simulators job runs on master and release-* pushes and pull requests into them only +publish-jobs.sh never removes a running or installs-app job without --force (the PC 2 abort class) +no shell assignment hides behind a trailing comment (the swallowed-defaults class) +no script kills or finds a process by a plain name or a file name (pgrep/pkill -f literals, ps | grep) +no deletion inside an inline bash -c / sh -c string in a tracked script (self-test first; the app cannot check it) +the identity check's own self-test (excluded research path passes, exported leak fails) +the Windows paths check's own self-test +the red watcher's own self-test (one line per run, posted once) +faucet unit tests +redesign package data tests (the /api/live contract the pages read) +the home hero's loop never idles in view, stops hidden, resumes without a jump +chain scene: the site's and the app's copies are scene/ byte for byte, the palette tokens live once (self-test, then the tree) +chain scene: a push paints with the document hidden and no animation frame (the blank /live of 7 Oct 2026; known-failed first) +chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused) +chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright; one retry) +no text overlaps: every served page at 390 to 1600 px, light and dark, the hero at each step (self-test first; IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs) +explorer, emission and public stats unit tests +ship tool self-test +relay unit tests +miner app notice strip and update card tests +launch gates: every row with its check, the handoff text clean (self-test, then the tree) +income per tier: the public table equals its inputs, the schedule arithmetic +hash-origin report: a known-finished day and a known-failed day +harness summaries never carry a raw 64-hex key (the writer's own redaction and check) +docs-only pushes skip the compile-or-compute CI jobs (the changes job's classifier) +the public ledger (docs/ledger-public.md) is what docs/fud-ledger.md generates: one row per item, no commit ids, times or team names (self-test first) +every workflow job carries timeout-minutes (site 15, changes 10, pow 60, sims 45; the hung-job class of 7 October 2026) +a box or network check gets one retry before it is red (retry-once self-test) +gh's active account on the pushing Mac is the stored Igneum entry (self-test: another login refused and named; the hook and the merge tool run the check live) +CI state reader: a commit's newest run, master's last compiled run, a branch's last red (fake gh; the merge rule's reader) +known failure +known success diff --git a/tools/ci/gate-manifest-check.sh b/tools/ci/gate-manifest-check.sh new file mode 100755 index 000000000..1f8d83d32 --- /dev/null +++ b/tools/ci/gate-manifest-check.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# The gate's manifest: tools/ci/checks.txt names every check tools/ci/pre-push.sh runs (one name per line, the text inside run "..."), +# and this check fails when a listed name has no run line in pre-push.sh or a run line carries a name the list lacks. Both ways, so a +# conflict resolution that takes an old copy of pre-push.sh (7 October 2026, 21:43 UK: 88 lines of checks and functions gone from the +# mirror's master for 40 minutes) and a new check added without its manifest line are both red. It runs in the never-push set: every +# push on every branch, before the merge's own push too. Adding or removing a check means editing checks.txt in the same commit, +# which is the point: a check leaves the gate only on purpose and in the diff. +# +# tools/ci/gate-manifest-check.sh # exit 1 naming each missing run line or unlisted check +# tools/ci/gate-manifest-check.sh --self-test # a fixture gate missing one listed check fails and names it; one with an unlisted +# # run line fails and names it; the real tree passes +# tools/ci/gate-manifest-check.sh --write # rewrite checks.txt from the current pre-push.sh (then commit both together) +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +names_in() { grep -E '^[[:space:]]+run "' "$1" | sed -E 's/^[[:space:]]+run "([^"]+)".*/\1/'; } +compare() { # -> exit 1 with the lines + local gate="$1" manifest="$2" rc=0 n + while IFS= read -r n; do [ -n "$n" ] || continue; names_in "$gate" | grep -qxF -- "$n" || { echo "gate-manifest: listed check has no run line in $(basename "$gate"): $n" >&2; rc=1; }; done < <(grep -vE '^\s*(#|$)' "$manifest") + while IFS= read -r n; do [ -n "$n" ] || continue; grep -qxF -- "$n" "$manifest" || { echo "gate-manifest: run line not in $(basename "$manifest"): $n" >&2; rc=1; }; done < <(names_in "$gate") + return $rc +} +case "${1:-}" in + --write) { echo "# every check tools/ci/pre-push.sh runs, one name per line (tools/ci/gate-manifest-check.sh --write regenerates it; commit it with the gate change)"; names_in "$HERE/pre-push.sh"; } > "$HERE/checks.txt"; echo "gate-manifest: wrote $HERE/checks.txt ($(grep -vc '^#' "$HERE/checks.txt") checks)"; exit 0 ;; + --self-test) + d="$(mktemp -d)"; trap 'rm -rf "$d"' EXIT; fails=0 + printf ' run "alpha check" true\n run "beta check" true\n' > "$d/gate.sh"; printf '# m\nalpha check\nbeta check\n' > "$d/m.txt" + compare "$d/gate.sh" "$d/m.txt" >/dev/null 2>&1 || { echo "self-test failed: a matching gate and manifest were reported"; fails=1; } + printf ' run "alpha check" true\n' > "$d/gate2.sh" + out="$(compare "$d/gate2.sh" "$d/m.txt" 2>&1)" && { echo "self-test failed: a dropped check passed"; fails=1; } + case "$out" in *"no run line"*"beta check"*) ;; *) echo "self-test failed: the dropped check was not named: $out"; fails=1 ;; esac + printf ' run "alpha check" true\n run "beta check" true\n run "gamma check" true\n' > "$d/gate3.sh" + out="$(compare "$d/gate3.sh" "$d/m.txt" 2>&1)" && { echo "self-test failed: an unlisted check passed"; fails=1; } + case "$out" in *"not in"*"gamma check"*) ;; *) echo "self-test failed: the unlisted check was not named: $out"; fails=1 ;; esac + compare "$HERE/pre-push.sh" "$HERE/checks.txt" >/dev/null 2>&1 || { echo "self-test failed: the tree's gate and manifest disagree (run tools/ci/gate-manifest-check.sh --write and commit)"; fails=1; } + [ "$fails" = 0 ] && echo "self-test passed: a dropped check and an unlisted check are each red and named; the tree's gate matches its manifest" + exit $fails ;; + "") compare "$HERE/pre-push.sh" "$HERE/checks.txt" && echo "gate-manifest: every one of $(grep -vc '^#' "$HERE/checks.txt") listed checks has its run line and every run line is listed" ;; + *) echo "usage: tools/ci/gate-manifest-check.sh [--self-test|--write]" >&2; exit 2 ;; +esac diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index ed4ca5a79..313b55cf3 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -26,6 +26,7 @@ cd "$(git rev-parse --show-toplevel)" || exit 1 # and fire this hook again inside the fixture: the first master push through the gate died that way (6 October 2026). unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_QUARANTINE_PATH GIT_PUSH_OPTION_COUNT MODE="${1:-local}"; MODE="${MODE#--}" +GATE_ROOT="$(pwd -P)" # the readers below are called from fixture repositories in the self-test, so by absolute path RED=0; N=0; LOG="$(mktemp)"; trap 'rm -rf "$LOG" "${SITE_TMP:-}"' EXIT T0=$(date +%s) @@ -52,12 +53,20 @@ site_build() { (cd "$SITE_TMP/site" && SITE_DOWNLOADS_OFFLINE=1 node build.mjs) } +wall_clock() { # : under GNU timeout where it exists (the runners); the Mac has none, and the job's timeout-minutes is the stop there. + # No array here: an empty array expanded under set -u is "unbound variable" on the Mac's bash 3.2 and ended the gate with no RED line (17:3x UK, 7 October 2026). + local secs="$1"; shift + if command -v timeout >/dev/null 2>&1; then timeout "$secs" "$@"; else "$@"; fi +} overlap_sweep() { + run "no lateral scroll: scrollWidth equals clientWidth on every route at five widths, both themes (self-test, then the site; skipped where there is no Playwright)" bash -c 'node tools/ci/scroll-width-check.mjs --self-test && node tools/ci/scroll-width-check.mjs --site site' # tools/ci/overlap-check.mjs: the known-failed fixture first, then the built site (the gate's temporary copy locally, the tree in # CI). A browser is needed: CI installs Playwright in the workflow; a machine without one ships the pages to a build box # (nothing heavy on the Mac). IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs through their mocks (slower, the box). local dir="site"; [ "$MODE" = ci ] || dir="$SITE_TMP/site" - if [ "${IGNEUM_OVERLAP_APPS:-0}" = 1 ]; then node tools/ci/overlap-check.mjs --self-test --site "$dir" --apps .; else node tools/ci/overlap-check.mjs --self-test --site "$dir"; fi + # a wall clock of 10 minutes where GNU timeout exists (the runners; the Mac ships the sweep to a box): the sweep took 192 s on a hosted + # runner on 7 October 2026 and three master jobs hung in this step for over two hours each the same afternoon + if [ "${IGNEUM_OVERLAP_APPS:-0}" = 1 ]; then wall_clock 600 node tools/ci/overlap-check.mjs --self-test --site "$dir" --apps .; else wall_clock 600 node tools/ci/overlap-check.mjs --self-test --site "$dir"; fi } structural_checks() { @@ -71,11 +80,14 @@ never_push_checks() { # A secret or an identity leak must not reach the remote on any branch; together about 20 s on the Mac. run "identity grep of the public export list and the served site" bash tools/ci/identity-check.sh run "no secret file names and no 64-hex secrets in the tree" bash -c 'bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh' + run "the gate's manifest: every listed check has its run line and every run line is listed (never-push: a conflict resolution cannot drop a check)" bash tools/ci/gate-manifest-check.sh # the third never-push class (7 October 2026, 20:5x UK): a founder name on ANY branch, because every branch went to the public host's # mirror and a branch green-stamped before the check existed carried one onto master through the deferred merge run "no founder name, personal login, earlier business or personal address in any tracked text file (the pre-public scrub; self-test first, encoded list)" bash -c 'bash tools/ci/founder-strings-check.sh --self-test && bash tools/ci/founder-strings-check.sh' - # rule 15 (7 October 2026): a release branch reads its own version in Cargo.toml, Cargo.lock and version.h from its first commit - run "release-version: a release-0.3.N branch reads 0.3.N in Cargo.toml, Cargo.lock and version.h (self-test first)" bash -c 'bash tools/ci/release-version-check.sh --self-test && bash tools/ci/release-version-check.sh' + # the gate's own manifest (7 October 2026, 22:2x UK): a merge on the mirror resolved a pre-push.sh conflict by taking an old copy and 88 lines of + # checks and functions left master's gate unseen for 40 minutes; every check name is listed in tools/ci/checks.txt and a name without its run + # line, or a run line without its name, is red on every push + run "every check in tools/ci/checks.txt has its run line here and every run line is listed (a conflict resolution cannot drop a check unseen; self-test first)" bash -c 'bash tools/ci/gate-manifest-check.sh --self-test && bash tools/ci/gate-manifest-check.sh' } tree_checks() { @@ -109,12 +121,12 @@ tree_checks() { run "long-running tools keep their body in one parsed block (the edited-while-running class)" bash -c 'bash tools/ci/whole-body-check.sh --self-test && bash tools/ci/whole-body-check.sh' run "build-remote without a priority flag bounds suites and benches (nice 10, 32 cores); a gate runs unbounded" bash tools/ci/build-kind-default-check.sh run "the class router is a preference with spill-over (a held or overloaded box hands the job to the other one)" bash tools/ci/route-spill-check.sh - run "no deletion inside an inline bash -c / sh -c string in a tracked script (self-test first; the app cannot check it)" bash tools/ci/inline-rm-check.sh --self-test - run "per-core leases, the quiet class and the reaper pass on the box (lease.sh and remote-run.sh self-tests over ssh)" bash tools/ci/box-locks-check.sh $( [ "$MODE" = ci ] && echo --ci ) + run "per-core leases, the quiet class and the reaper pass on the box (lease.sh and remote-run.sh self-tests over ssh; skipped on a runner with no box; one retry)" bash tools/ci/retry-once.sh box-locks bash tools/ci/box-locks-check.sh $( [ "$MODE" = ci ] && echo --ci ) run "the simulators job runs on master and release-* pushes and pull requests into them only" bash tools/ci/sims-branch-check.sh run "publish-jobs.sh never removes a running or installs-app job without --force (the PC 2 abort class)" bash tools/ci/publish-jobs-check.sh run "no shell assignment hides behind a trailing comment (the swallowed-defaults class)" bash -c 'bash tools/ci/defaults-line-check.sh --self-test && bash tools/ci/defaults-line-check.sh' run "no script kills or finds a process by a plain name or a file name (pgrep/pkill -f literals, ps | grep)" bash -c 'bash tools/ci/kill-by-name-check.sh --self-test && bash tools/ci/kill-by-name-check.sh' + run "no deletion inside an inline bash -c / sh -c string in a tracked script (self-test first; the app cannot check it)" bash tools/ci/inline-rm-check.sh --self-test run "the identity check's own self-test (excluded research path passes, exported leak fails)" bash tools/ci/identity-check.sh --self-test run "the Windows paths check's own self-test" bash tools/ci/windows-paths-check.sh --self-test run "the red watcher's own self-test (one line per run, posted once)" node tools/ci/red-watch.mjs --self-test @@ -123,15 +135,15 @@ tree_checks() { run "the home hero's loop never idles in view, stops hidden, resumes without a jump" node tools/site-redesign/tests/hero-loop-test.cjs run "chain scene: the site's and the app's copies are scene/ byte for byte, the palette tokens live once (self-test, then the tree)" bash -c 'node tools/scene/sync.mjs --self-test && node tools/scene/sync.mjs --check' run "chain scene: a push paints with the document hidden and no animation frame (the blank /live of 7 Oct 2026; known-failed first)" node tools/scene/paint-test.cjs - run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs - run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh + run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs tools/scene/legend.test.mjs tools/scene/shard-words.test.mjs + run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright; one retry)" bash tools/ci/retry-once.sh scene-parity bash tools/scene/parity-remote.sh run "no text overlaps: every served page at 390 to 1600 px, light and dark, the hero at each step (self-test first; IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs)" overlap_sweep - run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/api/public-stats.test.mjs + run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/lib/leaderboard.test.mjs site/api/public-stats.test.mjs run "ship tool self-test" node tools/ship-app.mjs --self-test run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs run "launch gates: every row with its check, the handoff text clean (self-test, then the tree)" bash -c 'node tools/ci/launch-gates-check.mjs --self-test && node tools/ci/launch-gates-check.mjs' - run "income per tier: the public table equals its inputs, the schedule arithmetic" bash -c 'node tools/launch/income-tiers.mjs --check && node --test tools/launch/income-tiers.test.mjs' + run "income per tier: the public table equals its inputs, the schedule arithmetic" bash -c 'node tools/launch/income-tiers.mjs --check && node --test tools/launch/income-tiers.test.mjs && node tools/launch/income-page.mjs --check' run "hash-origin report: a known-finished day and a known-failed day" node --test tools/observer/hash-origin.test.mjs run "harness summaries never carry a raw 64-hex key (the writer's own redaction and check)" node infra/fast-time/lib/redact-keys.mjs --self-test run "docs-only pushes skip the compile-or-compute CI jobs (the changes job's classifier)" bash tools/ci/docs-only-check.sh --self-test @@ -175,6 +187,33 @@ deferred_merge() { # [repo dir] -> "defer /dev/null || stat -c %Y "$f") )); [ "$age" -le 43200 ] || { echo "full the stamp for ${p2:0:8} is $age s old"; return; } echo "defer $p2" } +# Master takes only what CI has already passed (standing rule, 7 October 2026, 17:2x UK: era-vdf's tip 0e2d6b1c was merged with +# no ci run at all and master's igneum-pow suite stayed red for 40 minutes under docs-only merges). For a push to master the hook +# asks tools/ci/ci-state.mjs: a two-parent merge needs a green run on its SECOND parent (the branch's own run on the exact +# commit), a plain commit needs a green run on itself (a fast-forward of a branch CI passed); anything else is refused with the +# run's state, and the lane uses tools/ci/merge-to-master.sh, which waits for a queued run. gh unreachable = refused (unknown). +# release-* branches keep the full local gate alone (the shipper's cuts carry their own box suite line). +master_ci_ok() { # -> 0 and a line, or 1 and the reason + local lsha="$1" rsha="$2" parents p2 want line state + parents=$(git rev-list --parents -n 1 "$lsha" 2>/dev/null | cut -d' ' -f2-); set -- $parents + if [ -n "${2:-}" ] && [ -z "${3:-}" ] && [ "$1" = "$rsha" ]; then want="$2"; else want="$lsha"; fi + line=$(node "$GATE_ROOT/tools/ci/ci-state.mjs" "$want" 2>&1); state="${line%% *}" + if [ "$state" = success ]; then echo " master takes ${want:0:8}: ci $line"; return 0; fi + echo "pre-push gate: REFUSED. master takes only a commit whose own ci run is green on that exact commit; ${want:0:8} is: $line" >&2 + echo " Use tools/ci/merge-to-master.sh (it pushes the branch for a run when there is none, waits for a queued run and refuses a red)." >&2 + return 1 +} +master_rule_binds() { # : 0 when the CI rule applies to this push (a GitHub remote, no declared exception), 1 with a printed line otherwise + local url="${1:-}" + if [ -n "${IGNEUM_MASTER_EXCEPTION:-}" ]; then echo " EXCEPTION to the CI rule for this push, declared by main: $IGNEUM_MASTER_EXCEPTION (the local gate is the verdict)"; return 1; fi + case "$url" in *github.com*) return 0 ;; esac + echo " the remote ${url:-?} is not GitHub (a mirror): the CI rule binds GitHub's master; the local gate is the verdict here"; return 1 +} +branch_red_line() { # : the branch's newest completed ci run, when red, printed before the light gate (nothing on green or no gh) + local line; line=$(node "$GATE_ROOT/tools/ci/ci-state.mjs" --branch-red "$1" 2>/dev/null) || return 0 + case "$line" in previous\ CI\ red*) echo " $line" ;; esac + return 0 +} finish() { local what="$1" secs=$(( $(date +%s) - T0 )) if [ "$RED" = 0 ]; then echo "pre-push gate ($what): GREEN, $N checks in ${secs}s"; [ "${STAMP:-0}" = 1 ] && stamp_green; exit 0; fi @@ -198,6 +237,7 @@ case "$MODE" in # the light gate carries the two never-push classes beside the structural checks, and the full gate runs them too declare -f never_push_checks | grep -q 'tools/ci/no-secrets-check.sh' || { echo "self-test failed: the never-push checks do not run the no-secrets check"; fails=1; } declare -f never_push_checks | grep -q 'tools/ci/identity-check.sh' || { echo "self-test failed: the never-push checks do not run the identity grep"; fails=1; } + declare -f never_push_checks | grep -q 'tools/ci/founder-strings-check.sh' || { echo "self-test failed: the never-push checks do not run the founder-strings check"; fails=1; } grep -qE '^\s+structural_checks; never_push_checks; finish "feature branch"' "$0" || { echo "self-test failed: the hook's light gate does not run the never-push checks"; fails=1; } # the green stamp and the deferral, in a fixture repository: a merge of a stamped branch onto the remote tip defers; an # unstamped branch, a stale stamp, a merge onto an older tip and a plain commit all take the full gate @@ -259,6 +299,16 @@ FAKEGH if [ "$which" = full ]; then # a merge of a green-stamped branch onto the exact remote tip goes through on the light gate (CI runs the full one) verdict=""; while read -r lref lsha rref rsha; do case "$rref" in refs/heads/master|refs/heads/release-*) verdict=$(deferred_merge "$lsha" "$rsha"); break ;; esac; done <<<"$REFS" + # a push to master: the pushed commit (or its branch parent) must already have a green ci run on that exact commit. CI runs on + # GitHub, so the rule binds a push whose remote is github.com; a push of master to a box mirror (build@:/srv/igneum.git) + # takes the local gate as before. IGNEUM_MASTER_EXCEPTION="" lifts the CI rule for one push and is printed with + # the push (7 October 2026, 19:5x UK: the GitHub account suspended, lanes landing on the box mirror's master by main's ruling, + # the box gate stamp as the verdict; GitHub gets the fast-forward when it answers again). + if master_rule_binds "${2:-}"; then + while read -r lref lsha rref rsha; do + if [ "$rref" = refs/heads/master ] && [ "$lsha" != 0000000000000000000000000000000000000000 ]; then master_ci_ok "$lsha" "$rsha" || exit 1; fi + done <<<"$REFS" + fi case "$verdict" in defer*) echo "pre-push gate: a merge of green-stamped ${verdict#defer } onto the remote tip: the light gate here, the full gate in CI on landing:" structural_checks; never_push_checks; finish "merge of a green branch (full gate deferred to CI)" ;; @@ -267,6 +317,7 @@ FAKEGH esac else echo "pre-push gate: a feature branch, the light gate (the two structural checks, the no-secrets check, the identity grep):" + while read -r lref lsha rref rsha; do case "$rref" in refs/heads/*) branch_red_line "${rref#refs/heads/}" ;; esac; done <<<"$REFS" structural_checks; never_push_checks; finish "feature branch" fi ;; ci|local)