ci: nothing runs on a PR; the fast lane on push to our branches, the full lane where a merge lands - #980
ci: nothing runs on a PR; the fast lane on push to our branches, the full lane where a merge lands#980gafferongames wants to merge 7 commits into
Conversation
The fast lane's first red (run 34600421804, the rust row on PR #949) was the lane's own bug and not the pull request's. The leg-gate case statement DOES carry a rust entry — the `::error::no fixed-form gate is registered` line in the log is the shell echoing the case's unreachable default arm, not a gate that ran. What failed is the override beside it: make tables-rust-fixedform tables-rust-versioning RUSTUP_BIN=/usr/bin copied out of test/conformance/rust/ci.json, where it is right: every target that row names is a `cargo build` and the image's cargo is the one it wants. But make prepends $(RUSTUP_BIN) to PATH around the whole recipe, and `tables-rust-versioning` is a GO TEST that shells out to cargo — so /usr/bin went in front of the Go 1.26 actions/setup-go had just installed and the image's own go answered: go: go.mod requires go >= 1.26 (running go 1.24.13; GOTOOLCHAIN=local) make: *** [make/rust.mk:309: tables-rust-versioning] Error 1 The gate asserted no row of §5 before it died. So the rust row now passes no RUSTUP_BIN at all, exactly as ci-full.yml's rust versioning step does and for the reason that step names: the default is a homebrew keg that does not exist on a runner, prepending a directory that is not there costs nothing, and cargo resolves out of the toolchain the dtolnay/rust-toolchain step installed. internal/ci grows the gate that keeps it: TestNoWorkflowShadowsTheGoToolchain DISCOVERS from make/*.mk every variable a recipe prepends to PATH (today RUSTUP_BIN and BEAM_PATH) and refuses ci-fast.yml handing any of them one of the runner image's own bin directories. make/rust.mk names the same fact where the recipe is. Every other leg row was checked against ci-full.yml and the conformance registry and needs no change: each gate target already depends on build/fixedform-corpus itself, so no row owes an explicit corpus build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…an/ci-merge-only # Conflicts: # .github/workflows/ci-fast.yml
…a merge lands The owner's ruling, 2026-09-11, verbatim: "Here is another option to speed up, check in to branches without PRs. Only run tests when merging into main, local runners elsewhere." The pull request stays the READ SURFACE — the diff, the body, the owed lists, another line's verdict — and costs nothing to keep open, because no workflow of ours is triggered by a pull-request event any more. ci-fast.yml: the trigger is the PUSH to one of our branches (rowan/**, johnny/**, emma/**, freddy/**, stella/**) plus workflow_dispatch. No pull_request trigger at all. Every job stays on [self-hosted, studio], and the fork guard becomes structural rather than a job-level `if:`: the only trigger is a push to a branch of this repository, so no fork's code can reach the pool. ci-full.yml: it runs WHERE A MERGE LANDS. A push to fixed-table-form and a `gh workflow run` dispatch take the studio pool; a push to main and the nightly stay on hosted ubuntu-latest — the one hosted run per main merge, on the x86-64 box we do not own, which is what proves the bytes. The `full-ci` label path remains for an explicit ask but fires on `labeled` ONLY, so it is no longer a plain pull-request trigger. Three rows cannot move and say so in place: big-endian (s390x cross gcc and qemu-user at pinned Ubuntu versions), windows and msvc. Two portability fixes for the pool, which is macOS: `$(nproc)` becomes `$(getconf _NPROCESSORS_ONLN)`, and erlef/setup-beam — which has no macOS support — is gated on `runner.environment == 'github-hosted'`, where dist/ already carries the BEAM on PATH. The rust row's RUSTUP_BIN resolves the conflict between #975 and #974 in favour of both: never an image bin directory (#974's rule, which internal/ci's TestNoWorkflowShadowsTheGoToolchain keeps), and the directory the cargo on PATH is actually in, so the row does not depend on the make default being absent on whichever pool it lands on. docs/CONTRIBUTING.md states the loop in five lines and cites the ruling. The CAA workflow (cla.yml, pull_request_target) is untouched: it is not a test and it is not ours to change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Measured, on this branchWhat ran on the pull-request event: nothing but the CAA.
No The lane needs no change. Verified against this PR with Two findings the pool owes, neither caused by this change
Both belong to the pool's setup rather than to the trigger shape, and both want their own branch. |
…n install on the pool, the siblings go-test-touched was missing #980's first full-lane dispatch on the studio pool went 52 of 72 red (run 34603066277), and the fast lane's go-test-touched went red with it (run 34603030435). Three causes, all of them the warm self-hosted workspace rather than the diff. (1) THE SIBLING CLONES WERE NOT IDEMPOTENT. A self-hosted runner's workspace PARENT persists between runs, and the sibling runtimes the Makefiles reach for as ../serialize* live there. actions/checkout cleans the repository directory; it does not touch the parent. So the second run of any job that clones a sibling dies by name: fatal: destination path 'serialize' already exists and is not an empty directory Every clone step in both workflows now `rm -rf` the target and re-clones at the pinned tag. The rust versioning row's `if [ ! -d ../serialize.rs ]` guard goes too: it did not fail, it REUSED whatever the previous run left, which is how a stale sibling outlives a pin bump. The repository checkout itself is untouched and still clean on every run — nothing here sets `clean: false`. (2) THE CS ROWS, AND THE PIN WAS NEVER THE PROBLEM. The pin (.github/dotnet-version) is the band `10.0`; the Studio has 10.0.400 AND 10.0.401 in ~/.dotnet, both inside it, both complete and both working. The defect is that ci-full.yml's four setup-dotnet steps were not gated to hosted runners, unlike ci-fast.yml's. All six studio runners share one $HOME, so the pool ran six concurrent installs of the same SDK into the same ~/.dotnet, and a job read one of them half-written: the library 'libhostpolicy.dylib' required to execute the application was not found in '/Users/glenn/.dotnet/sdk/10.0.401/' — every cs versioning row failing on a probe that never built. All four steps are now `if: runner.environment == 'github-hosted'`, so the pool installs nothing and uses the SDK already on its PATH. No SDK was installed and the pin did not move: matching the pin is what this does. (3) go-test-touched HAD NO SIBLING CHECKOUT AT ALL. A broad diff hands it `./...`, which reaches every leg's table codegen test, and those generate a probe whose go.mod replaces github.com/mas-bandwidth/serialize.go with ../serialize.go. With no sibling the gate does not skip, it fails by name — internal/codegen/gotable at 190 s: replacement directory .../actions-runner-3/_work/schema/serialize.go does not exist It now clones all six siblings, idempotently, for the same reason the negative-control matrix clones all six: the job's package list is not known when the step is written. Not fixed here, and not ours: the java row's REFUSED BY NAME is the tip's pre-§5 Java leg, which PR #920 carries. docs/CONTRIBUTING.md gains the pool's operating notes — the six runners and their directories, `./svc.sh stop|start` per directory and the rule never to touch them during a benchmark window, the warm-workspace rule with both ways it was earned, and where each toolchain lives on the Studio. Gates: go test ./internal/ci/ green; gofmt clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The owner's ruling, 2026-09-11, verbatim:
This pull request is the shape of that ruling. It also carries the merges of #975 (the fast lane on the six studio self-hosted runners) and #974 (the rust row stops shadowing its own Go toolchain), resolved in favour of both.
What changes
The pull request stays the READ surface and starts nothing. The diff, the body and the owed lists live here; Emma, Johnny and DeepSeek read them. No workflow of ours is triggered by a
pull_requestevent any more, so an open pull request costs zero hosted minutes and zero queue.ci-fast.yml— the push to one of our branches. Trigger:pushtorowan/**,johnny/**,emma/**,freddy/**,stella/**, plusworkflow_dispatch. Nopull_requesttrigger at all. Every job staysruns-on: [self-hosted, studio]and never a hosted runner, so a push to a branch of ours gets a second-opinion fast run on our own hardware for free. The fork guard becomes structural rather than a job-levelif:— the only trigger is a push to a branch in this repository, so no fork's code can reach the pool.ci-full.yml— where a merge lands.fixed-table-formmainubuntu-latest— the one hosted run per main mergegh workflow run ci-full.yml --ref <branch>full-cilabellabeledonly — an explicit ask, not a plain pull-request triggerThree rows cannot move and say so in place:
big-endian(s390x cross gcc andqemu-userat pinned Ubuntu versions),windowsandmsvc. The pool is macOS and has none of those.Two portability fixes for the pool:
$(nproc)becomes$(getconf _NPROCESSORS_ONLN)(three rows), anderlef/setup-beam— which has no macOS support — is gated onrunner.environment == 'github-hosted', wheredist/already carries the BEAM on PATH.The merge condition is the local gate (
merge-lane.sh run --local-gates) plus a read. The lane already supports a pull request with NO hosted checks at all:pr_checksreportsg0/p0/r0whengh pr checksreturns nothing, andone_pass's(( C_PENDING > 0 || C_GREEN == 0 ))branch does not wait when a green gate exists for the PR's current head. No lane change was needed; a hosted RED still stops a PR, as before.Not touched:
cla.yml(the Contributor Assignment Agreement,pull_request_target). It is not a test and not ours to change — it is the one check that still appears on a pull request.docs/CONTRIBUTING.mdstates the loop in five lines — local gate, push the branch, open the PR as the record, a read from another line, the lane merges on local green + read, the full lane runs on the merge — and says hosted CI runs only on main and nightly, citing the ruling with its date.Gates
go test ./internal/ci/green (pins, and the Go-shadowing gate).gofmt -l .prints nothing.🤖 Generated with Claude Code
The studio pool's three defects, found by #980's own first dispatch
The first full-lane dispatch on the pool (run 34603066277) went 52 of 72 jobs
red, and the fast lane's
go-test-touchedwent red with it (run 34603030435).None of the three causes was the diff; all three were the pool.
(1) The sibling clones were not idempotent on a warm workspace
A self-hosted runner's workspace parent persists between runs, and the
sibling runtimes the Makefiles reach for as
../serialize*live there.actions/checkoutcleans the repository directory; it does not touch theparent. So the second run of any job that clones a sibling dies by name:
Every clone step in both workflows now
rm -rfthe target and re-clones atthe pinned tag — ten sites in
ci-full.yml, and the fast lane'sleg-gatealready had it. The rust versioning row's
if [ ! -d ../serialize.rs ]guardgoes with them: it did not fail, it reused whatever the previous run left,
which is how a stale sibling outlives a pin bump. The repository checkout itself
is untouched and still cleans on every run — nothing in either file sets
clean: false, and nothing may.(2) The cs rows — the pin was never the problem
The decision asked for was install-10.0.400 or move-the-pin-to-10.0.401.
Neither:
.github/dotnet-versionholds the band10.0, and the Studio has10.0.400and10.0.401under~/.dotnet, both inside the band, bothcomplete, both working (
dotnet --versionunder aglobal.jsonpinning eitheranswers). The pin and the Studio already match.
The defect is that
ci-full.yml's foursetup-dotnetsteps were not gated tohosted runners, unlike
ci-fast.yml's. All six studio runners share one$HOME, so the pool ran six concurrent installs of the same SDK into the same~/.dotnet, and a job read one of them half-written:— every
TestFixedVersioningrow failing on a probe that never built. All foursteps are now
if: runner.environment == 'github-hosted', so the pool installsnothing and uses the SDK already on its PATH at
~/.local/bin/dotnet. No SDKwas installed and the pin did not move; matching the pin is what this does.
(3)
go-test-touchedhad no sibling checkout at allThis is the
internal/codegen/gotableFAIL at 190 s. A broad diff hands the job./..., which reaches every leg's table codegen test, and those generate a probewhose
go.modreplacesgithub.com/mas-bandwidth/serialize.gowith../serialize.go. With no sibling the gate does not skip — it fails by name:It now clones all six siblings, idempotently, for the same reason the
negative-control matrix clones all six: the job's package list is not known when
the step is written.
(4) The java row — noted, not chased
REFUSED BY NAMEon the java row is the tip's pre-§5 Java leg. PR #920 carriesthe fix. Nothing here touches it, and it is the one row expected red until #920
lands.
The pool's operating notes, in
docs/CONTRIBUTING.mdA new The studio pool subsection under the gates: the six runners and their
directories (
~/actions-runner,~/actions-runner-2..6),./svc.sh stop|startper directory and the rule never to touch them during a benchmark window, the
warm-workspace rule with both ways it was earned (a bare clone fails by name;
a
[ ! -d ]guard quietly reuses a stale sibling), why a toolchain install isnot a per-job step on a pool that shares one
$HOME, and a table of where eachtoolchain lives on the Studio.
Gates
go test ./internal/ci/green.gofmt -l .prints nothing.