From 74bb841dfe713f932b0f8bb9f3ecdae748f7c8a8 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:04:22 +0000 Subject: [PATCH] merge-to-master: an unknown CI read is waited through inside the deadline, not refused at once (7 October 2026, 18:0x UK: 500s on pushes, a 404 on a live run id, then the account suspension; the rule refuses only a verdict, and a missing verdict only at the deadline) ci_gate: unknown prints the reason and asks again until --ci-wait runs out (CI_WAIT_SECS for the self-test). master_gate: three reads over a minute before an unknown counts; a lasting unknown still refuses (no verdict, no merge). Self-test: a blip (unknown, then green) merges; a lasting unknown is refused at the deadline; the same two shapes for master's read. Co-Authored-By: Claude Fable 5.1 --- tools/ci/merge-to-master.sh | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tools/ci/merge-to-master.sh b/tools/ci/merge-to-master.sh index 5983a239..1e7bd890 100755 --- a/tools/ci/merge-to-master.sh +++ b/tools/ci/merge-to-master.sh @@ -27,13 +27,14 @@ clock() { TZ=Europe/London date '+%H:%M %Z'; } # waits on a queued or running run; refuses (1) a red or unknown one. master_gate: refuses (1) when master's last compiled run is red. ci_gate() { local sha="$1" branch="$2"; shift 2; local line state deadline pushed=0 - deadline=$(( $(date +%s) + CI_WAIT_MIN * 60 )) + deadline=$(( $(date +%s) + ${CI_WAIT_SECS:-$(( CI_WAIT_MIN * 60 ))} )) while :; do line=$($CI_STATE "$sha" 2>&1); state="${line%% *}" case "$state" in success) echo "merge-to-master: ci on ${sha:0:8} is green: $line"; return 0 ;; none) if [ "$pushed" = 1 ]; then echo "merge-to-master: waiting for a ci run to appear on ${sha:0:8} ($(clock))"; else echo "merge-to-master: no ci run on ${sha:0:8} yet; pushing $branch so CI runs it ($(clock))"; "$@" || { echo "merge-to-master: the branch push failed; CI cannot run ${sha:0:8}" >&2; return 1; }; pushed=1; fi ;; pending) echo "merge-to-master: ci on ${sha:0:8} is ${line#* * * }; waiting ($(clock))" ;; + unknown) echo "merge-to-master: ci on ${sha:0:8} could not be read (${line#* * * }); GitHub or gh wobbling, asking again ($(clock))" ;; # 7 Oct 2026 18:0x UK: 500s on pushes, a 404 on a live run id *) echo "merge-to-master: REFUSED. ci on ${sha:0:8} is $line" >&2; echo " A branch whose own ci run is not green never merges (CI red is stop-the-line). Fix the branch, push a new commit, and run this again." >&2; return 1 ;; esac [ "$(date +%s)" -lt "$deadline" ] || { echo "merge-to-master: REFUSED. ci on ${sha:0:8} gave no verdict in $CI_WAIT_MIN minutes ($line); run it again when the queue drains (gh run list --branch $branch --limit 3)" >&2; return 1; } @@ -41,7 +42,12 @@ ci_gate() { done } master_gate() { - local line state; line=$($CI_STATE --master-code 2>&1); state="${line%% *}" + local line state try + for try in 1 2 3; do # an unknown read (gh or GitHub wobbling) is asked again, three times over a minute, before it counts + line=$($CI_STATE --master-code 2>&1); state="${line%% *}" + [ "$state" = unknown ] && [ "$try" -lt 3 ] && { echo "merge-to-master: master's verdict could not be read (${line#* * * }); asking again ($(clock))"; sleep "${CI_POLL_SECS:-30}"; continue; } + break + done case "$state" in success|none|pending) echo "merge-to-master: master's last compiled run: $line"; return 0 ;; *) if [ "$FIXES_MASTER" = 1 ]; then echo "merge-to-master: master's last compiled run is $state and this merge is declared the fix (--fixes-master): $line"; return 0; fi @@ -67,9 +73,10 @@ FAKE printf 'failure 2 u push run: igneum-pow tests at "igneum-pow tests (release)" ' > "$d/answer-red" ci_gate red b false >/dev/null 2>&1 && { echo "self-test failed: a red branch run merged"; fails=1; } - printf 'unknown - - gh: exit 1 -' > "$d/answer-unk" - ci_gate unk b false >/dev/null 2>&1 && { echo "self-test failed: an unknown verdict merged"; fails=1; } + printf 'unknown - - gh: exit 1\n' > "$d/answer-unk" + CI_WAIT_SECS=1 ci_gate unk b false >/dev/null 2>&1 && { echo "self-test failed: a lasting unknown verdict merged"; fails=1; } + printf 'unknown - - gh: HTTP 404\nsuccess 9 u push run\n' > "$d/answer-blip" + ci_gate blip b false >/dev/null 2>&1 || { echo "self-test failed: a GitHub blip (unknown, then green) was refused instead of waited through"; fails=1; } printf 'pending 3 u queued since 16:00 UK pending 3 u in_progress since 16:00 UK success 3 u push run at 16:05 UK @@ -91,8 +98,12 @@ success 4 u push run printf 'success 6 u master @e5171a32, compile job success ' > "$d/answer-master"; rm -f "$d/i-master" FIXES_MASTER=0 master_gate >/dev/null || { echo "self-test failed: a green master refused a merge"; fails=1; } + printf 'unknown - - gh: HTTP 404\nsuccess 7 u master, compile job success\n' > "$d/answer-master"; rm -f "$d/i-master" + FIXES_MASTER=0 master_gate >/dev/null || { echo "self-test failed: an unknown master read that turned green refused a merge"; fails=1; } + printf 'unknown - - gh: exit 1\n' > "$d/answer-master"; rm -f "$d/i-master" + FIXES_MASTER=0 master_gate >/dev/null 2>&1 && { echo "self-test failed: a lasting unknown master read let a merge through"; fails=1; } rm -rf "$d" - [ "$fails" = 0 ] && echo "self-test passed: a green branch run merges; a red or unknown one is refused; 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 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 [ -z "$(git status --porcelain --untracked-files=no)" ] || { echo "merge-to-master: the tree has uncommitted tracked changes; commit first" >&2; exit 1; }