Skip to content

ci: run the merge gate on our own runners, not the contended hosted pool - #418

Open
avrabe wants to merge 3 commits into
mainfrom
ci/gates-on-self-hosted
Open

avrabe wants to merge 3 commits into
mainfrom
ci/gates-on-self-hosted

Conversation

@avrabe

@avrabe avrabe commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

The gate is not slow. It is queued behind GitHub's hosted pool for work our own machine is idle enough to take.

Measured, not assumed

  • Last merge to main (aa48860): 76 jobs, 630 job-minutes, 59 min wall-clock.
  • While writing this, 78 checks were queued at once across two PRs.
  • The thread sanitizer sat QUEUED for over two hours with no runner assigned — while the two self-hosted sanitizer jobs in the same workflow were picked up in three minutes (pulseengine-ci-01-6 and -10, running concurrently).
  • The pool is real: instances -5, -6, -7, -9, -10, -11, -12 have all taken jobs recently. Today 1 of 58 job definitions targets it.

What moves here, and why these

The cheap required gates — that is where the leverage is. Of the 22 contexts main requires, 19 need no container and could move; these nine need nothing but actions/checkout plus cargo or python, both already proven on this runner by fuzz-smoke and sanitizers:

workflow jobs
kill-criteria data-overlap, fixture-freshness, object-freshness, graph-env, required-contexts, wcet-evt, wcet-sidecar, proof-completeness, renode-targets, retry-loops, zephyr-fork-pin (11)
rivet-v-closure guard, v-closure (2)
gust-targets gust-targets (1)

Together they are ~9 job-minutes. That is the point: trivial work sitting in a queue behind 34 Zephyr matrix jobs, so a merge waits hours on gates that run in seconds.

Not moved — each for a stated reason, not by omission

  • mpu-enforcement needs sudo apt-get install qemu-system-arm, and the self-hosted containers have no passwordless sudo. Same class as the deliberately-hosted thread sanitizer (TSan's vm.mmap_rnd_bits sysctl). Stays on ubuntu-24.04 until qemu-system-arm is in the runner image.
  • Rust Coverage and the cross-arch seam gate likewise need apt (curl, binutils-arm-none-eabi).
  • Everything with a container: — 17 jobs including all of Zephyr and LLVM-LTO — is blocked on the podman work on pulseengine-ci-01 (graphroot volume + subuid/newuidmap). That is where 67% of the compute is: Zephyr C Coverage alone is 39 minutes, and the 34 qemu_cortex_m3 jobs are 360 job-minutes between them. Moving those is what would drain the queue; moving these is what stops merges waiting on it meanwhile.

The failure mode this introduces, made visible

Every moved job gains timeout-minutes: 30 where it had none. If the self-hosted pool is ever down, these fail loudly instead of hanging a required context forever. runs-on has no fallback, so that is the tradeoff being taken deliberately rather than discovered later.

🤖 Generated with Claude Code

https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo

MEASURED on the last merge to main (aa48860, 76 jobs): 630 job-minutes,
59 minutes wall-clock. On the two PRs open while writing this, 78 checks
were queued at once and the `thread` sanitizer sat QUEUED for over two
hours with no runner assigned — while the two self-hosted sanitizer jobs
beside it in the same workflow were picked up in three minutes
(pulseengine-ci-01-6 and -10, running concurrently).

So the gate is not slow. It is waiting behind GitHub's hosted pool for
work our own machine is idle enough to take. The pool is real: runner
instances -5, -6, -7, -9, -10, -11 and -12 have all taken jobs recently.

This moves the CHEAP REQUIRED GATES, which is where the leverage is. Of
the 22 contexts main requires, 19 do not need a container and could move;
the nine here need nothing but `actions/checkout` plus cargo or python,
both already proven on this runner by fuzz-smoke and sanitizers:

  kill-criteria    data-overlap, fixture-freshness, object-freshness,
                   graph-env, required-contexts, wcet-evt, wcet-sidecar,
                   proof-completeness, renode-targets, retry-loops,
                   zephyr-fork-pin                                (11 jobs)
  rivet-v-closure  guard, v-closure                                (2 jobs)
  gust-targets     gust-targets                                    (1 job)

Together they are ~9 job-minutes. That is the point: they are trivial work
that has been sitting in a queue behind 34 Zephyr matrix jobs, so a merge
waits hours on gates that run in seconds.

NOT MOVED, and each for a stated reason rather than by omission:

  * `mpu-enforcement` needs `sudo apt-get install qemu-system-arm`, and the
    self-hosted containers have no passwordless sudo. Same reason the
    `thread` sanitizer is deliberately hosted (TSan's vm.mmap_rnd_bits
    sysctl). It stays on ubuntu-24.04 until qemu-system-arm is in the
    runner image.
  * `Rust Coverage` and the cross-arch seam gate likewise need apt
    (curl, binutils-arm-none-eabi).
  * Everything with a `container:` — 17 jobs including ALL of Zephyr and
    LLVM-LTO — is blocked on the podman work on pulseengine-ci-01
    (graphroot volume + subuid/newuidmap). That is where 67% of the compute
    is: Zephyr C Coverage alone is 39 minutes and the 34 qemu_cortex_m3
    jobs are 360 job-minutes between them. Moving those is what would
    actually drain the queue; moving these is what stops merges waiting on
    it meanwhile.

Every moved job gains `timeout-minutes: 30` where it had none, so if the
self-hosted pool is ever down these fail loudly instead of hanging a
required context forever — the failure mode this change introduces, made
visible rather than left implicit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo
@avrabe

avrabe commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

[fathom (gale) — it works, with an A/B inside a single run, and one honest wrinkle]

kill-criteria run 35832723443, created 07:37:53Z. Twelve minutes later:

job runner wait run
a Rocq proof that is merely STATED cannot pass pulseengine-ci-01-5 10.8 min 0.2 min ✅
VER-OS-WCET-001 kill-criterion can still fail pulseengine-ci-01-5 11.1 min 0.2 min ✅
every renode_test target defined is actually run pulseengine-ci-01-7 10.9 min 0.2 min ✅
committed Renode ELF fixtures are not older… pulseengine-ci-01-10 11.2 min 0.3 min ✅
no CI retry loop can swallow its own failure pulseengine-ci-01-5 11.3 min 0.2 min ✅
a denied write really faults (left on ubuntu-24.04) UNASSIGNED >12 min still queued

The last row is the control, and it is in the same run as the others: the one job I deliberately did not move — because it needs sudo apt-get install qemu-system-arm — is the one still waiting for a runner. Earlier today a hosted job (thread sanitizer) sat queued for over two hours.

The gates take twelve seconds. That is the whole finding. Work measured in seconds was waiting behind a queue measured in hours.

The wrinkle, because it changes the next ask

Our own pool is not instant either: ~11 minutes of queue before pickup. Only four instances took work in this run — -5 (4 jobs), -10 (3), -6 (1), -7 (1) — although -9, -11 and -12 have taken jobs on other days. So the effective width right now is about four, and with 14 jobs moved here that is ~3 waves.

So this PR converts hours of hosted queue into minutes of our own queue, which is the win, but the residual 11 minutes is pool width and not something the repo can fix. Two levers, both on the host:

  1. More runner instances online — the cheapest way to cut the 11 minutes, and it needs no repo change.
  2. 67% of CI compute cannot move to our own runners: 17 container: jobs blocked on podman-in-podman on pulseengine-ci-01 #419's podman work — unrelated to this wait, but it is what lets the 67% of compute (Zephyr) leave the hosted pool at all.

I would rather state the 11 minutes than quietly present the 12-second run time as the result.

@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

avrabe and others added 2 commits September 23, 2026 20:05
`zephyr-fork-pin` failed on the self-hosted runner with exit 127,
`gh: command not found` — the rust-cpu image has no GitHub CLI. Ordinary,
loud, fixed by moving it back to a hosted runner until gale#419 puts `gh`
in the image.

`required-contexts` is the one worth reading twice. It also shells out to
`gh api`, and it **passed** on the self-hosted runner — because it
DEGRADES rather than fails when branch protection cannot be read:

    if gh api ".../branches/main/protection" > /tmp/protection.json; then
      readable=true    # checks the committed list against what main REQUIRES
    else
      readable=false   # checks only the committed list against the workflows
    fi

With no `gh` binary the else branch runs, the list-vs-protection drift
check never happens, and a REQUIRED context reports success having done
half of what it claims. Green, for a reason that does not hold — in a job
whose entire purpose is "every required context can actually report".

The degradation itself is right, and deliberate: the comment above it says
the run should say so "instead of passing as though it had checked", and it
does print the reason. What it does not do is distinguish A TOKEN THAT MAY
NOT READ from AN IMAGE THAT HAS NO BINARY. The first is an expected
permission boundary worth degrading for; the second is a broken
environment, and degrading for it silently narrows a gate.

So both are handled:
  * the job moves back to ubuntu-24.04, so the FULL check runs today, and
  * a `command -v gh` guard now fails loudly before the branch, so the
    missing-binary case can never again take the permission path — on any
    runner, including a hosted one that changes under us.

That guard is the part that outlives gale#419. When `gh` lands in the
image both jobs move back, and if it ever disappears again the gate says so
instead of quietly checking less.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011QG86sovTbfnPNY9SfhSmo

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant