Chore(ci): cancel superseded pull request CI runs via concurrency groups - #504
Open
AmaadMartin wants to merge 2 commits into
Open
Chore(ci): cancel superseded pull request CI runs via concurrency groups#504AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
AmaadMartin
force-pushed
the
feat/ci-concurrency-groups
branch
from
August 2, 2026 07:08
a694135 to
fe3c43f
Compare
added 2 commits
August 2, 2026 00:30
Every push to an open pull request started a fresh, additive set of CI runs while the previous runs for the same pull request kept executing to completion. Those superseded runs test a commit that has already been replaced, so their result is discarded -- the minutes are pure waste, and the worst offender runs on macos-latest. Add a workflow-scoped concurrency group keyed on the pull request number to the three pull_request-triggered workflows, so a newer run cancels the in-progress one. The group key falls back to the unique run id on non-pull-request events, which keeps every push-to-main run alone in its group: a merge can therefore neither cancel nor queue behind an in-progress main run. release-please.yml, auto-assignment.yml and csat.yml are deliberately untouched -- none of them can be superseded, and cancelling a release run would be harmful.
Discovers supersedable workflows from their pull_request activity types rather than hard-coding a file list, so a workflow added later is covered automatically, and encodes the rule that release-please must never cancel an in-progress run.
AmaadMartin
force-pushed
the
feat/ci-concurrency-groups
branch
from
August 2, 2026 07:30
fe3c43f to
c7a47e4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
N/A — no existing issue.
Problem: Every push to an open pull request starts a fresh, additive set of CI runs while the previous runs for the same pull request keep executing to completion.
grep -rn concurrency .github/returned no matches before this change: none of the six workflows declared a concurrency group. The superseded runs test a commit that has already been replaced, so their result is discarded — the minutes are pure waste. The worst offender,Cross-Language Tests, runs onmacos-latest(billed at 10x the Linux rate) and doesnpm install+ twogo mod tidy+npm run build+ the cross-language suite on every one of them; single head branches were observed accumulating 3–17 overlapping runs.Solution: Add a workflow-scoped
concurrencyblock to the threepull_request-triggered workflows —validation.yaml,cross-language-integration.yml,license-check.yml:Why this exact key, rather than the more common
github.ref/github.head_refforms:github.refis unsafe here. All three workflows also trigger onpushtomain, wheregithub.refisrefs/heads/main— every main run would share one group, so a later merge would cancel an in-progress main run. Downgrading tocancel-in-progress: ${{ github.event_name == 'pull_request' }}does not fix it either: with the defaultqueue: single, a newly queued run cancels the existing pending run in the same group, so three merges in quick succession would leave run 2 cancelled.github.event.pull_request.number || github.run_idputs each non-pull-request run alone in its own group, where it can be neither cancelled nor queued.github.head_refcollides across forks. It is the bare head branch name, so two pull requests opened from different forks with the same branch name (patch-1is very common on a public repo) would land in the same group and cancel each other. The pull request number cannot collide. This also matches the sibling repogoogle/adk-python(.github/workflows/pr-triage.yml), which already uses${{ github.event.pull_request.number || github.run_id }}.${{ github.workflow }}is included because concurrency group names are repository-global; without it the three workflows would cancel one another. All six workflowname:values are distinct.The block is top-level (workflow-scoped), not per-job, so a cancelled
validationrun takes all three matrix legs (ubuntu-latest,windows-latest,macos-latest) with it.Deliberately not modified:
release-please.yml(runs only onpushtomain, has nothing to supersede, and a group would risk a release-creating run being cancelled while pending),auto-assignment.ymlandcsat.yml(pull_request: types: [opened]/issues: types: [closed]— each fires at most once and can never be superseded). Runner/matrix changes are out of scope.Behavioural note: superseded pull request runs now end as
cancelledrather thansuccess/failure. Nothing in the repository pins these workflows as required status checks, so no configuration needs updating.Collision check (required by our process):
gh pr list --repo AmaadMartin/adk-js --state open --limit 1000returned 404 open PRs; I grepped the diffs of all 21 CI/workflow-adjacent ones (#450, #428, #427, #421, #418, #416, #415, #414, #406, #403, #393, #379, #377, #370, #345, #343, #338, #306, #296, #237, #133) forconcurrency— zero hits, so nothing else adds a concurrency group. #403 (jobtimeout-minutes) and #450/#406/#296 touch the same workflow files but at different keys, and #418/#343 add repo-config tests undertests/integration/repo_config/, a different directory. No stacking required.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
New file
tests/integration/workflows/workflow_concurrency_test.ts(91 lines, runs in the existingintegrationvitest project, whichnpm run test:coverage— and therefore thevalidationworkflow — already executes). It reads only.github/workflows, shells out to nothing, and touches no network. It derives the supersedable set from each workflow'spull_requestactivity types (explicittypes, or GitHub'sopened/synchronize/reopeneddefault whentypesis omitted) instead of hard-coding a file list, so a workflow added later is covered automatically andauto-assignment.yml(types: [opened]) is correctly excluded.Proof the tests can fail. Every assertion was run against mutated input and observed to fail with an actionable message:
concurrencyblock fromvalidation.yamlvalidation.yaml declares no concurrency group: expected undefined to be definedcancel-in-progress: true→falseinlicense-check.ymlexpected false to be true // Object.is equality${{ github.workflow }}-${{ github.ref }}incross-language-integration.yml(drops the run-id fallback that protectsmain)expected '${{ github.workflow }}-${{ github.ref…' to contain 'github.event.pull_request.number'cancel-in-progress: truegroup torelease-please.ymlexpected true to be false // Object.is equalityon:block fromlicense-check.ymllicense-check.yml declares no 'on' trigger block: expected undefined to be defined'synchronize'→'synchronised') so nothing matchesexpected 0 to be greater than 0Mutations 5 and 6 are the vacuity guards: js-yaml v4 follows the YAML 1.2 core schema, so the workflow key
on:parses as the string'on'and not the booleantrue— if a future parser changed that, or if the discovery filter silently matched nothing, the parameterised assertions would all disappear rather than fail. Both cases now fail loudly. Mutation 6 also confirms an emptyit.eachdoes not abort collection.Coverage: unchanged by construction —
vitest.config.tscollects coverage only fromcore/src/**,dev/src/**,integrations/src/**, and this change adds zero lines under those roots. The thresholds are untouched.No suppressions were added (
@ts-expect-error,eslint-disable,any,as any: zero in the diff). The parsed YAML is typed viaunknown+ anisWorkflownarrowing guard, andexpect.fail-free — assertions carry messages instead. No new dependency:js-yamlis already a runtime dependency ofcore/devhoisted to the workspace root, and@types/js-yamlis already a root devDependency, sopackage.json/package-lock.jsonare untouched.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
Because a
pull_requestrun executes the merge ref, this pull request exercises the new blocks on its own checks — so the supersession test below was run for real on this branch, not just described.Supersession works — observed. On an earlier revision of this branch (SHAs since rewritten by a review fixup): pushed commit A, waited until its runs were live, then pushed commit B with an identical tree.
gh run list --branch feat/ci-concurrency-groupsimmediately after:gh run view 30737173542confirms"conclusion": "cancelled","event": "pull_request"for commit A'svalidationrun.validationwas the only one of the three still in flight when the newer push landed;License Header Check(7s) andCross-Language Testshad already completed, so there was nothing left to cancel — exactly the intended behaviour.Group isolation — observed. In the same run list,
auto-assignmentfor the superseded SHA completedsuccess: it carries no concurrency group, and the${{ github.workflow }}prefix keeps it out ofvalidation's group. Nothing belonging to another workflow or another pull request was cancelled.mainis never cancelled. Not observable pre-merge (this branch produces nopush-to-mainevents). Reasoned from the resolved group key: on a push,github.event.pull_request.numberis null, so the group becomes<workflow>-<github.run_id>andrun_idis unique per run — a main run is alone in its group and can be neither cancelled nor queued. After merge, confirm thepush-to-mainruns of all three workflows complete (and that two closely spaced merges both finish, neithercancellednor stuckqueued), and thatrelease-pleasestill opens/updates its release pull request.Local sanity, run on the exact commit pushed:
npx vitest run --project integration tests/integration/workflows→ 5 passednpm run build→ succeedsnpm run lint→ cleannpm run format:check→ "All matched files use Prettier code style!"npm run ts:check→ 281 pre-existing errors, byte-identical to the count on the base commit (b390217); zero of them mention the new file. This script is not run by CI today.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.
CI on this pull request (head commit
c7a47e4): all real test jobs green.