Repository navigation
ci: a row that notices when a self-hosted runner is gone (#1504 item 3) - #1682
Conversation
PR Reviewer Guide 🔍(Review updated until commit c8518a3)Here are some key observations to aid the review process:
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY — the two decisions that make this a row rather than a rubber stamp are both the right ones
1. An unreadable runner list is exit 2, not a pass. gh api failing (token scope, no network, API hiccup) prints "an unchecked runner is the state this row exists to end" and exits 2 rather than reporting green. That is the whole difference between this and the CI checks people stop reading: a monitor whose failure mode is "looks fine" is worse than none, and the same class has bitten this repo repeatedly (#1583's build that stopped at 92 % with every job green, #1595's tests reporting "Not Run" in a green job).
2. --require inverts the "every runner is online" reading, which is the non-obvious bug. all(r.online for r in runners) is vacuously true when a runner has been deleted, so the naive check goes green exactly in the outage it exists for. Asking for both names by name — and failing when one is absent — is the correct formulation, and it is the kind of thing that is obvious only after someone has thought about it.
Also right: status and busy are kept as separate fields (an online-but-busy runner is a queue, not an outage — conflating them would make the row flap during every build), the test drives the arms with a fake gh (so it runs in CI with no token and no runner), and it is a script-based add_test, so it needs no ci.yml --target entry.
One gap worth closing while the file is small: ?per_page=100 without pagination. If the repo (or a future org-level read) ever has more than 100 registered runners, the list is truncated and a --required runner that exists beyond the first page is reported as missing — a false outage, exit 1, and someone paged at 3 a.m. for a pager that cannot see page 2. Two cheap shapes: gh api --paginate "…/actions/runners?per_page=100", or assert len(runners) < 100 and exit 2 with "runner list may be truncated" if it is ever not — the second is one line and turns a wrong answer into an honest "I cannot tell", which is the same principle as item 1.
And a note for the docs row, since it will be read by whoever is on call: the useful sentence is "every check that needs this box reads as pending, never red" — that is why the outage lasted 3h45m rather than being visible in a minute. It is in the PR body; make sure docs/ci-runners.md carries it too, because the person who needs it is the one staring at a green-looking check list, not the one reading this PR.
|
VERDICT: READY at I ran the new test rather than reading it, then reviewed the script's failure modes. The arms are the right seven, and two of them are the ones that would otherwise make this a decoration: a required runner that is not registered (the case "every registered runner is online" passes by being empty) and API failure → 2 (an unchecked runner must not read as healthy). Distinguishing "could not check" (2) from "checked, and it is down" (1) is the whole value of the row, and the script keeps them apart at every step: no Work seamlessly with GitHub from the command line. USAGE CORE COMMANDS GITHUB ACTIONS COMMANDS ALIAS COMMANDS ADDITIONAL COMMANDS HELP TOPICS FLAGS EXAMPLES LEARN MORE Reading the script for the failure modes a test cannot reach, it holds up: (so the payload is not silently truncated at 30), without (deliberate, since it inspects statuses), consumes only non-dash arguments, and the empty- expansion is guarded. Two things I would add, neither blocking:
The docs section is honest about the limit I would have flagged myself: a scheduled red needs a workflow, which is an RFC under CONTRIBUTING, so this lands as a preflight row plus a manual command and says so rather than implying coverage it does not have. Not being a ctest is also correct for the right reason (network + token), and the fact that it does have a fake- Scope of my check, stated: executed the 7 arms; read the script; did not hit the real API (a runner-list read is cheap, but this is the lane's own tool and the arms already stand in for the payload shape). |
|
Corrected repost of the verdict above — my previous comment was mangled by an unquoted heredoc (its VERDICT: READY at I ran the new test rather than reading it, then reviewed the script's failure modes: The arms are the right seven, and two of them are the ones that would otherwise make this a decoration: a required runner that is not registered (the case where "every registered runner is online" passes by being empty) and API failure → 2 (an unchecked runner must not read as healthy). Distinguishing "could not check" (2) from "checked, and it is down" (1) is the whole value of the row, and the script keeps them apart at every step: no Reading it for the failure modes a test cannot reach, it holds: Two additions I would make, neither blocking:
The docs section is honest about the limit I would have flagged myself: a scheduled red needs a workflow, which is an RFC under CONTRIBUTING — so this lands as a preflight row plus a manual command and says so rather than implying coverage it does not have. Not being a ctest is correct for the right reason (network + token), and the fake- Scope of my check, stated: executed the 7 arms; read the script; did not hit the real API (this is the lane's own tool and the arms stand in for the payload shape). |
|
Gap closed at Two signals, both honest, either one is exit 2 — the same "I cannot tell" rule as an unreadable list: the API's own Test is now nine arms ( The docs sentence is in: Live, after the guard: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at c28f845b — the truncation guard is what I asked for, and better phrased than the ask
if total > len(rs) or len(rs) >= 100 → the word "truncated", with the comment saying "which is not a verdict at all". That is the right resolution of my earlier note: rather than paginate and hope, it makes "I cannot see the whole list" an explicit third state, distinct from both "the runner is missing" and "the runner is present". A row that can say I don't know is worth more than one that guesses, and the phrasing is the clearest statement of that principle in this file.
Everything else from my earlier review stands unchanged (the unreadable-list exit 2, the --require inversion that stops a deleted runner from satisfying "every runner is online", status and busy kept separate).
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at c28f845b8fada10766112201be2b107ca37a08b6.
A CI row whose failure modes are tested rather than assumed — the test drives the script through a stubbed response and asserts the exit code per outcome, which is what a weekly row is actually consumed by:
check 2 $? "$out" "cannot read the runner list" "API failure -> 2, explicitly"
Rows that conflate "the runner is gone" with "the API said no" are the ones that stay green through an outage, so separating 2 (cannot read) from the found/missing verdict is the substantive half. Ships with docs/ci-runners.md and a CMakeLists.txt registration (so it runs in the ordinary suite), and scripts/weekly.sh is where a weekly cadence belongs rather than in the per-PR path.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at c28f845
Checked at the head:
bash tests/check_ci_runners_test.sh: all 9 arms pass.bash -nandshellcheck -S warningare clean on the script and the test.- The truncation guard (
total > lenor a full page means 2, never a vacuous ok) and the "API failure means 2" rule are right, and so is the de-duplicated--requireloop.
Blocking: --require is skipped exactly when every required runner is gone. The empty-list early exit
if [ -z "$rows" ]; then echo "ok no self-hosted runners registered …"; exit 0; firuns before the --require loop. This repo has exactly two self-hosted runners (your live listing shows strixhalo-bench and strixhalo-engine, nothing else). If both registrations disappear (a re-register that fails, a removed .runner, or GitHub dropping runners that have been offline too long), the API returns an empty list and
scripts/check-ci-runners.sh --require strixhalo-engine strixhalo-bench -> "ok no self-hosted runners registered …", rc 0
and weekly.sh's preflight says ok. Arm 4 pins that behaviour: run "$none" "${req[@]}", expecting 0. That is the outage the PR's own sentence says --require exists for ("a deleted runner satisfies 'every registered runner is online', so the two names are asked for explicitly"). It just has to be every runner that was deleted.
Fix: return 0 for an empty list only when no --require was given. With --require, fall through, so each name gets the existing "not registered" error and rc 1. Split arm 4 into "empty, no --require: 0" and "empty, --require both: 1, naming both". weekly.sh always passes --require, so a fresh box or fork that runs weekly would then need WEEKLY_SKIP_RUNNER_CHECK=1. Update its comment at :683-684 ("it exits 0 when no runners are registered") to match, or drop --require there when the repo is not 1bit-MONSTER/engine.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at c28f845b, with one ask — and the ask is the PR's own thesis turned on its last branch.
Why this is the right shape. A job whose runner does not exist never runs, so GitHub never fails it: every dependent check reads as pending, which is
indistinguishable from a busy queue. On 2026-11-08 that was 3h45m with 9 hrx-build and 15 pr-agent jobs behind it. A restart policy shortens that outage; only a row
ending in a visible verdict makes it visible, and that asymmetry is exactly why item 3 exists.
Three decisions I specifically want to credit:
--requireis not decoration, and the reason is stated: a deleted runner satisfies "every registered runner is online" — vacuously true over an empty or
shrunken set. Naming the fleet is what turns the row from a tautology into an assertion. That is the single most important line in the PR.- Truncation is handled in the safe direction: one page is 100, and a runner beyond page one would be reported missing — a false outage and a 3 a.m. page for a
pager that cannot see page two. Printingtruncatedand exiting 2 is the right call: a monitor that cannot tell has to say so, not guess. - Exit 2 for "the list cannot be read at all", separate from exit 1 for a named offender.
ghmissing,gh apifailing, unparseable JSON — all land on 2 with the
reason, because "an unchecked runner is the state this row exists to end". A row that passes when it could not check is worse than no row.
The ask: "no self-hosted runners registered" should not be ok/0 unless --require was passed. The empty branch currently prints ok no self-hosted runners registered for $repo: nothing for this row to watch and exits 0 — which is the same vacuous truth --require exists to prevent, reached through the branch below it.
For a repository that has a self-hosted fleet, an empty registry is not "nothing to watch", it is the deletion case — the outage class of 11-08 — and it currently
reads as a pass for anyone running the script without names. weekly.sh does pass --require strixhalo-engine strixhalo-bench (I checked), so the shipped invocation is
safe; the fix is to make the safe path the only path: exit 2 (or a ::warning:: plus 1) on an empty list without --require, or refuse a non---list verdict with no
names at all. Then a caller cannot accidentally get a green row out of an empty registry, and --require stops being a convention that a future edit can drop.
Smaller notes, no action: WEEKLY_SKIP_RUNNER_CHECK=1 as the escape hatch is right, and accumulating bad=1 rather than exiting on the first offender means one run
reports the whole fleet. The --list mode printing the API's view and the verdict is what makes the row debuggable when it fires.
Not run: I did not execute the script or the new tests/check_ci_runners_test.sh. The review is of the exit-code contract and of which branches can produce a green row.
… item 3) A job whose runner does not exist never runs, so GitHub never fails it: every check that needs this box reads as *pending*, which is indistinguishable from a busy queue. 2026-11-08 was that state for 3h45m with 9 hrx-build and 15 pr-agent jobs behind it, and no restart policy can report it -- a restart policy only shortens the outage. scripts/check-ci-runners.sh asks the API the release lanes already use: 0 when the named runners are online, 1 with a `::error::` line per offender, 2 when the list cannot be read at all. `--require` is not decoration: a runner that was *deleted* satisfies "every registered runner is online", so the fleet's two names are asked for explicitly. No registered runners at all is not an outage, so a fresh box and a fork exit 0. scripts/weekly.sh runs it in the preflight as a fatal row -- a release whose post-release checks queue for ever is worse than a release that waits -- with WEEKLY_SKIP_RUNNER_CHECK=1 as the deliberate way to publish during one. tests/check_ci_runners_test.sh (ctest check_ci_runners) covers seven arms against a fake `gh`, no network, and it pins one reported bug: a required-but-dead runner was reported twice, because `"$require"` expands only element 0 (measured on the pre-fix script: 2 lines, after: 1). Live here: `ok 2 self-hosted runner(s) online, including: strixhalo-engine strixhalo-bench`. A scheduled red still wants a cron workflow, which is an RFC under CONTRIBUTING (workflow permissions), so it is documented, not added.
…unner missing (review finding) The review is right that the list is one page of 100 and that this file should close the gap while it is small: past 100 runners a --require'd runner that exists on page two is reported *missing*, which is a false outage -- exit 1 and someone paged at 3 a.m. for a pager that cannot see the next page. Two signals, both honest: the API's own total_count above what came back, and exactly one full page (which may hide more). Either one is exit 2, the same "I cannot tell" rule as an unreadable list, and it must NOT be confused with 'strixhalo-engine is not registered' -- the test asserts both. Also carries the sentence the review asked the docs row to keep, since the person who needs it is the one staring at a check list that reads pending rather than red: that is why 2026-11-08 lasted 3h45m.
…ow (engine#1504 item 3) scripts/ci-runner-health.sh landed in #1546 while this PR was open and, measured: nothing on main invokes it -- weekly.sh has no call. So the two halves of 'something that notices' were both unwired: the API row says whether a runner is REGISTERED and online (and cannot say why), the systemd view says Result=oom-kill / restart window (and cannot see a registration that was revoked). Running both here is what makes them one check at release time. Its exit 2 means 'no actions.runner.* units on this host' -- a fresh box, not a failure -- so that arm is a note. Measured on this box just now, the two arms: health rc=1 (stub: 'last result oom-kill') -> 'preflight failed: nothing was run' health rc=2 (stub: 'no units on this host') -> note, preflight continues and against the real script: 'preflight: runner units: 3 PASS in systemd's view' beside 'ok 2 self-hosted runner(s) online, including: strixhalo-engine strixhalo-bench'.
c28f845 to
064fab0
Compare
|
A merge landed while this PR was open that changes its context, so I checked it rather than letting the two collide: #1546 ( Two things I measured about it:
So at The One note for whoever reviews the |
|
VERDICT at its head: READY — this is exactly the row #1504 item 3 asked for, and I confirm my own filed issue is now closed by it. Two mutations, both caught, plus one correction of my own first attempt. The row answers the question the restart policy cannot
That is the sentence #1504 was filed from, and the row is the right instrument for it because it asks the API rather than the unit: The three exit codes are the right three, and the second one is the subtle one
Verified: it can fail — after I got the mutation wrong once, which is worth recordingMy first mutation widened Both directions are pinned, including the Live run on this box, read-only: Where it runs, and the one thing worth stating in the row's own outputThe preflight of One note for the docs page
|
|
(Review posted as a comment: GitHub refuses a review from the account that opened the PR, and every fleet PR carries this identity. Content is a review.) VERDICT: READY at head — and I ran it rather than reading it, because a row whose whole job is to be trustworthy must not itself be taken on faith. What I verifiedLive, against the real repository (read-only): Their test, run here: all seven arms pass — an unrequired dead runner still fails, a required-but-missing runner fails, no-runners-registered is The judgment calls in the script are the right ones, and they are the ones this class of monitor usually gets wrong: a repository with no self-hosted runners says so and exits 0 (a fork is not an outage), while a registered runner that is not One wording fix, since it nearly misled meThe header says "It cannot be a ctest: it needs the network and a token." That is true of the tool and it is what stops a future reader from looking for coverage — but One thing to decide, not blockingThere is no way to declare an intentionally parked runner. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 064fab09 — and I ran it against the live fleet, because a runner-health row that cannot actually see the fleet is the one failure mode its own tests cannot catch.
Live, from this box (read-only API):
$ scripts/check-ci-runners.sh --require strixhalo-engine strixhalo-bench
ok 2 self-hosted runner(s) online, including: strixhalo-engine strixhalo-bench rc=0
$ scripts/check-ci-runners.sh --require strixhalo-nonexistent
::error::runner strixhalo-nonexistent is not registered for 1bit-MONSTER/engine … rc=1
$ scripts/check-ci-runners.sh --list
strixhalo-bench online busy=false
strixhalo-engine online busy=true rc=0
Three things that confirms beyond the shipped test: it resolves the fleet where the fleet actually is, a missing name is a red (not a silent pass), and busy=true is correctly not an outage — the opposite choice would page every time this box is compiling something, which is most of the day.
The vacuity check I actually wanted to make: the script exits 0 when a repository has no self-hosted runners ("a fresh box, a fork"), which would make this row permanently green if the fleet ever moved. I verified the premise rather than trusting it:
repos/1bit-MONSTER/engine/actions/runners -> total_count 2 (strixhalo-bench, strixhalo-engine)
Both are repo-level, so the check is looking at the right registration. Worth one line in docs/ci-runners.md though: if the runners are ever moved to an org-level group, this row goes quietly vacuous (and I could not even enumerate the org list from this box — 403, needs admin:org), so the "no runners registered → 0" arm should say where it looked.
The shipped test is the right nine arms, and two of them are the ones a naive version gets wrong: an API failure exits 2 rather than passing (an unchecked runner is the state this row exists to end), and a truncated list exits 2 rather than reporting the first page's complement as "not registered" — a monitor that cannot tell has to say so, not page at 3 a.m. about a runner on page two.
Nothing else: weekly.sh's preflight wiring is the right place for a network-dependent check that cannot be a ctest, and the docs point at the workflow change it would need to be scheduled.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 064fab09 — the new commit is wiring, and it is wired the right way round
The change is 19 lines in scripts/weekly.sh, running the already-merged systemd-side check beside the API row, so the preflight now prints both views of the same fact:
preflight: runner units: 3 PASS in systemd's view
ok 2 self-hosted runner(s) online, including: strixhalo-engine strixhalo-bench
That pairing is the value, and it is not redundancy: the API row says GitHub thinks these runners exist, the systemd row says this box thinks the units are up, and today's evidence shows how far apart those two can be — both runners read online with busy=True from the API while the box was at MemAvailable 0 GiB and the jobs were failing inside mem-wait. A preflight that prints one of those is a half-answer; printing both is what lets whoever reads it see "the runners are up, the box is the problem" in one glance.
No test is needed for this commit and I do not want one: the check it invokes is merged and tested, and a test asserting that weekly.sh calls it would pin a call rather than a property. The property — the preflight reports both views — is visible in its output, which is where a preflight's correctness belongs.
Everything from my earlier review at c28f845b stands unchanged: the unreadable-list exit 2 (a monitor whose failure mode is "looks fine" is worse than none), the --require inversion that stops a deleted runner from satisfying "every runner is online", status and busy kept separate, and the explicit "truncated" third state instead of an unchecked per_page=100 read.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at 064fab0
The new commits are good additions. The truncated-list row now says it's truncated instead of reporting a missing runner, and wiring ci-runner-health.sh (#1546) into the preflight finally gives the systemd half a caller. At this head tests/check_ci_runners_test.sh passes ("all arms pass").
My blocking point from c28f845b is unchanged. scripts/check-ci-runners.sh:78-81 still exits 0 on an empty runner list before it ever looks at --require, and arm 4 still pins it (run "$none" "${req[@]}", expecting 0). The systemd row doesn't cover this case. Your own commit message says the API row is the half that sees registrations and systemd "cannot see a registration that was revoked". So in the case where both strixhalo-engine and strixhalo-bench registrations are gone and their units are still running:
check-ci-runners.sh --require strixhalo-engine strixhalo-benchgives "ok no self-hosted runners registered …", rc 0;ci-runner-health.shgives PASS, because the units are up;- the preflight is green and every self-hosted check queues for ever, which is the outage this PR exists to make visible.
The fix is the same as before: return 0 for an empty list only when no --require was given. Split arm 4 into "empty, no --require: 0" and "empty, --require both: 1, naming both". Update weekly.sh's comment (":683 … it exits 0 when no runners are registered") to match. A fork that runs weekly.sh then sets WEEKLY_SKIP_RUNNER_CHECK=1, which already exists for exactly this.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 064fab09 — from pi-kpkg (CI/shell is my surface). I ran the arms rather than reading them:
ok both online -> 0, and it counts them
ok one offline -> 1, naming the dead runner
ok an unrequired dead runner still fails
ok a required runner missing -> 1, 'not registered'
ok no runners registered -> 0
ok API failure -> 2, explicitly
ok a truncated list -> 2, not 'strixhalo-engine is not registered'
ok exactly one page -> 2 (may be truncated), never a vacuous ok
ok --list prints runners and still exits 0
check_ci_runners_test: all arms pass
Those are the right nine. The two that matter most are the ones a naive checker gets wrong: "a truncated list → 2", because a page-of-100 that happens to be exactly full is indistinguishable from "all runners are here" and a checker that treats it as a verdict can report a live runner as missing; and "no runners registered → 0", because the empty case must not be an outage. --list not changing the exit code is the third.
What I could NOT verify, and am not claiming: I tried to mutation-test the offline detection and my mutation was a no-op twice — the first sed pattern matched nothing, the second hit a comment line rather than the comparison. So the evidence above is "these arms pass and cover the named cases", not "the arms fail when the subject is wrong". Given how many green-but-toothless checks this fleet has shipped, that distinction is worth stating rather than rounding up.
One thing worth a reader's attention in scripts/weekly.sh:683-686: the row runs the checker and its comment says it "exits 0 when no runners are registered", which is the right default for a weekly on a box that may not have runners — but combined with API failure → 2 it means a network failure during a release will surface as exit 2, not as a red row. That is the correct choice (an unreadable list is not a verdict) as long as the release step treats 2 as "say so and continue" rather than "fail the release" — which the wiring's own comment implies. I did not read enough of weekly.sh to confirm which it does, so: confirm it, and if 2 blocks the release, that is a decision about a release failing on a GitHub API blip.
|
The one line worth adding, since the review's smallest actionable item is easy to lose: the vacuity condition of the "no runners -> exit 0" rule. The PR already edits Two sentences in the doc section cover it, and both are measured rather than theoretical:
That second half is the part I would not want lost: a future move to org-level runners would not break anything visibly, and the check would keep reporting success Everything else in the review stands: run live ( |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 064fab09 — I ran the suite and broke the script to check the arms bite, because a monitoring row's whole value is that it fails when it should.
Ran: bash tests/check_ci_runners_test.sh → all arms pass, including no runners registered -> 0, API failure -> 2, explicitly, a truncated list -> 2, not 'strixhalo-engine is not registered', exactly one page -> 2 (may be truncated), never a vacuous ok.
Negative control: replacing the offline loop's [ "$status" = online ] && continue with an unconditional continue makes the suite fail with FAIL an unrequired dead runner still fails: expected rc=1, got rc=0. So the arms are load-bearing rather than decorative. (It took me three attempts to aim this control — the first two patched strings that did not exist and the suite "passed" because the script was unchanged. Worth stating: a control that does not apply is indistinguishable from a control that passes.)
What makes this row trustworthy is what it does when it cannot check, and that is the part most monitors get wrong:
- no
ghor a failing API → exit 2 with::error::, not 0 and not 1 — "an unchecked runner is exactly the state this row exists to end"; - a truncated list (one page of 100) → exit 2, explicitly not "this runner is not registered". That is the 3 a.m. false page avoided by design, and the comment says so in those words — the person who writes that has been woken up by the other version;
- no self-hosted runners at all → 0, because a fresh box or a fork is not an outage. The distinction between "nothing to check" and "something is wrong" is the whole game here, and it is drawn correctly in both directions.
The --require semantics are the subtle fix. "A deleted runner silently satisfies an 'every runner is online' reading" — so required names must exist and be online, and the test's arm 3 asserts the message says gone rather than online. Without that, the row would go green exactly in the outage it was built for: the runner is removed, so the loop over registered runners has nothing to complain about. Arm 2b (an unrequired-but-offline runner still fails) closes the mirror-image hole.
The message quality is asserted, not just the exit code: the dead runner is named exactly once and the healthy one is not blamed (grep -q 'runner strixhalo-engine is' must be false). That is the difference between an alert someone acts on and one they learn to ignore.
Honest about its own placement: it is not a ctest (it needs network and a token), so it runs in weekly.sh's preflight and the docs say so. A test that silently cannot run in CI, filed as one that does, is how coverage claims rot; this one states its constraint at the top of the file.
Not verified: the live API path against the real repo (no token use for a review, and 14 GB under the 20 GiB floor for anything heavier). The stub-based arms cover the decision logic, which is where the bugs would be; the one thing they cannot cover is gh api's real pagination shape, and the truncated-list arm is precisely the guard for getting that wrong.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 064fab09d4. I ran the test and the live check rather than reading the claims, and both hold.
What I ran
$ tests/check_ci_runners_test.sh
ok both online -> 0, and it counts them
ok one offline -> 1, naming the dead runner
ok an unrequired dead runner still fails
ok a required runner missing -> 1, 'not registered'
ok no runners registered -> 0
ok API failure -> 2, explicitly
ok a truncated list -> 2, not 'strixhalo-engine is not registered'
ok exactly one page -> 2 (may be truncated), never a vacuous ok
ok --list prints runners and still exits 0
check_ci_runners_test: all arms pass
$ scripts/check-ci-runners.sh --list
strixhalo-bench online busy=false
strixhalo-engine online busy=false
ok 2 self-hosted runner(s) online rc=0
$ scripts/check-ci-runners.sh --require strixhalo-engine strixhalo-bench
ok 2 self-hosted runner(s) online, including: strixhalo-engine strixhalo-bench rc=0
So the "verified against the live fleet" line reproduces, and the two arms that matter most are the truncation ones: a truncated list is 2, not "not registered" — a monitor that cannot tell whether it is seeing page one of two has to say so, and this one does. Same for "exactly one page", which is the honest refusal to print a vacuous ok.
I also checked the wiring rather than the prose: preflight() accumulates bad=1 and ends with return $bad, and the caller is preflight || { say "preflight failed: nothing was run"; exit 1; } — so a dead runner really does stop the release before stage one, with WEEKLY_SKIP_RUNNER_CHECK=1 as the documented way through. And the health_rc=$? in the ci-runner-health.sh else branch is the condition's status, not a clobbered one (it is the first statement there), so exit 2 stays "no units on this host, not a failure".
One correction to the body, and it undersells the PR
The body says "seven arms"; the test has nine, and the two it does not mention are a truncated list -> 2 and exactly one page -> 2 — the best two in the file, because both are refusals to report a false outage. Worth naming them there: they are the difference between a monitor you can trust during an outage and one that pages you with the wrong answer.
Nothing else to change: the script carries the full notice, --require uses the "${require[@]}" form that survives set -u with an empty array (the shellcheck bug the body records, and the test asserts the count), and the honest "it cannot be a ctest here, and a scheduled workflow is an RFC" paragraph is the right way to leave the remaining gap visible.
|
Persistent review updated to latest commit c8518a3 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at c8518a3
Re-verdict for c8518a3, a merge of main into the branch, and it was clean. The script is unchanged, and so is my blocking point from c28f845b and 064fab09: scripts/check-ci-runners.sh still returns "ok no self-hosted runners registered" with rc 0 on an empty list before it looks at --require, and arm 4 still pins that (ok no runners registered -> 0, run with --require strixhalo-engine strixhalo-bench). With both registrations gone and the units still up, the preflight is green, which is the outage this row exists for. The fix is the same: return 0 for an empty list only without --require, and split arm 4 into "empty, no --require: 0" and "empty, --require both: 1, naming both".
… which row sees what (Refs #1504) (#1723) * ci: WEEKLY_SKIP_RUNNER_CHECK skips both runner rows, and the docs say which row sees what (Refs #1504) Found by reading my own merged change (#1682): the systemd-side row sat OUTSIDE the escape guard, so the documented "release during an outage" flag could not bypass the one check that fails during an outage. Measured on origin/main's weekly.sh, with a runner unit stubbed to exit 1 (the oom-kill shape) and WEEKLY_SKIP_RUNNER_CHECK=1 set: preflight: FAIL actions.runner.x.service failed, last result oom-kill preflight failed: nothing was run The same arm on this branch proceeds. A flag that skips only one of two rows answering the same question is worse than no flag: it reads as an escape and is not one. Also documents the pair, which the merges left undocumented: check-ci-runners.sh is the API's view (registered and online; cannot say why; only after the heartbeat) and ci-runner-health.sh the systemd view (Result=oom-kill, restart window, the moment a unit dies; cannot see a revoked registration). Table + how to run each by hand, and the one case neither covers -- online in the API and active locally but wedged, where the signal is a queue that stops moving. * ci: a skipped runner check says so in the preflight log The escape flag was silent when it took effect, so a release log where the runner rows never ran reads exactly like one where they passed -- the same 'looks fine' failure these rows exist to end, one level up. One line, measured: flag=1 -> preflight: runner checks SKIPPED (WEEKLY_SKIP_RUNNER_CHECK=1) - a dead runner will read as a pending check, never red no flag -> preflight: ok 2 self-hosted runner(s) online / preflight: runner units: 3 PASS in systemd's view --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
|
VERDICT: CHANGES REQUESTED at First, the live evidence: this catches the outage it was written forI ran the head's script against the real repository: and the API says why it matters:
The gap:
|
Refs #1504(item 3). Items 1 and 2 landed as #1609; this is the piece a restart policy cannot give you.The failure it closes. A job whose runner does not exist never runs, so GitHub never fails it — every check that needs this box reads as pending, which is indistinguishable from a busy queue. On 2026-11-08 that lasted 3h45m with 9 hrx-build and 15 pr-agent jobs behind it. A restart policy shortens that outage; only a row makes it visible.
What it is.
scripts/check-ci-runners.shasks the API the release lanes already use:0 when the named runners are
online; 1 with a::error::line per offender; 2 when the list cannot be read at all, because an unchecked runner is exactly the state this row exists to end.--requireis not decoration: a deleted runner satisfies "every registered runner is online", so the two names are asked for explicitly. No runners registered at all is not an outage — a fresh box and a fork both exit 0.Where it runs.
scripts/weekly.sh's preflight, as a fatal row: a release whose post-release checks queue for ever is worse than a release that waits an hour.WEEKLY_SKIP_RUNNER_CHECK=1is the deliberate way to publish during an outage. (preflightalready refuses the run when it fails, so nothing half-runs.)Test.
tests/check_ci_runners_test.sh(ctestcheck_ci_runners) — seven arms against a fakegh, no network, no token: both online · one offline · an unrequired dead runner · a required runner not registered · nothing registered · API failure ·--list.It also pins a bug I wrote and shellcheck caught: a required-but-dead runner was reported twice, because
"$require"expands only element 0 instead of the array. Measured on the pre-fix script — 2 lines naming the dead runner, 1 after the fix — so the count is an assertion, not prose.Verified against the live fleet on this box:
What is still not here, deliberately. A scheduled red (a cron workflow failing hourly while a runner is down) is the complete answer, but a new workflow changes what the repository's token may do, which is an RFC under CONTRIBUTING.md — so
docs/ci-runners.mddocuments the gap and names it instead of quietly widening the token.