Skip to content

ci: nothing runs on a PR; the fast lane on push to our branches, the full lane where a merge lands - #980

Draft
gafferongames wants to merge 7 commits into
fixed-table-formfrom
rowan/ci-merge-only
Draft

gafferongames wants to merge 7 commits into
fixed-table-formfrom
rowan/ci-merge-only

Conversation

@gafferongames

@gafferongames gafferongames commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

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."

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_request event 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: push to rowan/**, johnny/**, emma/**, freddy/**, stella/**, plus workflow_dispatch. No pull_request trigger at all. Every job stays runs-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-level if: — 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.

event runners
push to fixed-table-form the studio pool (self-hosted)
push to main hosted ubuntu-latest — the one hosted run per main merge
nightly schedule hosted
gh workflow run ci-full.yml --ref <branch> the studio pool
full-ci label hosted, and now on labeled only — an explicit ask, not 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. The pool is macOS and has none of those.

Two portability fixes for the pool: $(nproc) becomes $(getconf _NPROCESSORS_ONLN) (three rows), 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 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_checks reports g0/p0/r0 when gh pr checks returns nothing, and one_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.md states 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.
  • Measured wall times are in the comment below.

🤖 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-touched went 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/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 — ten sites in ci-full.yml, and the fast lane's leg-gate
already had it. The rust versioning row's if [ ! -d ../serialize.rs ] guard
goes 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-version holds the band 10.0, and the Studio has
10.0.400 and 10.0.401 under ~/.dotnet, both inside the band, both
complete, both working (dotnet --version under a global.json pinning either
answers). The pin and the Studio already match.

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:

A fatal error was encountered. The library 'libhostpolicy.dylib' required to
execute the application was not found in '/Users/glenn/.dotnet/sdk/10.0.401/'.

— every TestFixedVersioning 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 at ~/.local/bin/dotnet. 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

This is the internal/codegen/gotable FAIL at 190 s. A broad diff hands the job
./..., 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:

Probe.go:12:2: github.com/mas-bandwidth/serialize.go@v0.0.0: replacement
directory /Users/glenn/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.

(4) The java row — noted, not chased

REFUSED BY NAME on the java row is the tip's pre-§5 Java leg. PR #920 carries
the fix.
Nothing here touches it, and it is the one row expected red until #920
lands.

The pool's operating notes, in docs/CONTRIBUTING.md

A new The studio pool subsection under the gates: the six runners and their
directories (~/actions-runner, ~/actions-runner-2..6), ./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 (a bare clone fails by name;
a [ ! -d ] guard quietly reuses a stale sibling), why a toolchain install is
not a per-job step on a pool that shares one $HOME, and a table of where each
toolchain lives on the Studio.

Gates

  • go test ./internal/ci/ green.
  • gofmt -l . prints nothing.

gafferongames and others added 4 commits September 11, 2026 08:53
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>
@gafferongames

Copy link
Copy Markdown
Contributor Author

Measured, on this branch

What ran on the pull-request event: nothing but the CAA. gh run list --branch rowan/ci-merge-only after the push and the PR open:

run event wall
Contributor Assignment Agreement (34603068077) pull_request_target 8s
CI fast (34603030435) push, on studio 6m49s (13:12:48 → 13:19:37)
CI full (34603066277) workflow_dispatch, on studio 9m09s, 72 jobs (13:13:11 → 13:22:20)

No pull_request-event workflow run exists. The push-triggered fast run's check runs still appear ON the pull request (GitHub attaches them to the head SHA), so the read surface keeps a status without a pull-request trigger — and a RED there still stops the lane.

The lane needs no change. Verified against this PR with MERGE_LANE_DIR pointed at a scratch lane (the real lane untouched): pr_checks reports g0/p0/r0 when gh pr checks returns nothing, and one_pass's (( C_PENDING > 0 || C_GREEN == 0 )) branch does not wait when a green gate exists for the PR's current head — #980 hosted checks are p10/g4, but the local fast lane is green on 9188f05c3410 — not waitingwould merge now.

Two findings the pool owes, neither caused by this change

  1. ci-full cannot land on the studio pool yet: the sibling clone steps are not idempotent on a warm workspace. 52 of 72 jobs failed at Check out the … serialize runtimes (pinned releases) with fatal: destination path 'serialize' already exists and is not an empty directory — a fast run had already cloned the siblings into the same workspace. The fix is one line per clone step (rm -rf the target first, the way a warm runner needs). Until it lands, a merge into fixed-table-form would be red for a reason that has nothing to do with the schema; the 9m09s above is therefore a FLOOR measurement, not a green full lane. The 20 that passed include windows, msvc, lint, cpp-lock and generated.
  2. Three fast-lane rows are red on the pool for environment reasons: cslibhostpolicy.dylib missing under /Users/glenn/.dotnet/sdk/10.0.401 (the pin is 10.0.400, so the row resolved a different SDK); javatables-java-fixedform REFUSED BY NAME; go-test-touchedinternal/codegen/gotable FAIL (190s).

Both belong to the pool's setup rather than to the trigger shape, and both want their own branch.

gafferongames and others added 2 commits September 11, 2026 09:30
…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>
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