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');