Skip to content

chore(ci): pin runner images to ubuntu-26.04 - #997

Merged
mforce merged 2 commits into
mainfrom
chore/pin-runner-images
Sep 29, 2026
Merged

mforce merged 2 commits into
mainfrom
chore/pin-runner-images

Conversation

@mforce

@mforce mforce commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

Owner directive, 2026-09-29: "we should not use ubuntu-latest, we should use a specific version." Pins all 17 runs-on: ubuntu-latest occurrences this PR owns to ubuntu-26.04, adds an AGENTS.md rule that keeps it pinned, and teaches actionlint the new label.

  • Ubuntu 26.04 went GA on GitHub-hosted runners on 2026-09-17 (actions/runner-images#14747), and ubuntu-latest itself migrates to 26.04 between 2026-10-19 and 2026-11-19 (actions/runner-images#14748). Pinning to ubuntu-26.04 now lands on where the fleet is already heading, not away from it.
  • .NET is explicitly installed per-job via actions/setup-dotnet (dotnet-version: 10.0.x), Node via actions/setup-node where that action is used, and Playwright downloads its own Chromium rather than using a system browser, so none of those three versions ride the runner image. The runner-provided set is larger than just Docker, though: ci.yml's classifier-self-test and build-and-test jobs invoke the runner's native node directly (no setup-node step), the simulation scripts (tools/simulation/*.sh) call native python3, openssl and docker compose, and release promotion (docs/releasing.md) uses the runner's native gh and jq. Checked each against what 26.04 changes and found no incompatibility, but the claim is "checked, no issue found," not "these don't touch the runner image."
  • 26.04 ships Docker 29.4.2 versus 24.04's 28.0.4. Nothing in this repo pins to a specific Docker Engine version, and the integration test leg (Testcontainers/Postgres) and the sim-stack jobs are the callers that exercise it most.
  • Playwright needs 1.61+ to recognize ubuntu26.04-x64 for playwright install --with-deps; tools/simulation/ui/package.json is already on ^1.62.1, so e2e-smoke.yml is safe to pin.
  • ci.yml's image and publish jobs are left on ubuntu-latest on purpose: feat(ci): publish amd64 and arm64 images #996 (ci/multi-arch-images) owns pinning both as part of its multi-arch matrix.
  • Adds .github/actionlint.yaml declaring ubuntu-26.04/ubuntu-26.04-arm as known labels. actionlint v1.7.12 predates the 26.04 GA and otherwise reports the label as unknown (rhysd/actionlint#682, unresolved upstream).
  • Adds an AGENTS.md paragraph next to the existing Actions-SHA-pinning rule: the label is pinned deliberately, what it's pinned to, why (a silent ubuntu-latest retarget can change what's preinstalled on the image under a pipeline that stays green, and Publish multi-arch images so Cluckwork runs on arm64 (Raspberry Pi and ARM VPS) #995's multi-arch image publishing depends on the Docker version specifically), and what triggers a review (a new Ubuntu LTS reaching GA, or a deprecation announcement for the pinned one).

Verification

  • actionlint (v1.7.12) is clean over every changed workflow with the new config in place, and reproduces 17 unknown-label errors without it, so the config isn't silencing anything else.
  • Full CI is green at head 37bcb2e0: https://github.com/mforce/cluckwork/pull/997/checks. Job durations sit inside the run-to-run noise main already shows on ubuntu-latest; nothing moved materially. See the verification comment below for the per-job breakdown and one flake investigation.

Related: owner directive 2026-09-29 (no tracked issue). Coordinates with #996 on ci.yml's image/publish jobs.

Owner directive, 2026-09-29: never float on ubuntu-latest. Ubuntu 26.04 went
GA on GitHub-hosted runners 2026-09-17 (actions/runner-images#14747), and
ubuntu-latest itself moves to 26.04 between 2026-10-19 and 2026-11-19
(actions/runner-images#14748), so this pins to where the fleet is already
heading. Node, .NET and Playwright are installed per-workflow via
setup-node/setup-dotnet/playwright install and are unaffected by the runner
OS version; Playwright 1.61+ (this repo runs 1.62.1) already recognizes
ubuntu26.04-x64 for --with-deps.

Leaves ci.yml's image and publish jobs on ubuntu-latest: PR #996
(ci/multi-arch-images) is mid-flight rewriting both into a two-architecture
matrix with its own ubuntu-26.04 / ubuntu-26.04-arm runner labels.

Adds .github/actionlint.yaml so actionlint (v1.7.12, predates the 26.04 GA)
stops flagging the new label as unknown.

Adds an AGENTS.md paragraph next to the Actions-SHA-pinning rule recording
why the label is pinned, what triggers a review of the pin, and that #995's
multi-arch image publishing depends on the Docker version specifically.
@mforce

mforce commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner Author

CI verification (head 37bcb2e0)

All 18 checks are green: https://github.com/mforce/cluckwork/pull/997/checks

1. What Ubuntu 26.04 actually changed for these jobs

Confirmed the runner image via the job logs (Image: ubuntu-26.04). In the jobs that failed or were slowest, .NET, Node and Playwright's browser binary are explicitly installed per-job rather than riding the image:

  • The four .NET test legs and build-and-test pull dotnet-version: 10.0.x via actions/setup-dotnet; the integration leg's log shows it resolved to SDK 10.0.401, same as it would on 24.04.
  • web and e2e-smoke.yml pull node-version: 26 via actions/setup-node.
  • e2e-smoke.yml downloads its own Chromium build (npx playwright install --with-deps chromium; the run log confirms chromium: Playwright's downloaded build), not the OS's system browser.

That is not the whole picture, though (the PR body has the corrected version): classifier-self-test and build-and-test also invoke the runner's native node directly, with no setup-node step, and the simulation scripts and release promotion use native python3, openssl, docker compose, gh and jq. 26.04 ships Docker 29.4.2 versus 24.04's 28.0.4 (Ubuntu2604-Readme.md); Tests (integration) (Testcontainers/Postgres) and the sim-stack jobs exercise it most and both passed clean. Checked the rest of the runner-provided set too and found no incompatibility, but "checked, none found" is the honest claim, not "nothing else touches the image."

One Playwright smoke over the simulation fixture (2/3) run failed on the first attempt (optical-size.spec.ts: the largest non-Georgia text leaf found on /expenses measured 16px, under the 24px display-cut floor). The mechanism: optical-size.spec.ts:30 waits only for the page heading before measuring, but the 2rem period-total figure it needs is gated on !expenses.reloading && expenses.meta !== null (web/src/routes/ExpensesPage.tsx:778) and only renders once expense data has loaded. Measuring early can select ordinary 16px text instead. Reran just that shard and it passed clean on the same commit, which is consistent with a pre-existing readiness race in the test rather than a 26.04-specific defect. It would be overstating it to call the runner image unrelated, though: a runner change alters timing, and altered timing is exactly what exposes a readiness race like this one, so a faster or slower runner will keep surfacing it. Flagging it here per the "any 26.04-caused failure is the most valuable finding" instruction; the finding is the pre-existing race, not a regression that needs a workflow fix.

2. Timings

Compared this run's job durations against three recent main runs on ubuntu-latest (24.04):

job this PR (26.04) main range (24.04, n=3)
Tests (domain) 34s 31s
Tests (application) 58s 58s
Tests (apphost) 57s 49s
Tests (integration) 646s 432–621s
Build, audit and schema docs 113s 70–105s
Web typecheck, test, and build 509s 328–498s
Image build + Trivy scan 119s 131s

Everything here sits inside the run-to-run noise main already shows on the same ubuntu-latest label (e.g. integration alone spans 432–621s across three same-image runs). Nothing moved materially because of the pin.

3. .github/actionlint.yaml

actionlint v1.7.12 (the current release) was cut before the 26.04 GA and has no ubuntu-26.04 in its bundled runner-label list, so it reports every pinned line as an unknown label (upstream: rhysd/actionlint#682, unresolved). This file adds ubuntu-26.04 and ubuntu-26.04-arm to self-hosted-runner.labels, actionlint's documented extension point for exactly this case (per its own error message), so it stops flagging a known-good GitHub-hosted label. It doesn't silence anything else: ran actionlint over every changed workflow with the config in place and it's clean, and without the config in place it reproduces the same 17 "unknown label" errors this PR's lines would otherwise cause.

4. ci.yml scope

git diff origin/main...HEAD -- .github/workflows/ci.yml touches exactly 6 runs-on lines: classifier-self-test, changes, tests, build-and-test, web, dependency-review. The image and publish jobs are untouched, left on ubuntu-latest for #996 to pin as part of its multi-arch matrix.

Review round 1 on #997 (Codex): the comment had the removal condition
backwards. #996 landing is when a workflow starts USING the
ubuntu-26.04-arm label, not when the entry becomes safe to drop. Keep
both entries; the only correct trigger to remove the file is a released
actionlint that knows the labels natively (rhysd/actionlint#682).

Also corrects an overstated PR-body/comment claim (Node/.NET/Playwright
are the only per-job-installed tools, Docker the only runner-provided
one) and softens the optical-size.spec.ts flake framing per the same
review round; those are PR body/comment edits, no code change.
@mforce
mforce merged commit 1df21bb into main Sep 29, 2026
19 checks passed
@mforce
mforce deleted the chore/pin-runner-images branch September 29, 2026 19:07
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