the project lead, 7 October 2026 15:02 UK: build-1 at load 139 / 114 / 90 with both slots held and a 1 h 40 min queue while build-2 read 4.5 with free slots, because the class router pinned each class to its box. Now lib.sh bs_route_spill reads the preferred box (free slots, 1-minute load) with one ssh and hands the job to the other box when the preferred one has no free slot or sits above load 64 and the other qualifies; neither qualifying queues on the class's own box. The decision travels as BR_ROUTE_* into the JSONL "route" object for the dashboard. build-2's slots file reads 3; everything on box 2 runs at nice 10 / 32 cores / -j 32, and a bounded run takes the band its slot owns so three never share a core. Self-test tools/ci/route-spill-check.sh (thirteen cases) in the gate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
146 lines
12 KiB
Bash
Executable file
146 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# The one gate. Every fast tree check CI runs, in one script, so the local gate and CI can never drift: the `site` job
|
|
# of .github/workflows/ci.yml calls `tools/ci/pre-push.sh --ci`, and the pre-push hook (tools/ci/install-hooks.sh) calls
|
|
# `tools/ci/pre-push.sh --hook` before any push to master or a release-* branch and refuses the push on red.
|
|
#
|
|
# tools/ci/pre-push.sh # the full gate over this working tree (any worktree; reads the tree, writes nothing in it)
|
|
# tools/ci/pre-push.sh --ci # the same, with the site built in place (a CI checkout is disposable)
|
|
# tools/ci/pre-push.sh --hook # from .git/hooks/pre-push: full gate for master and release-*, the two structural
|
|
# # checks (conflict markers, Windows paths) for every other ref
|
|
# tools/ci/pre-push.sh --self-test # the runner fires on a known failure, passes a known success, and the hook picks the
|
|
# # right gate from the ref lines
|
|
# tools/ci/pre-push.sh --list # the check names, one per line
|
|
#
|
|
# What is NOT here, on purpose: the three compile-or-compute jobs (igneum-pow tests, igneum-census, the simulators) and
|
|
# the live public API check (master only, a network call), which stay separate steps in ci.yml. Everything here finished
|
|
# in under 25 s on the Mac on 6 October 2026 (no-secrets 10 s, everything else under 3 s each).
|
|
#
|
|
# Local mode never writes into the worktree: the site is built in a temporary copy with SITE_DOWNLOADS_OFFLINE=1
|
|
# (063bbca, 6 October 2026: a hook that built in place rewrote the downloads snapshot in five worktrees). The link, ledger
|
|
# and identity checks then read the committed pages; CI builds in place and checks the rebuilt pages, the one difference.
|
|
set -uo pipefail
|
|
cd "$(git rev-parse --show-toplevel)" || exit 1
|
|
# git hands a hook its own repository through the environment (GIT_DIR, GIT_INDEX_FILE, GIT_PREFIX, ...). Left in place,
|
|
# every nested git inside the self-tests (remote-run.sh builds a mirror and pushes into it) would act on THIS repository
|
|
# 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#--}"
|
|
RED=0; N=0; LOG="$(mktemp)"; trap 'rm -rf "$LOG" "${SITE_TMP:-}"' EXIT
|
|
T0=$(date +%s)
|
|
|
|
run() {
|
|
# run <name> <command...>: one line per check; the output of a red check is shown in full
|
|
local name="$1"; shift; N=$((N + 1))
|
|
local s=$(date +%s)
|
|
if "$@" >"$LOG" 2>&1; then
|
|
printf ' ok %3ds %s\n' "$(( $(date +%s) - s ))" "$name"
|
|
else
|
|
printf ' RED %3ds %s\n' "$(( $(date +%s) - s ))" "$name"; sed 's/^/ /' "$LOG" | cut -c1-240; RED=1
|
|
fi
|
|
}
|
|
run_quiet() { "$@" >/dev/null 2>&1; }
|
|
|
|
site_build() {
|
|
if [ "$MODE" = ci ]; then node site/build.mjs; return; fi
|
|
SITE_TMP="$(mktemp -d)"; cp -R site "$SITE_TMP/site"
|
|
(cd "$SITE_TMP/site" && SITE_DOWNLOADS_OFFLINE=1 node build.mjs)
|
|
}
|
|
|
|
structural_checks() {
|
|
run "no conflict markers in tracked files" bash tools/ci/no-conflict-markers.sh
|
|
run "every tracked path is valid on Windows (colon, trailing dot, reserved names, length)" bash tools/ci/windows-paths-check.sh
|
|
}
|
|
|
|
tree_checks() {
|
|
run "site build (in a temporary copy locally, in place in CI)" site_build
|
|
run "internal link check of site/*.html" node tools/ci/link-check.mjs
|
|
run "every served page carries the full header (six items, Download, burger)" node tools/ci/site-nav-check.mjs
|
|
run "ledger sentences present verbatim on their public pages" node tools/ci/ledger-text-check.mjs
|
|
run "identity grep of the public export list and the served site" bash tools/ci/identity-check.sh
|
|
run "shell inside .github/workflows parses (bash -n, the PowerShell 5.1 rule)" node tools/ci/check-workflow-shell.mjs
|
|
run "PowerShell drive-reference check (\$name: in a double-quoted string)" bash tools/ci/ps-drive-ref-check.sh
|
|
run "copied sources are re-stamped before a build" bash tools/ci/copied-sources-check.sh
|
|
run "override params files parse with no duplicate key" bash tools/ci/override-json-check.sh
|
|
run "second-engine playbooks log to a file and end their tree (C35)" bash tools/ci/second-engine-check.sh
|
|
run "no playbook quits, pauses or resumes the installed app" bash -c 'bash tools/ci/playbook-quit-check.sh --self-test && bash tools/ci/playbook-quit-check.sh'
|
|
run "no script writes into another worktree or walks Projects" bash -c 'bash tools/ci/no-foreign-tree-writes.sh --self-test && bash tools/ci/no-foreign-tree-writes.sh'
|
|
run "the signer is never piped into head" bash tools/ci/signer-pipe-check.sh
|
|
run "bash bodies in PowerShell job scripts pass bash -n" bash -c 'bash tools/ci/bash-body-check.sh --self-test && bash tools/ci/bash-body-check.sh'
|
|
run "run jobs test their fetched kit before use" bash -c 'bash tools/ci/kit-path-check.sh --self-test && bash tools/ci/kit-path-check.sh'
|
|
run "every Windows spawn of the app runs with a hidden console" bash -c 'node tools/ci/windows-spawn-check.mjs --self-test && node tools/ci/windows-spawn-check.mjs'
|
|
run "pinned guest programs match their manifest" bash tools/ci/pinned-guests-check.sh
|
|
run "root prover playbooks kill the GPU server and unlink its socket" bash tools/ci/prover-socket-check.sh
|
|
run "commit-string gate self-test" bash tools/ci/commit-string-check.sh --self-test
|
|
run "build server remote checkout self-test" bash infra/build-server/remote-run.sh --self-test
|
|
run "a slot holder keeps its own line for the whole run (the watcher-trust rule)" bash infra/build-server/remote-run.sh --self-test-keeper
|
|
run "the remote checkout resets the mirror's tree before the branch checkout (the stale-overlay class)" bash -c 'bash tools/ci/mirror-reset-check.sh --self-test && bash tools/ci/mirror-reset-check.sh'
|
|
run "the remote checkout's clean spares a lane's scratch (.igneum-scratch-spare, the fixed prefixes, never -x; the lost-scratch class)" bash -c 'bash tools/ci/scratch-spare-check.sh --self-test && bash tools/ci/scratch-spare-check.sh'
|
|
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 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 "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
|
|
run "faucet unit tests" node --test site/api/faucet.test.mjs
|
|
run "redesign package data tests (the /api/live contract the pages read)" node tools/site-redesign/tests/data-tests.cjs
|
|
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 "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.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 "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 "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 "hash-origin report: a known-finished day and a known-failed day" node --test tools/observer/hash-origin.test.mjs
|
|
}
|
|
|
|
gated_refs() {
|
|
# stdin: the pre-push hook's lines "<local ref> <local sha> <remote ref> <remote sha>". Prints "full" when any remote
|
|
# ref is master or release-*, else "light".
|
|
local lref lsha rref rsha full=0
|
|
while read -r lref lsha rref rsha; do
|
|
case "$rref" in refs/heads/master|refs/heads/release-*) full=1 ;; esac
|
|
done
|
|
[ "$full" = 1 ] && echo full || echo light
|
|
}
|
|
|
|
finish() {
|
|
local what="$1" secs=$(( $(date +%s) - T0 ))
|
|
if [ "$RED" = 0 ]; then echo "pre-push gate ($what): GREEN, $N checks in ${secs}s"; exit 0; fi
|
|
echo "pre-push gate ($what): RED after $N checks in ${secs}s. Fix it before pushing (every check above is one CI runs; the same script runs there)." >&2
|
|
exit 1
|
|
}
|
|
|
|
case "$MODE" in
|
|
self-test)
|
|
fails=0
|
|
st="$(mktemp)" # run in this shell, not a $(...) subshell, so RED is visible here
|
|
run "known failure" false >"$st" 2>&1; out="$(cat "$st")"; case "$out" in *"RED"*"known failure"*) ;; *) echo "self-test failed: a failing check was not reported RED"; fails=1 ;; esac
|
|
[ "$RED" = 1 ] || { echo "self-test failed: a failing check did not set RED"; fails=1; }
|
|
RED=0
|
|
run "known success" true >"$st" 2>&1; out="$(cat "$st")"; rm -f "$st"; case "$out" in *"ok"*"known success"*) ;; *) echo "self-test failed: a passing check was not reported ok"; fails=1 ;; esac
|
|
[ "$RED" = 0 ] || { echo "self-test failed: a passing check set RED"; fails=1; }
|
|
[ "$(printf 'refs/heads/x 1 refs/heads/master 2\n' | gated_refs)" = full ] || { echo "self-test failed: a push to master did not select the full gate"; fails=1; }
|
|
[ "$(printf 'refs/heads/x 1 refs/heads/release-0.3.15 2\n' | gated_refs)" = full ] || { echo "self-test failed: a push to release-0.3.15 did not select the full gate"; fails=1; }
|
|
[ "$(printf 'refs/heads/x 1 refs/heads/x 2\n' | gated_refs)" = light ] || { echo "self-test failed: a push to a feature branch selected the full gate"; fails=1; }
|
|
[ "$(printf '' | gated_refs)" = light ] || { echo "self-test failed: a push with no refs selected the full gate"; fails=1; }
|
|
[ "$fails" = 0 ] && echo "self-test passed: a failing check is RED and fails the gate, a passing one is ok; master and release-* select the full gate, other refs the light one"
|
|
exit $fails ;;
|
|
list)
|
|
grep -E '^\s+run "' "$0" | sed -E 's/^\s+run "([^"]+)".*/\1/' ;;
|
|
hook)
|
|
which="$(gated_refs)"
|
|
if [ "$which" = full ]; then
|
|
echo "pre-push gate: a push to master or release-*, the full gate (the same checks CI runs):"
|
|
structural_checks; tree_checks; finish "push to master or release-*"
|
|
else
|
|
echo "pre-push gate: a feature branch, the two structural checks:"
|
|
structural_checks; finish "feature branch"
|
|
fi ;;
|
|
ci|local)
|
|
[ "$MODE" = ci ] && echo "pre-push gate in CI (the same script as the local hook):" || echo "pre-push gate over this working tree:"
|
|
structural_checks; tree_checks; finish "$MODE" ;;
|
|
*) echo "usage: tools/ci/pre-push.sh [--ci|--hook|--self-test|--list]" >&2; exit 2 ;;
|
|
esac
|