diff --git a/tools/ci/github-suspended b/tools/ci/github-suspended new file mode 100644 index 000000000..44dd7c48b --- /dev/null +++ b/tools/ci/github-suspended @@ -0,0 +1,4 @@ +# GitHub is unreachable for this repository: the igneum-labs account was suspended on 7 October 2026 at 18:02 UK (every call 403). +# While this file exists, tools/ci/merge-to-master.sh refuses a GitHub remote before any gh or git call and the pre-push hook refuses any +# push to a GitHub remote; lanes land on the box mirror (--remote box or MERGE_REMOTE=box). Main removes this file at the cut-over. +suspended-since: 2026-10-07T17:02:00Z diff --git a/tools/ci/merge-to-master.sh b/tools/ci/merge-to-master.sh index f94c5392b..1cebd10de 100755 --- a/tools/ci/merge-to-master.sh +++ b/tools/ci/merge-to-master.sh @@ -25,6 +25,16 @@ while [ $# -gt 0 ]; do case "$1" in --tries) TRIES="$2"; shift 2 ;; --ci-wait) C # ruling of 7 October 2026, 19:5x UK). CI runs on GitHub only, so the CI rule binds the origin remote; on a mirror the box gate # stamp is the verdict and the merge message says so. IGNEUM_MASTER_EXCEPTION, when set, is printed by the hook with the push. remote_is_github() { case "$(git remote get-url "$REMOTE" 2>/dev/null)" in *github.com*) return 0 ;; *) return 1 ;; esac; } +# While tools/ci/github-suspended exists (the account suspension, 7 October 2026), a GitHub remote is refused before any gh or git call: +# at 03:39 UK on 8 October a lane's default-remote call polled GitHub's CI 21 times (403 each) before it was killed by pid. The marker +# is tracked and dated; main removes it at the cut-over. IGNEUM_GITHUB_SUSPENDED_FILE overrides the path (the self-test). +SUSPENDED_FILE="${IGNEUM_GITHUB_SUSPENDED_FILE:-$ROOT/tools/ci/github-suspended}" +github_suspended_refusal() { # : prints the refusal and returns 0 when the remote is GitHub and the marker stands + [ -f "$SUSPENDED_FILE" ] || return 1 + case "$(git remote get-url "$1" 2>/dev/null)" in *github.com*) ;; *) return 1 ;; esac + echo "merge-to-master: REFUSED. GitHub is unreachable ($(grep -E '^suspended-since' "$SUSPENDED_FILE" | head -1); tools/ci/github-suspended stands): nothing is pushed, fetched or polled there. Land on the box mirror: tools/ci/merge-to-master.sh --remote box (or MERGE_REMOTE=box). Main removes the marker at the cut-over." >&2 + return 0 +} CI_STATE="${CI_STATE_CMD:-node tools/ci/ci-state.mjs}" # the self-test swaps in a fake clock() { TZ=Europe/London date '+%H:%M %Z'; } @@ -75,6 +85,13 @@ mirror_master() { # if [ "$SELF_TEST" = 1 ]; then fails=0; d=$(mktemp -d); fake="$d/ci-state.sh" + # the suspension marker: a GitHub remote is refused with the line, exit 2, before anything else; a mirror remote is not; no marker, no refusal + printf 'suspended-since: 2026-10-07T17:02:00Z\n' > "$d/marker" + out="$(IGNEUM_GITHUB_SUSPENDED_FILE="$d/marker" bash "$0" --remote origin 2>&1)"; rc=$? + [ "$rc" = 2 ] || { echo "self-test failed: a GitHub remote under the marker was not refused with exit 2 (exit $rc): $out"; fails=1; } + case "$out" in *"REFUSED. GitHub is unreachable"*"--remote box"*) ;; *) echo "self-test failed: the refusal did not name the switch: $out"; fails=1 ;; esac + ( SUSPENDED_FILE="$d/marker"; github_suspended_refusal box >/dev/null 2>&1 ) && { echo "self-test failed: the box mirror remote was refused under the marker"; fails=1; } + ( SUSPENDED_FILE="$d/no-marker"; github_suspended_refusal origin >/dev/null 2>&1 ) && { echo "self-test failed: a GitHub remote was refused without the marker"; fails=1; } # the fake answers from $d/answer- (one line per call, consumed top to bottom; the last line repeats) and $d/answer-master cat > "$fake" <<'FAKE' #!/usr/bin/env bash @@ -134,9 +151,10 @@ success 4 u push run REMOTE=origin; remote_is_github || { echo "self-test failed: origin was not read as GitHub (is origin's URL github.com?)"; fails=1; } ( cd "$d/src" && git remote add mirror "$d/mirror.git" ) 2>/dev/null; REMOTE=mirror; ( cd "$d/src" && remote_is_github ) && { echo "self-test failed: a bare-path mirror remote was read as GitHub"; fails=1; }; REMOTE=origin rm -rf "$d" - [ "$fails" = 0 ] && echo "self-test passed: the CI rule binds a GitHub remote only; a landed master is fast-forwarded to every mirror and a refused mirror push is a line, not a failure; a green branch run merges; a red one is refused; an unknown read is waited through and refused only at the deadline; a queued run is waited for with the clock; an unrun branch is pushed once and then waited for; a red master refuses every merge but the declared fix" + [ "$fails" = 0 ] && echo "self-test passed: a GitHub remote is refused with exit 2 while the suspension marker stands and a mirror remote is not; the CI rule binds a GitHub remote only; a landed master is fast-forwarded to every mirror and a refused mirror push is a line, not a failure; a green branch run merges; a red one is refused; an unknown read is waited through and refused only at the deadline; a queued run is waited for with the clock; an unrun branch is pushed once and then waited for; a red master refuses every merge but the declared fix" exit $fails fi +github_suspended_refusal "$REMOTE" && exit 2 # before any gh or git call [ -z "$(git status --porcelain --untracked-files=no)" ] || { echo "merge-to-master: the tree has uncommitted tracked changes; commit first" >&2; exit 1; } bash tools/ci/gh-account-check.sh || exit 1 # gh's active account on this Mac is the stored Igneum entry (main's rule, 7 October 2026, 21:5x UK) SHA=$(git rev-parse "$BRANCH"); G=$(cd "$(git rev-parse --git-common-dir)" && pwd -P) diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index 554d31288..db6fb4093 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -206,6 +206,13 @@ master_ci_ok() { # -> 0 and a line, or 1 and the reas 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 } +github_suspended() { # : 0 with a printed refusal when the marker tools/ci/github-suspended stands and the remote is GitHub + local f="${IGNEUM_GITHUB_SUSPENDED_FILE:-$GATE_ROOT/tools/ci/github-suspended}" + [ -f "$f" ] || return 1 + case "${1:-}" in *github.com*) ;; *) return 1 ;; esac + echo "pre-push gate: REFUSED. GitHub is unreachable ($(grep -E '^suspended-since' "$f" | head -1); tools/ci/github-suspended stands): no push, fetch or poll there. Push to the box mirror (git push box ...; landings: tools/ci/merge-to-master.sh --remote box). Main removes the marker at the cut-over." >&2 + return 0 +} 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 @@ -288,6 +295,14 @@ FAKEGH rm -rf "$fx" "$fakebin" # the CI rule binds a GitHub remote; a mirror remote and a declared exception take the local gate, each with a printed line master_rule_binds https://github.com/igneum-network/igneum.git >/dev/null || { echo "self-test failed: the CI rule did not bind a GitHub remote"; fails=1; } + # the suspension marker: a GitHub remote is refused with the line, a mirror remote is not, no marker no refusal + mk=$(mktemp); printf 'suspended-since: 2026-10-07T17:02:00Z\n' > "$mk" + out=$(IGNEUM_GITHUB_SUSPENDED_FILE="$mk" github_suspended https://github.com/igneum-network/igneum.git 2>&1) || { echo "self-test failed: a GitHub remote under the marker was not refused"; fails=1; } + case "$out" in *"REFUSED. GitHub is unreachable"*"--remote box"*) ;; *) echo "self-test failed: the refusal did not name the switch: $out"; fails=1 ;; esac + ( IGNEUM_GITHUB_SUSPENDED_FILE="$mk" github_suspended ssh://build@188.40.146.49/srv/igneum.git >/dev/null 2>&1 ) && { echo "self-test failed: a mirror remote was refused under the marker"; fails=1; } + ( IGNEUM_GITHUB_SUSPENDED_FILE="$mk.none" github_suspended https://github.com/x/y.git >/dev/null 2>&1 ) && { echo "self-test failed: a GitHub remote was refused without the marker"; fails=1; } + rm -f "$mk" + grep -qE 'github_suspended "\$\{2:-\}" && exit 1' "$0" || { echo "self-test failed: the hook does not refuse a GitHub push under the marker"; fails=1; } master_rule_binds build@188.40.146.49:/srv/igneum.git >/dev/null && { echo "self-test failed: the CI rule bound a box mirror remote"; fails=1; } ( IGNEUM_MASTER_EXCEPTION="main, 7 Oct 2026 19:5x UK: GitHub suspended" master_rule_binds https://github.com/x/y.git >/dev/null ) && { echo "self-test failed: a declared exception did not lift the CI rule"; fails=1; } out=$(IGNEUM_MASTER_EXCEPTION="ruling text" master_rule_binds https://github.com/x/y.git); case "$out" in *"EXCEPTION"*"ruling text"*) ;; *) echo "self-test failed: the exception was not printed with its ruling: $out"; fails=1 ;; esac @@ -298,6 +313,7 @@ FAKEGH hook) # gh's active account on this Mac is the stored Igneum entry, before any push (main's rule, 7 October 2026, 21:5x UK; tools/ci/gh-account-check.sh) bash "$GATE_ROOT/tools/ci/gh-account-check.sh" || exit 1 + github_suspended "${2:-}" && exit 1 # the suspension marker: no push to GitHub at all (8 October 2026, 03:39 UK: 21 polls of a 403 before a kill by pid) REFS="$(cat)"; which="$(printf '%s\n' "$REFS" | gated_refs)" 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)