live-22: the gate script is master's own plus the legend, shard-words and leaderboard test files (the release branch's copy had carried a line for a file master does not hold)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a18382ca12
commit
c41feaab33
1 changed files with 76 additions and 16 deletions
|
|
@ -41,12 +41,25 @@ run() {
|
|||
}
|
||||
run_quiet() { "$@" >/dev/null 2>&1; }
|
||||
|
||||
# In place ONLY inside GitHub Actions (a disposable checkout); a `--ci` run on a lane's Mac builds in the temporary copy like the hook,
|
||||
# because the in-place build rewrote four tracked site files (bench.html, index.html, journey.html, journey.json) in the running
|
||||
# worktree and every lane had to discard them before a merge (the horizon lane, 7 October 2026). --self-test proves the tree is
|
||||
# unchanged after a site build outside Actions.
|
||||
site_in_place() { [ "$MODE" = ci ] && [ "${GITHUB_ACTIONS:-}" = true ]; }
|
||||
site_build() {
|
||||
if [ "$MODE" = ci ]; then node site/build.mjs; return; fi
|
||||
if site_in_place; 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)
|
||||
}
|
||||
|
||||
overlap_sweep() {
|
||||
# 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
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -61,15 +74,17 @@ never_push_checks() {
|
|||
}
|
||||
|
||||
tree_checks() {
|
||||
run "site build (in a temporary copy locally, in place in CI)" site_build
|
||||
run "site build (in a temporary copy here, in place only inside GitHub Actions)" 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 "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)" bash -c 'node tools/ci/site-nav-check.mjs --self-test && node tools/ci/site-nav-check.mjs'
|
||||
run "vendor marks: site/lib/marks.mjs is brand/marks/vendor-marks.mjs byte for byte (the app and the site draw one set)" cmp brand/marks/vendor-marks.mjs site/lib/marks.mjs
|
||||
run "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)" node tools/site/sheet-test.mjs --self-test
|
||||
run "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)" bash -c 'node tools/ci/padding-check.mjs --self-test && node tools/ci/padding-check.mjs --site "${SITE_TMP:-.}/site"'
|
||||
run "ledger sentences present verbatim on their public pages" node tools/ci/ledger-text-check.mjs
|
||||
never_push_checks
|
||||
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 "the prover pair is built from the pinned node's exec types (prover-pair-check)" bash -c 'bash tools/ci/prover-pair-check.sh --self-test && bash tools/ci/prover-pair-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'
|
||||
|
|
@ -79,7 +94,6 @@ tree_checks() {
|
|||
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 "no permanent miner fault in the engine (miner-faults.md MF-2)" bash -c 'bash tools/ci/permanent-fault-check.sh --self-test && bash tools/ci/permanent-fault-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
|
||||
|
|
@ -89,6 +103,8 @@ 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 "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 "the simulators job runs on master and release-* pushes and pull requests into them only" bash tools/ci/sims-branch-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
|
||||
|
|
@ -101,19 +117,16 @@ tree_checks() {
|
|||
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 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)" 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/lib/leaderboard.test.mjs site/api/public-stats.test.mjs
|
||||
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
||||
run "interface bundle publisher self-test (pack, and sign when the signer is built)" node tools/ui-ota/publish.mjs --self-test
|
||||
run "heat gate reader self-test (a known hold passes, a known drift fails)" node tools/heat-gate.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 app/igneum-app/ui/ui-ota.test.mjs app/igneum-app/ui/fold.test.mjs
|
||||
run "the Intel rotate-fold rewrite: Intel gets the shift form, AMD and NVIDIA untouched (proto-opencl/test_intel_rotr.c)" bash -c 'cc -std=c99 -Wall -Wextra -o "${TMPDIR:-/tmp}/test_intel_rotr.$$" proto-opencl/test_intel_rotr.c && "${TMPDIR:-/tmp}/test_intel_rotr.$$"; s=$?; rm -f "${TMPDIR:-/tmp}/test_intel_rotr.$$"; exit $s'
|
||||
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 "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 "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
|
||||
}
|
||||
|
||||
gated_refs() {
|
||||
|
|
@ -126,9 +139,32 @@ gated_refs() {
|
|||
[ "$full" = 1 ] && echo full || echo light
|
||||
}
|
||||
|
||||
# The green stamp (main, 7 October 2026, the push-race class: a 100 s gate on the merge commit against a master that moves every
|
||||
# minute starved every merge, six rejections in a row). A full gate that ends GREEN over a CLEAN tree records the commit it ran on
|
||||
# in .git/igneum-gate-green/<sha> (the repository's own .git, shared by its worktrees). The hook then lets a MERGE commit through
|
||||
# on the light gate when its first parent is exactly the remote tip being replaced (nothing unknown underneath) and its second
|
||||
# parent carries a stamp younger than 12 hours: the branch's own gate was green on that commit, master's tip was green by its
|
||||
# CI, and CI runs the same full gate on the merge the moment it lands (ci.yml), which is the backstop for the union.
|
||||
# tools/ci/merge-to-master.sh is the merge tool that uses it; its merge message names the stamped commit.
|
||||
stamp_dir() { local g; g=$(git rev-parse --git-common-dir 2>/dev/null) || return 1; ( cd "$g" 2>/dev/null && printf '%s/igneum-gate-green' "$(pwd -P)" ); } # absolute: a worktree's answer is relative
|
||||
stamp_green() { # [repo dir]: after a GREEN full gate; only when no tracked file differs from HEAD (a dirty tree would lie)
|
||||
local d="${1:-.}" sha dir
|
||||
[ -z "$(git -C "$d" status --porcelain --untracked-files=no 2>/dev/null)" ] || return 0
|
||||
sha=$(git -C "$d" rev-parse HEAD 2>/dev/null) || return 0; dir=$(cd "$d" && stamp_dir); mkdir -p "$dir" 2>/dev/null || return 0
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ > "$dir/$sha" 2>/dev/null && echo " (green stamp recorded for ${sha:0:8})"
|
||||
}
|
||||
deferred_merge() { # <local sha> <remote sha> [repo dir] -> "defer <branch sha>" or "full <reason>"
|
||||
local lsha="$1" rsha="$2" d="${3:-.}" parents p1 p2 dir f age
|
||||
parents=$(git -C "$d" rev-list --parents -n 1 "$lsha" 2>/dev/null | cut -d' ' -f2-) || { echo "full unknown commit"; return; }
|
||||
set -- $parents; p1="${1:-}"; p2="${2:-}"; [ -n "$p2" ] && [ -z "${3:-}" ] || { echo "full not a two-parent merge"; return; }
|
||||
[ "$p1" = "$rsha" ] || { echo "full first parent ${p1:0:8} is not the remote tip ${rsha:0:8}"; return; }
|
||||
dir=$(cd "$d" && stamp_dir); f="$dir/$p2"; [ -f "$f" ] || { echo "full no green stamp for ${p2:0:8}"; return; }
|
||||
age=$(( $(date +%s) - $(stat -f %m "$f" 2>/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"
|
||||
}
|
||||
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
|
||||
if [ "$RED" = 0 ]; then echo "pre-push gate ($what): GREEN, $N checks in ${secs}s"; [ "${STAMP:-0}" = 1 ] && stamp_green; 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
|
||||
}
|
||||
|
|
@ -150,22 +186,46 @@ case "$MODE" in
|
|||
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; }
|
||||
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
|
||||
fx=$(mktemp -d); ( cd "$fx" && git init -q -b master . && git -c user.name=t -c user.email=t@t commit -q --allow-empty -m a ) 2>/dev/null
|
||||
A=$(git -C "$fx" rev-parse HEAD); git -C "$fx" checkout -q -b b; git -C "$fx" -c user.name=t -c user.email=t@t commit -q --allow-empty -m b; B=$(git -C "$fx" rev-parse HEAD)
|
||||
git -C "$fx" checkout -q master; git -C "$fx" -c user.name=t -c user.email=t@t merge -q --no-ff -m "merge b" b; M=$(git -C "$fx" rev-parse HEAD)
|
||||
[ "$(deferred_merge "$M" "$A" "$fx")" = "full no green stamp for ${B:0:8}" ] || { echo "self-test failed: an unstamped branch merge was not sent to the full gate: $(deferred_merge "$M" "$A" "$fx")"; fails=1; }
|
||||
( cd "$fx" && git checkout -q "$B" && stamp_green . >/dev/null && git checkout -q master )
|
||||
[ "$(deferred_merge "$M" "$A" "$fx")" = "defer $B" ] || { echo "self-test failed: a stamped branch merge onto the remote tip was not deferred: $(deferred_merge "$M" "$A" "$fx")"; fails=1; }
|
||||
[ "$(deferred_merge "$M" "$B" "$fx")" = "full first parent ${A:0:8} is not the remote tip ${B:0:8}" ] || { echo "self-test failed: a merge onto another tip was deferred"; fails=1; }
|
||||
[ "$(deferred_merge "$B" "$A" "$fx")" = "full not a two-parent merge" ] || { echo "self-test failed: a plain commit was deferred"; fails=1; }
|
||||
touch -t 202001010000 "$(cd "$fx" && stamp_dir)/$B"; case "$(deferred_merge "$M" "$A" "$fx")" in "full the stamp for ${B:0:8} is "*) ;; *) echo "self-test failed: a stale stamp was honoured"; fails=1 ;; esac
|
||||
( cd "$fx" && echo x > dirty && git add dirty && git -c user.name=t -c user.email=t@t commit -q -m d && echo y > dirty && stamp_green . | grep -q recorded ) && { echo "self-test failed: a dirty tree was stamped"; fails=1; }
|
||||
rm -rf "$fx"
|
||||
# a --ci site build outside GitHub Actions leaves the tracked site files as they are (the horizon lane's four rewritten files)
|
||||
before=$(git status --porcelain -- site | sort); ( MODE=ci GITHUB_ACTIONS= site_build >/dev/null 2>&1 ); after=$(git status --porcelain -- site | sort)
|
||||
[ "$before" = "$after" ] || { echo "self-test failed: a --ci site build outside GitHub Actions changed tracked site files: $(git status --porcelain -- site | tr '\n' ' ')"; fails=1; }
|
||||
MODE=ci GITHUB_ACTIONS= site_in_place && { echo "self-test failed: --ci outside GitHub Actions chose the in-place build"; fails=1; }
|
||||
MODE=ci GITHUB_ACTIONS=true site_in_place || { echo "self-test failed: --ci inside GitHub Actions did not choose the in-place build"; fails=1; }
|
||||
declare -f tree_checks | grep -q 'never_push_checks' || { echo "self-test failed: the full gate does not run the never-push checks"; 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 (structural checks, no-secrets, identity grep)"
|
||||
[ "$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; a merge of a green-stamped branch onto the remote tip defers to CI, every other shape takes the full gate; a --ci site build outside GitHub Actions leaves the tree unchanged (structural checks, no-secrets, identity grep)"
|
||||
exit $fails ;;
|
||||
list)
|
||||
grep -E '^\s+run "' "$0" | sed -E 's/^\s+run "([^"]+)".*/\1/' ;;
|
||||
hook)
|
||||
which="$(gated_refs)"
|
||||
REFS="$(cat)"; which="$(printf '%s\n' "$REFS" | 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-*"
|
||||
# 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"
|
||||
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)" ;;
|
||||
*) echo "pre-push gate: a push to master or release-*, the full gate (the same checks CI runs)${verdict:+ ($verdict)}:"
|
||||
STAMP=1; structural_checks; tree_checks; finish "push to master or release-*" ;;
|
||||
esac
|
||||
else
|
||||
echo "pre-push gate: a feature branch, the light gate (the two structural checks, the no-secrets check, the identity grep):"
|
||||
structural_checks; never_push_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" ;;
|
||||
[ "$MODE" = local ] && STAMP=1; structural_checks; tree_checks; finish "$MODE" ;;
|
||||
*) echo "usage: tools/ci/pre-push.sh [--ci|--hook|--self-test|--list]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
|
|
|||
Loading…
Reference in a new issue