ci: add concurrency groups to cancel superseded PR runs - #54
Conversation
Superseded runs currently burn to completion instead of cancelling.
Measured in mystira-workspace over the same period: a workflow without
concurrency had 72 supersessions run to completion (856 wasted
wall-minutes, zero cancelled), while one with cancel-in-progress had 38
supersessions cancelled within ~1 min each (36 min total).
CI workflows cancel on pull_request only — ci.yml, lint.yml,
monorepo-ci.yml, security.yml, validate-templates.yml, and
validate-version.yml:
concurrency:
group: <workflow-id>-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Deploy, release, and destroy workflows get a group with NO
cancel-in-progress so runs serialise instead of being interrupted:
deploy-autopr-engine, deploy-website, destroy-infra, the five release-*
workflows, and copilot-setup-steps.
pr-comment-handler.yml keys on github.event.issue.number, since
issue_comment events carry no pull_request object, and does not cancel —
cancelling it would silently drop a queued command.
Indentation follows each file's existing style (four spaces in
monorepo-ci.yml, release-*.yml, and validate-version.yml; two
elsewhere).
No merge queue is configured on this repo, so gating cannot stall a
queue. actionlint reports the same five pre-existing findings before and
after this change.
📝 WalkthroughWalkthroughGitHub Actions workflows now use concurrency groups based on pull requests, issue numbers, or Git references. Pull-request workflows cancel superseded runs. Other workflows prevent overlapping runs for the same reference. ChangesWorkflow concurrency controls
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb617f206a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| types: [created] | ||
|
|
||
| concurrency: | ||
| group: pr-comment-handler-${{ github.event.issue.number }} |
There was a problem hiding this comment.
Preserve every queued PR comment event
When three comments arrive on the same PR while the first handler is still running, the second run becomes pending and the third replaces it, so the second comment is never analyzed or applied. Omitting cancel-in-progress does not create an unbounded serialized queue: GitHub concurrency permits at most one running and one pending run and cancels an existing pending run when another is queued. Since each issue_comment run contains a distinct payload, this grouping can silently discard user commands; use a group unique to the comment or another serialization mechanism that retains every event.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/destroy-infra.yml:
- Around line 6-7: Update the workflow concurrency configuration to use a
ref-independent group such as destroy-infra-production so production teardown
runs serialize across all refs. Configure the concurrency queue to preserve
every pending destroy request by adding queue: max.
In @.github/workflows/monorepo-ci.yml:
- Around line 24-26: Fix the indentation of the child keys in the workflow’s
concurrency block: align group and cancel-in-progress two spaces beneath
concurrency, preserving their existing values.
In @.github/workflows/pr-comment-handler.yml:
- Around line 7-8: Update the concurrency configuration in
.github/workflows/pr-comment-handler.yml at lines 7-8 to add queue: max so
pending commands for the same pull request are retained. Review
.github/workflows/copilot-setup-steps.yml lines 10-11,
.github/workflows/destroy-infra.yml lines 6-7, and
.github/workflows/release-desktop.yml lines 8-9; add queue: max there only if
every invocation for the same ref or tag must execute, otherwise make no change.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 501daecc-a3a1-4ce6-bcfc-5a1f63bbef74
📒 Files selected for processing (16)
.github/workflows/ci.yml.github/workflows/copilot-setup-steps.yml.github/workflows/deploy-autopr-engine.yml.github/workflows/deploy-website.yml.github/workflows/destroy-infra.yml.github/workflows/lint.yml.github/workflows/monorepo-ci.yml.github/workflows/pr-comment-handler.yml.github/workflows/release-desktop.yml.github/workflows/release-orchestration-utils.yml.github/workflows/release-vscode-extension.yml.github/workflows/release-website.yml.github/workflows/release.yml.github/workflows/security.yml.github/workflows/validate-templates.yml.github/workflows/validate-version.yml
| concurrency: | ||
| group: destroy-infra-${{ github.ref }} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/destroy-infra.yml
printf '%s\n' '--- related workflow references ---'
rg -n --glob '.github/workflows/**' 'concurrency:|group:|cancel-in-progress:|environment:|terraform (destroy|apply)|destroy' .github/workflows
printf '%s\n' '--- repository guidance ---'
rg -n 'queue: max|concurrency|destroy-infra|production' .github README.md 2>/dev/null || trueRepository: phoenixvc/codeflow-engine
Length of output: 6353
🌐 Web query:
site:docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions concurrency one running one pending queued job new queued cancels pending
💡 Result:
In the context of GitHub Actions, the behavior you described is the default configuration for concurrency groups [1]. When a concurrency group has a job already running and a new job is triggered, the new job enters a pending state [1]. By default, if another job is already in the pending state within that same group, that existing pending job is canceled and replaced by the new incoming job [1]. This default behavior is defined by the queue property being set to single, which ensures at most one job or workflow run can be pending in the concurrency group at any time [1]. To change this behavior to allow more jobs to queue instead of being canceled, you can set the queue property to max, which allows up to 100 jobs to wait in the queue before additional jobs are canceled [1]. Additionally, you can control whether the currently running job is canceled by setting the cancel-in-progress property to true [1].
Citations:
🏁 Script executed:
#!/bin/bash
set -eu
url='https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions'
python3 - <<'PY'
import urllib.request
url = "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions"
data = urllib.request.urlopen(url, timeout=20).read().decode("utf-8", "replace")
for needle in ("queue:", "queue", "cancel-in-progress", "pending"):
print(f"--- {needle} ---")
start = 0
found = False
while True:
pos = data.lower().find(needle.lower(), start)
if pos < 0:
break
found = True
print(data[max(0, pos-180):pos+320].replace("\n", " ")[:500])
start = pos + len(needle)
if not found:
print("not found")
PYRepository: phoenixvc/codeflow-engine
Length of output: 2449
🌐 Web query:
GitHub Actions concurrency queue key queue: max official documentation
💡 Result:
In GitHub Actions, the queue: max property within the concurrency configuration allows up to 100 jobs or workflow runs to be placed in a pending state per concurrency group [1][2]. Key details regarding queue: max: - Functionality: By default, only one run can be in a pending state for a concurrency group, with new runs canceling the previous one [1][3]. Setting queue: max enables a queue of up to 100 pending runs that execute sequentially [1][4]. - Queue Limit: Once the queue reaches the limit of 100 pending runs, any additional jobs or workflow runs added to that concurrency group will be canceled [1][2]. - Restriction: You cannot use queue: max in combination with cancel-in-progress: true [1][3]. Attempting to do so will result in a workflow validation error because they represent conflicting behaviors for handling in-progress runs [1][4]. Example configuration: concurrency: group: production-deploy queue: max [1][3]
Citations:
- 1: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
- 2: https://docs.github.com/en/actions/reference/limits
- 3: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax?use_case=
- 4: https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
import urllib.request
url = "https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions"
data = urllib.request.urlopen(url, timeout=20).read().decode("utf-8", "replace")
for needle in ("queue:", "queue", "cancel-in-progress", "pending"):
print(f"--- {needle} ---")
start = 0
found = False
while True:
pos = data.lower().find(needle.lower(), start)
if pos < 0:
break
found = True
print(data[max(0, pos-180):pos+320].replace("\n", " ")[:500])
start = pos + len(needle)
if not found:
print("not found")
PYRepository: phoenixvc/codeflow-engine
Length of output: 2449
Serialize all production destroys and preserve pending requests.
- Use a ref-independent group such as
destroy-infra-production;github.refallows destroys from different refs to run concurrently. - If every teardown request must execute, add
queue: max; otherwise, a new pending run replaces the existing pending run in the same group.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/destroy-infra.yml around lines 6 - 7, Update the workflow
concurrency configuration to use a ref-independent group such as
destroy-infra-production so production teardown runs serialize across all refs.
Configure the concurrency queue to preserve every pending destroy request by
adding queue: max.
| concurrency: | ||
| group: monorepo-ci-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the concurrency block indentation.
The YAML validator flags Line 25 because the child keys use four spaces. Reindent the block to the expected two-space level.
Proposed fix
concurrency:
- group: monorepo-ci-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+ group: monorepo-ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| concurrency: | |
| group: monorepo-ci-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| concurrency: | |
| group: monorepo-ci-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} |
🧰 Tools
🪛 GitHub Check: Validate YAML Files
[failure] 25-25:
25:5 [indentation] wrong indentation: expected 2 but found 4
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/monorepo-ci.yml around lines 24 - 26, Fix the indentation
of the child keys in the workflow’s concurrency block: align group and
cancel-in-progress two spaces beneath concurrency, preserving their existing
values.
Source: Linters/SAST tools
| concurrency: | ||
| group: pr-comment-handler-${{ github.event.issue.number }} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
for f in \
.github/workflows/pr-comment-handler.yml \
.github/workflows/copilot-setup-steps.yml \
.github/workflows/destroy-infra.yml \
.github/workflows/release-desktop.yml
do
printf '\n===== %s =====\n' "$f"
sed -n '1,45p' "$f"
done
printf '\n===== concurrency and queue keys =====\n'
rg -n -C 3 'concurrency:|cancel-in-progress|queue:' .github/workflowsRepository: phoenixvc/codeflow-engine
Length of output: 12590
🌐 Web query:
GitHub Actions workflow syntax concurrency pending runs queue max replace pending run cancel-in-progress official documentation
💡 Result:
In GitHub Actions, the concurrency keyword controls how multiple runs of a workflow or job within the same concurrency group behave [1][2]. Default Behavior By default, a concurrency group allows at most one running job or workflow and one pending job or workflow [3][2]. If a new job or workflow is queued while another is already pending in the same group, the existing pending run is automatically canceled and replaced by the new one [3][2]. Queueing with queue: max To support sequential processing of multiple runs, you can use the queue property [1][4]. Setting queue: max allows up to 100 jobs or workflow runs to be queued in a single concurrency group [5][6]. Once this limit is reached, any additional runs entering the group will be rejected [6]. This setting is intended for workflows that must execute in order, such as deployments to a shared environment [4]. Cancellation Control with cancel-in-progress The cancel-in-progress property determines whether an active, running job is terminated when a new job enters the concurrency group [3][2]. - When cancel-in-progress: true, any currently running job or workflow in the group will be canceled when a new one starts [3][2]. - When cancel-in-progress: false (the default) or omitted, active jobs are allowed to finish, and the new job will wait in the queue [2][7]. Combining these settings allows you to customize whether you prefer to stop old runs to save resources or queue them to ensure all tasks execute sequentially [8][4]. Concurrency group names must be unique if you intend to isolate workflows; if multiple workflows share the same group name, they will compete for that single concurrency slot [5].
Citations:
- 1: https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs
- 2: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
- 3: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax?use_case=
- 4: https://github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues/
- 5: https://docs.github.com/enterprise-cloud@latest/actions/using-jobs/using-concurrency
- 6: https://docs.github.com/en/actions/reference/limits
- 7: https://latchkey.dev/learn/github-actions/github-actions-concurrency-pending-job-canceled
- 8: https://docs.github.com/en/actions/concepts/workflows-and-actions/concurrency
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
files = [
".github/workflows/pr-comment-handler.yml",
".github/workflows/copilot-setup-steps.yml",
".github/workflows/destroy-infra.yml",
".github/workflows/release-desktop.yml",
]
for name in files:
text = Path(name).read_text(encoding="utf-8-sig")
concurrency = text.split("concurrency:", 1)[1].split("\njobs:", 1)[0]
group = re.search(r"group:\s*(.+)", concurrency)
cancel = re.search(r"cancel-in-progress:\s*(.+)", concurrency)
queue = re.search(r"queue:\s*(.+)", concurrency)
print(name)
print(" group:", group.group(1).strip() if group else "<missing>")
print(" cancel-in-progress:", cancel.group(1).strip() if cancel else "<omitted>")
print(" queue:", queue.group(1).strip() if queue else "<omitted>")
print(" trigger:", text.split("on:", 1)[1].split("concurrency:", 1)[0].strip().replace("\n", " "))
PY
printf '\n===== official documentation references =====\n'
curl -L --fail --silent --show-error \
https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax \
| rg -n -i -C 3 'queue: max|pending|concurrency group|cancel-in-progress' | head -120Repository: phoenixvc/codeflow-engine
Length of output: 20500
🌐 Web query:
site:docs.github.com/en/actions "queue: max" concurrency GitHub Actions
💡 Result:
In GitHub Actions, the queue: max setting is used within the concurrency configuration to allow multiple jobs or workflow runs to wait in a pending state within the same concurrency group [1][2]. Key details regarding queue: max: - Capacity: When queue: max is specified, up to 100 jobs or workflow runs can be in a pending (queued) state simultaneously for that specific concurrency group [3][1]. - Queue Full Behavior: If the concurrency group's queue reaches this limit of 100 pending runs, any additional jobs or workflow runs triggered for that group will be canceled [1][2]. - Comparison to Default: By default, GitHub Actions uses a single queue setting, which permits only one pending job or workflow run at a time; when a new run is triggered, it cancels any existing pending run in that group [1][2]. - Purpose: This setting is useful when you need multiple runs to execute sequentially without them automatically canceling each other, ensuring they all eventually run in the order they were triggered [4]. Note that this queue limit is specific to the concurrency feature and is distinct from other limitations, such as matrix strategy parallelism (max-parallel) or total runner concurrency limits, which are managed at the account or organization level [3][5].
Citations:
- 1: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency
- 2: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
- 3: https://docs.github.com/en/actions/reference/limits
- 4: https://docs.github.com/en/actions/concepts/workflows-and-actions/concurrency
- 5: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations
Preserve required pending runs with queue: max. GitHub Actions cancels an existing pending run when a new run enters the same group. Add queue: max when every invocation must execute.
pr-comment-handler.yml: required to avoid dropping commands for the same pull request.copilot-setup-steps.yml: add it if every setup invocation for the same ref must execute.destroy-infra.yml: add it if every teardown request for the same ref must execute.release-desktop.yml: add it only if repeated runs for the same tag ref must execute. Different tag refs use different groups.
queue: max retains up to 100 pending runs. Additional runs are canceled.
📍 Affects 4 files
.github/workflows/pr-comment-handler.yml#L7-L8(this comment).github/workflows/copilot-setup-steps.yml#L10-L11.github/workflows/destroy-infra.yml#L6-L7.github/workflows/release-desktop.yml#L8-L9
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/pr-comment-handler.yml around lines 7 - 8, Update the
concurrency configuration in .github/workflows/pr-comment-handler.yml at lines
7-8 to add queue: max so pending commands for the same pull request are
retained. Review .github/workflows/copilot-setup-steps.yml lines 10-11,
.github/workflows/destroy-infra.yml lines 6-7, and
.github/workflows/release-desktop.yml lines 8-9; add queue: max there only if
every invocation for the same ref or tag must execute, otherwise make no change.
Superseded runs currently burn to completion instead of cancelling. This repo billed 504 Actions minutes ($3.04) 1–12 Aug 2026.
Why
Measured in
phoenixvc/mystira-workspaceover the same period:[CI] Validate PR[CI] Identity Productcancel-in-progress: trueSame churn, 24x the waste. Reference implementation: phoenixvc/mystira-workspace#3739.
What changed
CI — cancels on
pull_requestonly (ci,lint,monorepo-ci,security,validate-templates,validate-version):Six workflows all trigger on
pull_requesttomaster/develop, so a rapid push sequence currently starts six duplicate stacks and lets every superseded one finish.Group only, NO
cancel-in-progress— runs serialise instead of being interrupted:deploy-autopr-engine,deploy-websitedestroy-infrarelease,release-desktop,release-orchestration-utils,release-vscode-extension,release-websitecopilot-setup-stepsworkflow_dispatchonly; a fixed group prevents an accidental double-dispatchpr-comment-handler.ymlTriggered by
issue_comment, which carries nopull_requestobject —github.event.pull_request.numberis always null there, so the standard key would collapse every comment across the repo into one group keyed ongithub.ref. It usesgithub.event.issue.numberinstead, and does not cancel: cancelling would silently drop a queued command.Note on formatting
This repo mixes YAML indentation styles. Each block follows the file it lands in — four spaces in
monorepo-ci.yml, the fiverelease-*.yml, andvalidate-version.yml; two spaces elsewhere. Worth knowing when reading the diff, which otherwise looks inconsistent.Safety checks
merge_grouptriggers; no branch-protection merge queue; no rulesets configured. Gating cannot stall a queue.cancel-in-progresson any deploy / release / destroy workflow — asserted mechanically, not by eye.actionlint1.7.12: 5 findings before, 5 after — none new, none removed. The two pre-existingtemplates/*.ymlsyntax findings are in.github/workflows/templates/, untouched here.jobssurvived intact.Config-only; no job logic, runner, or step changed.
Summary by CodeRabbit