From f9f024eef6a8eae4b3a33b1351839852f36e9e39 Mon Sep 17 00:00:00 2001 From: Oluwatobi Adelegan Date: Fri, 4 Sep 2026 18:19:48 +0100 Subject: [PATCH 1/3] QVAC-24629 infra: cancel superseded CI runs on push and dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.yml's concurrency group fell back to github.run_id whenever head_ref was empty. head_ref is only set on pull_request events, so every push and every workflow_dispatch produced a group unique to that run and nothing was ever cancelled. Key the group on the PR number, falling back to github.ref, so pushes and dispatches for the same branch supersede each other. This is the block the QVAC monorepo uses across its addon lanes (.cursor/rules/devops/github-actions.mdc "Concurrency"). build.yml's workflow_dispatch can publish a release (create_release input), so that one path keeps a group of its own, keyed by run_id: a release run is neither cancelled nor cancelling, and a later push to the same branch cannot supersede it half-way through. Guarding cancel-in-progress instead would not work — cancel-in-progress governs whether the INCOMING run cancels others, so it would stop a release from cancelling others while still letting a later push cancel the release. runner-names-validate.yml had no concurrency block at all; it gets the plain form. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 11 ++++++++++- .github/workflows/runner-names-validate.yml | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32dfac655..7b9adaa05 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,8 +38,17 @@ on: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} +# One run per (workflow, PR/branch). The previous group fell back to +# github.run_id whenever head_ref was empty — i.e. on every push and +# workflow_dispatch — which made the group unique per run, so those runs never +# cancelled. Keying on the PR number (or the ref, off-PR) puts successive +# pushes and dispatches for the same branch in one group. +# +# A manual dispatch with create_release=true publishes a release, so it gets a +# group of its own and is neither cancelled nor cancelling: a later push to the +# same branch must not supersede a release half-way through. concurrency: - group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }} + group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' && inputs.create_release) && format('release-{0}', github.run_id) || (github.event.pull_request.number || github.ref) }} cancel-in-progress: true jobs: diff --git a/.github/workflows/runner-names-validate.yml b/.github/workflows/runner-names-validate.yml index e170af1d3..afdbd3155 100644 --- a/.github/workflows/runner-names-validate.yml +++ b/.github/workflows/runner-names-validate.yml @@ -20,6 +20,12 @@ on: permissions: contents: read +# One run per (workflow, PR/branch); a newer push or dispatch supersedes the +# in-flight validation. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: validate: runs-on: ubuntu-latest From 9d99d19779a4fb630759fa7eedb6b106799aa266 Mon Sep 17 00:00:00 2001 From: Oluwatobi Adelegan Date: Fri, 4 Sep 2026 18:56:39 +0100 Subject: [PATCH 2/3] fix: exempt the default branch from concurrency cancellation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up. The previous commit keyed every non-PR event on github.ref, which put all master pushes in one group with cancel-in-progress: true. That is where the whole risk of this change sat: - A release or publish job that runs on push to master could be cancelled mid-flight, leaving a created release with a partial asset set. - A cron-only lane whose run outlives its interval is killed by the next tick, so the cache it exists to populate is never saved. - Back-to-back merges leave the earlier commit with no CI verdict at all, which is precisely why upstream used the run_id fallback in the first place. - Self-hosted lanes without an `if: always()` teardown can orphan a server process holding its port and GPU memory when cancelled mid-test. - A workflow_dispatch taking a `sha` input — the normal way to bisect — had two runs for two different commits cancelling each other. Key the default branch on github.run_id, as upstream does, and keep per-PR and per-branch dedup everywhere else: group: ...-${{ github.event.pull_request.number || (github.ref == 'refs/heads/master' && github.run_id) || github.ref }} pull_request -> per PR (supersedes) push/dispatch !master -> per branch (supersedes) push/dispatch master -> per run (never cancels) github.run_id is always non-empty, so the && / || pair cannot fall through to the wrong operand. This keeps the behaviour that was actually asked for — one run per PR/branch, a re-push or re-dispatch replacing the previous one — while dropping the part that only ever destroyed signal. Comments corrected at the same time: several said "push or dispatch" on workflows that have no workflow_dispatch trigger. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 20 +++++++++++--------- .github/workflows/runner-names-validate.yml | 7 ++++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b9adaa05..6a836e2ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,17 +38,19 @@ on: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} -# One run per (workflow, PR/branch). The previous group fell back to -# github.run_id whenever head_ref was empty — i.e. on every push and -# workflow_dispatch — which made the group unique per run, so those runs never -# cancelled. Keying on the PR number (or the ref, off-PR) puts successive -# pushes and dispatches for the same branch in one group. +# Dedupe per PR and per branch: a re-push or re-dispatch supersedes the +# in-flight run. The previous group fell back to github.run_id whenever +# head_ref was empty — i.e. on every push and workflow_dispatch — which made +# the group unique per run, so those runs never cancelled. # -# A manual dispatch with create_release=true publishes a release, so it gets a -# group of its own and is neither cancelled nor cancelling: a later push to the -# same branch must not supersede a release half-way through. +# master is deliberately left on the run_id behaviour. The `release` job runs +# on BOTH a create_release dispatch and a plain push to master, and its +# create/upload steps are gated on `github.ref_name == 'master'` — so keying +# master on the ref would let one merge cancel another mid-uploadReleaseAsset +# and leave a published release with a partial asset set. Exempting the branch +# covers the dispatch and the push path in one rule. concurrency: - group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' && inputs.create_release) && format('release-{0}', github.run_id) || (github.event.pull_request.number || github.ref) }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.ref == 'refs/heads/master' && github.run_id) || github.ref }} cancel-in-progress: true jobs: diff --git a/.github/workflows/runner-names-validate.yml b/.github/workflows/runner-names-validate.yml index afdbd3155..1467cf86c 100644 --- a/.github/workflows/runner-names-validate.yml +++ b/.github/workflows/runner-names-validate.yml @@ -20,10 +20,11 @@ on: permissions: contents: read -# One run per (workflow, PR/branch); a newer push or dispatch supersedes the -# in-flight validation. +# Dedupe per PR and per branch: a newer push supersedes the in-flight +# validation (this workflow has no workflow_dispatch trigger). master is +# exempt on purpose — pushes to the default branch must not cancel each other. concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.ref == 'refs/heads/master' && github.run_id) || github.ref }} cancel-in-progress: true jobs: From 3f5b671e450f6692bafd3a9a24424f6145f42fa5 Mon Sep 17 00:00:00 2001 From: Oluwatobi Adelegan Date: Mon, 7 Sep 2026 13:03:00 +0100 Subject: [PATCH 3/3] fix: key the release exemption on the publish predicate, not the branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up (Ian, PR #38). Exempting only `master` left the partial-publish hazard live on any non-master `create_release: true` dispatch, because every publish gate in this file has a branch-free disjunct: release job (645) if: (push && ref == refs/heads/master) || github.event.inputs.create_release == 'true' docker push (226) push: (push && ref == refs/heads/master) || github.event.inputs.create_release == 'true' Create release (683) if: github.event_name == 'workflow_dispatch' || github.ref_name == 'master' Upload release (692) if: github.event_name == 'workflow_dispatch' || github.ref_name == 'master' So a create_release dispatch publishes a GitHub release and pushes ghcr images from ANY ref, while falling to the third arm and being cancellable by a second dispatch on the same ref. `Upload release` creates the release and then loops uploadReleaseAsset one zip at a time, so a cancel in between leaves a published release with a partial asset set — and the tag-immutability ruleset means that tag cannot be deleted or moved to redo it cleanly. Key the exemption on the predicate that actually gates publishing: group: ...-${{ github.event.pull_request.number || ((github.event.inputs.create_release == 'true' || github.ref_name == 'master') && github.run_id) || github.ref }} github.event.inputs is null on push and pull_request, so the added disjunct is inert there. ref_name matches the form the release steps themselves use. The comment previously paraphrased the gate as `github.ref_name == 'master'`, dropping the `github.event_name == 'workflow_dispatch' ||` half that invalidated its conclusion. It now quotes all four gates verbatim, since this block is the only record of why the exemption exists. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a836e2ca..270773159 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,14 +43,23 @@ env: # head_ref was empty — i.e. on every push and workflow_dispatch — which made # the group unique per run, so those runs never cancelled. # -# master is deliberately left on the run_id behaviour. The `release` job runs -# on BOTH a create_release dispatch and a plain push to master, and its -# create/upload steps are gated on `github.ref_name == 'master'` — so keying -# master on the ref would let one merge cancel another mid-uploadReleaseAsset -# and leave a published release with a partial asset set. Exempting the branch -# covers the dispatch and the push path in one rule. +# Anything that PUBLISHES is exempt and keyed per run instead, so it can never +# be superseded. The exemption keys on the publish predicate rather than on the +# branch, because every publish gate here has a branch-free disjunct: +# +# release job (645) if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event.inputs.create_release == 'true' +# docker push (226) push: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event.inputs.create_release == 'true' +# Create release (683) if: github.event_name == 'workflow_dispatch' || github.ref_name == 'master' +# Upload release (692) if: github.event_name == 'workflow_dispatch' || github.ref_name == 'master' +# +# So a `create_release: true` dispatch publishes from ANY ref, not just master. +# Keying the exemption on the branch alone would leave that path cancellable: +# `Upload release` creates the release and then loops uploadReleaseAsset one +# zip at a time, so a cancel in between leaves a published release with a +# partial asset set — and the tag-immutability ruleset means the tag cannot be +# deleted or moved to redo it cleanly. concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || (github.ref == 'refs/heads/master' && github.run_id) || github.ref }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || ((github.event.inputs.create_release == 'true' || github.ref_name == 'master') && github.run_id) || github.ref }} cancel-in-progress: true jobs: