diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 32a72ad664..c181e2a84d 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -146,6 +146,22 @@ jobs: cancel-superseded-pr-runs: if: github.event_name == 'pull_request_target' && (github.event.action == 'synchronize' || github.event.action == 'closed') + # Idempotent per PR: a fresh sweep re-verifies live state (live_target_matches + # below) before selecting or cancelling anything, so it fully subsumes + # whatever an older, not-yet-run instance would have done. cancel-in-progress + # true is the right shape here (current-head-run-coalescer.yml instead uses + # its own admission-order queueing, since each of its queued instances + # carries a DIFFERENT specific expected-head only it can act on): it caps + # this job to one running + one queued per PR instead of letting a push + # burst pile up N independent, mutually-non-deduped sweeps that each cost a + # full admission slot under the shared 60-job ceiling. Matches + # codeql-pr.yml's established group-key style (PR-number scoped). + concurrency: + group: >- + cancel-superseded-pr-runs-${{ + github.event.pull_request.base.repo.full_name || github.repository }}-${{ + github.event.pull_request.number || github.run_id }} + cancel-in-progress: true runs-on: ubuntu-24.04 # Bound this gh-api-only cleanup job so a stuck call (rate limit, hung # `gh api --paginate`) cannot silently occupy a runner for GitHub's diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index f2a2a9c1d7..82bca9448e 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -365,7 +365,12 @@ def test_strix_serializes_provider_evidence_per_repository_and_pr() -> None: scans for *other* PRs to be blocked by it. """ workflow = workflow_text("strix.yml") - concurrency_contract = workflow.split("concurrency:", 1)[1].split( + # Isolate the strix: job's own text first: cancel-superseded-pr-runs above + # it now carries its own (PR-scoped, dedup-only) concurrency: block, so a + # naive first-match split on the bare "concurrency:" literal would grab + # that job's block instead of this one. + strix_job = workflow.split("\n strix:\n", 1)[1] + concurrency_contract = strix_job.split("concurrency:", 1)[1].split( "permissions:", 1 )[0]