From dbfe5c6d63963d1bbdbeb2ff52c1f70dbfd8617c Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 5 Sep 2026 03:34:17 +0100 Subject: [PATCH 1/3] test(ci): assert ci-required never cancels an in-progress main run --- .../ci/smart-ci/required-concurrency.test.mjs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 scripts/ci/smart-ci/required-concurrency.test.mjs diff --git a/scripts/ci/smart-ci/required-concurrency.test.mjs b/scripts/ci/smart-ci/required-concurrency.test.mjs new file mode 100644 index 000000000..d970be5a7 --- /dev/null +++ b/scripts/ci/smart-ci/required-concurrency.test.mjs @@ -0,0 +1,87 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { test } from 'node:test' + +const requiredWorkflowUrl = new URL('../../../.github/workflows/ci-required.yml', import.meta.url) + +// A push run on the default branch must never be cancelled by the next merge: +// the tip's own required run is the landed-commit evidence (#2582). +const EXPECTED_GROUP = '${{ github.workflow }}-${{ github.ref }}' +const EXPECTED_CANCEL_IN_PROGRESS = "${{ github.ref != 'refs/heads/main' }}" + +function readLines(workflow) { + return workflow.replaceAll('\r\n', '\n').split('\n') +} + +function indentOf(line) { + return line.search(/\S/) +} + +// Line scan: every `concurrency:` mapping key, with its indentation and the +// scalar entries nested directly beneath it. +function collectConcurrencyBlocks(lines) { + const blocks = [] + lines.forEach((line, index) => { + const header = line.match(/^(\s*)concurrency:\s*$/) + if (!header) return + + const indent = header[1].length + const entries = new Map() + for (const next of lines.slice(index + 1)) { + if (next.trim() === '') continue + if (indentOf(next) <= indent) break + if (next.trimStart().startsWith('#')) continue + const entry = next.match(/^\s*([A-Za-z0-9_-]+):\s*(.*?)\s*$/) + if (entry) entries.set(entry[1], entry[2]) + } + blocks.push({ lineNumber: index + 1, indent, entries }) + }) + return blocks +} + +test('ci-required declares exactly one top-level concurrency block', async () => { + const lines = readLines(await readFile(requiredWorkflowUrl, 'utf8')) + const topLevel = collectConcurrencyBlocks(lines).filter((block) => block.indent === 0) + + assert.equal( + topLevel.length, + 1, + `expected one top-level concurrency block in ci-required.yml, found ${topLevel.length}`, + ) +}) + +test('the top-level concurrency group stays per-workflow-and-ref', async () => { + const lines = readLines(await readFile(requiredWorkflowUrl, 'utf8')) + const [topLevel] = collectConcurrencyBlocks(lines).filter((block) => block.indent === 0) + + assert.equal( + topLevel.entries.get('group'), + EXPECTED_GROUP, + 'ci-required.yml must keep one concurrency group per ref so main runs queue instead of running in parallel', + ) +}) + +test('in-progress cancellation is disabled on the default branch', async () => { + const lines = readLines(await readFile(requiredWorkflowUrl, 'utf8')) + const [topLevel] = collectConcurrencyBlocks(lines).filter((block) => block.indent === 0) + + assert.equal( + topLevel.entries.get('cancel-in-progress'), + EXPECTED_CANCEL_IN_PROGRESS, + 'ci-required.yml must not cancel an in-progress run on refs/heads/main (#2582)', + ) +}) + +test('no job-level concurrency block re-enables cancellation on main', async () => { + const lines = readLines(await readFile(requiredWorkflowUrl, 'utf8')) + const nested = collectConcurrencyBlocks(lines).filter((block) => block.indent > 0) + + for (const block of nested) { + const cancel = block.entries.get('cancel-in-progress') + if (cancel === undefined) continue + assert.ok( + cancel === 'false' || cancel === EXPECTED_CANCEL_IN_PROGRESS, + `job-level concurrency at ci-required.yml line ${block.lineNumber} sets cancel-in-progress: ${cancel}, which can cancel a main run (#2582)`, + ) + } +}) From a9f03a91dd89d2cd849213dbce7e05e9461980aa Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 5 Sep 2026 03:34:41 +0100 Subject: [PATCH 2/3] ci: stop cancelling in-progress required runs on main --- .github/workflows/ci-required.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-required.yml b/.github/workflows/ci-required.yml index 94e6f5b1c..fb879a9f2 100644 --- a/.github/workflows/ci-required.yml +++ b/.github/workflows/ci-required.yml @@ -89,9 +89,11 @@ permissions: # The required-lane gitleaks job runs in PR scan-mode, which reads the PR commit context. (#1132 review) pull-requests: read +# A push run on refs/heads/main is the landed-commit evidence for that tip, so it is never +# cancelled: during a merge wave main runs queue behind each other, PR refs still cancel (#2582). concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: docs-governance: From 0fc8fb25769782daeb119257215cd90dd0a333cf Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 5 Sep 2026 03:45:17 +0100 Subject: [PATCH 3/3] ci: state the concurrency guarantee precisely (one in-flight main run, newest pending survives) --- .github/workflows/ci-required.yml | 6 ++++-- scripts/ci/smart-ci/required-concurrency.test.mjs | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-required.yml b/.github/workflows/ci-required.yml index fb879a9f2..f7c0d7a4d 100644 --- a/.github/workflows/ci-required.yml +++ b/.github/workflows/ci-required.yml @@ -89,8 +89,10 @@ permissions: # The required-lane gitleaks job runs in PR scan-mode, which reads the PR commit context. (#1132 review) pull-requests: read -# A push run on refs/heads/main is the landed-commit evidence for that tip, so it is never -# cancelled: during a merge wave main runs queue behind each other, PR refs still cancel (#2582). +# A push run on refs/heads/main is landed-commit evidence, so an in-progress main run is not +# cancelled by the next merge. GitHub keeps one in-progress plus one pending run per group, so +# during a merge wave the run in flight completes and the newest tip runs next; intermediate +# pending main runs are still superseded. PR refs keep cancelling (#2582). concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} diff --git a/scripts/ci/smart-ci/required-concurrency.test.mjs b/scripts/ci/smart-ci/required-concurrency.test.mjs index d970be5a7..f98ca891d 100644 --- a/scripts/ci/smart-ci/required-concurrency.test.mjs +++ b/scripts/ci/smart-ci/required-concurrency.test.mjs @@ -4,8 +4,10 @@ import { test } from 'node:test' const requiredWorkflowUrl = new URL('../../../.github/workflows/ci-required.yml', import.meta.url) -// A push run on the default branch must never be cancelled by the next merge: -// the tip's own required run is the landed-commit evidence (#2582). +// An in-progress push run on the default branch must not be cancelled by the next merge: +// the run in flight completes and the newest tip runs next (GitHub keeps one in-progress +// plus one pending run per group, so intermediate pending main runs are still superseded). +// The tip's own required run is the landed-commit evidence (#2582). const EXPECTED_GROUP = '${{ github.workflow }}-${{ github.ref }}' const EXPECTED_CANCEL_IN_PROGRESS = "${{ github.ref != 'refs/heads/main' }}" @@ -57,7 +59,7 @@ test('the top-level concurrency group stays per-workflow-and-ref', async () => { assert.equal( topLevel.entries.get('group'), EXPECTED_GROUP, - 'ci-required.yml must keep one concurrency group per ref so main runs queue instead of running in parallel', + 'ci-required.yml must keep one concurrency group per ref so at most one main run is in flight at a time (#2582)', ) })