From 568f2118a3c762c5da79f00dccfef92a67172ff1 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 23:25:54 +0800 Subject: [PATCH] ci: name the Ubuntu image instead of asking for the queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asking for `ubuntu-latest` and asking for `ubuntu-24.04` gets the same machine. Both labels reported `Image: ubuntu-24.04`, `Version: 20260823.283.1`, provisioner `20260819.586`, runner `2.336.0` in their `Set up job` logs on the same day. So this changes which queue a job waits in, and nothing else. The queues are not the same. Across 1183 runner-backed jobs in eleven ASF repositories, `ubuntu-latest` has a median wait of 0.05 minutes and a p90 of 19.07; `ubuntu-24.04` has a median of 0.03 and a p90 of 0.53, while carrying 3.6x the jobs (455 against 126). The alias is not slower on average — it is unpredictable, and a required context is paid at the tail. `apache/flink` sees a p90 of 105.8 minutes on the alias. The sharpest control comes from this repository. One push to #4482 created nine first-layer jobs within the same second, none of them declaring `needs`. The three on `ubuntu-latest` waited 7.5, 7.68 and 7.7 minutes. The six on `ubuntu-24.04`, `ubuntu-24.04-arm`, `windows-2025`, `macos-15`, `windows-latest` and `macos-latest` waited between 3 and 24 seconds. It is not the `-latest` alias as such, since two of the fast six are aliases. It is not our own `concurrency` groups, since every workflow's median `run_started_at - created_at` is 0.0. It is not a self-hosted split, since `runner_group_name` reads `GitHub Actions` on all nine. `ci.yml` also has a reason of its own. Its bubblewrap step disables `apparmor_restrict_unprivileged_userns` specifically because Ubuntu 24.04 gates user namespaces that way. The required context already assumes this image; the alias only left that assumption free to drift without a commit. `windows-latest` and `macos-latest` stay as they are. Across 84 same-instant groups containing both `windows-latest` and `windows-2025`, the median paired difference in wait is 0.00 minutes, so pinning them would buy nothing measurable. Nine substitutions across eight workflows; thirteen jobs in the release and packaging lanes were already pinned. Two costs, both accepted deliberately. The automatic image upgrade becomes a manual commit — `ci-workflow-policy.test.mjs` holds the rule and says how to take an exemption. And `ubuntu-latest` carries 79.1% of this repository's Ubuntu job-minutes (3141 of 3970), so this raises our demand on the pinned label 6.2x, onto a pool whose wait was measured while it carried one sixth of that. The pool absorbs 3.6x more jobs than the alias today at a p90 of 0.53 minutes, and the worst pinned figure anywhere in the sample is `apache/iceberg` at 0.43 median against 18.78 on its own alias. If the p90 on `ubuntu-24.04` passes 2 minutes after this lands, that is the signal to revisit, with both sides finally measurable. Refs #4480 --- .github/workflows/ci.yml | 6 +++- .github/workflows/copilot-auto-review.yml | 2 +- .github/workflows/dependency-audit.yml | 2 +- .../workflows/gitoxide-helper-admission.yml | 2 +- .github/workflows/issue-pr-lifecycle.yml | 2 +- .github/workflows/pr-effort-label.yml | 2 +- .github/workflows/release.yml | 4 +-- .../workflows/runtime-host-peer-admission.yml | 2 +- scripts/ci-workflow-policy.test.mjs | 32 +++++++++++++++++++ 9 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60e432e9e3..3a7da1eb81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,11 @@ jobs: # Renaming it would leave that check unreported on every open pull request # until the rename merged, and nothing could merge while it was unreported. test: - runs-on: ubuntu-latest + # Pinned, not `ubuntu-latest`. The two resolve to the same image, but only + # the alias makes this required context wait at the tail, and the steps + # below already assume this image. `ci-workflow-policy.test.mjs` holds the + # rule for every lane. + runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/copilot-auto-review.yml b/.github/workflows/copilot-auto-review.yml index 858a9a08c8..a33f7e1de6 100644 --- a/.github/workflows/copilot-auto-review.yml +++ b/.github/workflows/copilot-auto-review.yml @@ -33,7 +33,7 @@ jobs: github.event.pull_request.draft == false && !endsWith(github.event.pull_request.user.login, '[bot]') && !contains(github.event.pull_request.labels.*.name, 'copilot-skip') - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 5 steps: diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 69572ba573..633771fa76 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -50,7 +50,7 @@ concurrency: jobs: audit: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - name: Check out the repository diff --git a/.github/workflows/gitoxide-helper-admission.yml b/.github/workflows/gitoxide-helper-admission.yml index 5d16b4dab1..2f9b0bfef3 100644 --- a/.github/workflows/gitoxide-helper-admission.yml +++ b/.github/workflows/gitoxide-helper-admission.yml @@ -57,7 +57,7 @@ jobs: fail-fast: false matrix: os: - - ubuntu-latest + - ubuntu-24.04 - macos-latest - windows-latest steps: diff --git a/.github/workflows/issue-pr-lifecycle.yml b/.github/workflows/issue-pr-lifecycle.yml index 9bbd8c117f..9515e03821 100644 --- a/.github/workflows/issue-pr-lifecycle.yml +++ b/.github/workflows/issue-pr-lifecycle.yml @@ -46,7 +46,7 @@ concurrency: jobs: lifecycle: if: github.repository == 'apache/maka' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 steps: diff --git a/.github/workflows/pr-effort-label.yml b/.github/workflows/pr-effort-label.yml index 8cd3d71b82..ac6d32ec8d 100644 --- a/.github/workflows/pr-effort-label.yml +++ b/.github/workflows/pr-effort-label.yml @@ -46,7 +46,7 @@ concurrency: jobs: label: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee1364dae6..565f571f88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ concurrency: jobs: release-identity: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 outputs: version: ${{ steps.identity.outputs.version }} @@ -364,7 +364,7 @@ jobs: # One draft release carries both platforms, so it is created once, after # every platform has been packaged and verified. needs: [release-identity, desktop, cli-macos-arm64] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: write diff --git a/.github/workflows/runtime-host-peer-admission.yml b/.github/workflows/runtime-host-peer-admission.yml index 1a6036ba4e..0011480711 100644 --- a/.github/workflows/runtime-host-peer-admission.yml +++ b/.github/workflows/runtime-host-peer-admission.yml @@ -53,7 +53,7 @@ concurrency: jobs: test: name: quality - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/scripts/ci-workflow-policy.test.mjs b/scripts/ci-workflow-policy.test.mjs index d0cd004fb4..1297375ab2 100644 --- a/scripts/ci-workflow-policy.test.mjs +++ b/scripts/ci-workflow-policy.test.mjs @@ -375,6 +375,38 @@ test('the recovery lane pairs its path filter with a nightly run and a main push assert.match(readWorkflow('windows-recovery.yml'), /\n {4}name: windows_recovery/u); }); +test('no lane asks for the one runner label that queues', () => { + // `ubuntu-latest` is the only label here whose wait for a runner is not + // predictable: its median is as good as any pinned label's, but its tail + // reaches tens of minutes, and the required context is paid at the tail + // rather than the median. Naming the image instead costs no coverage, + // because the two resolve to the same image; it costs the automatic image + // upgrade, which becomes a deliberate commit rather than a silent one. + // That is the trade this rule makes. To take it back for one lane, change + // this test — an exemption is worth as much as the review it passes, and + // no lane needs one today. + // + // The literal is banned outright rather than only where a runner is named. + // `runs-on` reaches a runner through matrix values, inline sequences and + // `include` objects, so any shape-aware matcher is a second authority that + // can disagree with GitHub's; under this rule the literal has no legitimate + // use anywhere, which makes its mere presence the honest contract. + const workflows = readdirSync(WORKFLOW_DIR).filter( + (file) => file.endsWith('.yml') || file.endsWith('.yaml'), + ); + + assert.ok(workflows.length > 0, 'no workflows found to check'); + + for (const name of workflows) { + // Comments stripped, so explaining the rule in a workflow cannot break it. + assert.doesNotMatch( + readWorkflow(name).replaceAll(/^[ \t]*#.*$/gmu, ''), + /\bubuntu-latest\b/u, + `${name}: ubuntu-latest queues for a runner; name the image instead`, + ); + } +}); + test('the recovery lane keeps every run kind out of one shared concurrency group', () => { const workflow = readWorkflow('windows-recovery.yml');