CI/CD operations
GitHub Actions concurrency and cancellation
Choose a GitHub Actions concurrency policy by the work you can safely discard. For pull request checks, cancel-in-progress: true can stop stale runs in the same group. For releases that must not overlap or disappear, use a separate group with queue: max and no in-progress cancellation. The default group keeps only one pending run, replacing an older pending run when another arrives; it is not a retained release queue.
Decide which work is replaceable
A pull request can receive several pushes while its checks are still running. Once a newer revision exists, finishing the older revision's test run may provide little value for the merge decision. A production release has a different risk: silently discarding a queued release attempt can leave a merge without a delivery attempt, and interrupting a running deploy can leave an uncertain state. These are different cancellation policies, so give them different concurrency groups. Decide whether the work is replaceable before choosing a YAML key.
GitHub Actions concurrency allows at most one running job or workflow run in a group. It can be declared at the workflow level to coordinate whole runs or at a job to coordinate only the job that shares the key. A workflow-level group can be useful when every job in a stale pull request run can be discarded. A job-level group can serialize only release work while unrelated build and test jobs proceed. Scope the group around the resource or decision that must be protected, and keep its name distinct from unrelated workflows.
A concurrency group is a scheduling rule, not a correctness proof. It cannot decide whether the newest code passed the required checks, whether a deploy reached the intended environment, or which revision is live. Keep each run's commit identity in the delivery record. The policy should make competing work predictable; the release process still needs its normal check gate, authorization, and post-deploy verification. Treat a cancelled run as cancelled, not as a successful result inherited by a newer run.
Sources: Workflow syntax for GitHub Actions — GitHub Docs · Concurrency — GitHub Docs
Latest-only pull request checks
This partial fragment is for a dedicated pull request check workflow whose older revisions may be stopped. It groups runs by workflow and pull request ref, then asks GitHub to cancel work already running in that group when a new run arrives. It deliberately omits checkout actions, runner selection, test commands, and a required-check configuration. Review those pieces in the real repository; the fragment describes only the concurrency decision.
# Partial example: runner, jobs, and steps are omitted.
on: pull_request
concurrency:
group: pr-checks-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueThe workflow prefix helps prevent a collision with a release group. The workflow name and ref distinguish separate pull requests within this scenario. A new run with the same group cancels an in-progress older run; the default single pending slot also means a new pending run replaces an older pending one. This is a deliberate latest-only rule. If the older run performs an external write or a deployment, it does not belong under this broad cancellation policy simply because the workflow also contains tests.
Workflow-level cancellation is a poor fit for a mixed workflow that starts a release after tests. In that design, a new pull request run could affect more than disposable checks if the group is reused carelessly. Move the cancellation group to the test job when only that job should be replaceable, or put release work in a separately named workflow and group. The names matter because GitHub concurrency groups are repository-wide and case-insensitive: two workflows with the same key can interfere even when their YAML files are different.
Sources: Workflow syntax for GitHub Actions — GitHub Docs · Control the concurrency of workflows and jobs — GitHub Docs
Retain queued release attempts without overlap
For a release job that must be serialized and retained, use a separate group with queue: max. The example assumes a trusted main-branch push and a preceding test job, but it is still partial YAML: event trust, deployment steps, permissions, environment rules, and verification need a real review. In this scenario, the group is attached to the release job, so tests can run independently before each release reaches the group. No two release jobs using the key run at once.
# Partial example: test job, runner, deployment steps omitted.
on:
push:
branches: [main]
jobs:
release:
needs: test
concurrency:
group: release-production
queue: max
cancel-in-progress: falsequeue: max permits as many as 100 pending jobs or workflow runs in one group; additional arrivals are cancelled when that queue is full. It is therefore a bounded queue, not a guarantee that every future merge will deploy. The explicit false line shows the chosen policy: do not cancel an active release when another candidate arrives. GitHub rejects queue: max combined with cancel-in-progress: true as invalid workflow syntax, because retaining a queue and cancelling its active job describe conflicting behaviors.
The release job enters its group only after its prerequisites are satisfied. GitHub documents first-in-first-out processing by the time a job or run starts waiting on that group, while warning that start timing can vary. Do not infer that a release queue is ordered by commit time, webhook arrival, or the moment a workflow was dispatched. If production must only move forward to the intended revision, compare each candidate with the current deployment state and apply an explicit release policy before acting.
Sources: Workflow syntax for GitHub Actions — GitHub Docs · Control the concurrency of workflows and jobs — GitHub Docs · Concurrency — GitHub Docs
What happens when A, B, then C arrive
Consider a snapshot with A still running, B pending, and C arriving in the same group. For the active-cancellation policy, assume A has not yet finished cancellation when C arrives; intermediate timing can vary. These letters are synthetic run labels, not real revisions or measured timing. Under the default queue: single with cancel-in-progress false, A keeps running, B is cancelled, and C becomes the sole pending run. Adding cancel-in-progress true also requests cancellation of A when a newer run arrives. That is useful for disposable pull request checks, but it is usually the wrong surprise for a production deployment.
| Policy | A: running | B: was pending | C: new arrival |
|---|---|---|---|
| Default queue: single; no active cancellation | Continues | Cancelled and replaced | Pending |
| Latest-only; cancel-in-progress: true | Cancelled | Cancelled and replaced | Runs or waits for cancellation to complete |
| Retained queue: max; no active cancellation | Continues | Remains pending | Also pending, within the queue limit |
The distinction between cancelling a pending item and cancelling an active job is easy to miss. Omitting cancel-in-progress does not preserve every waiting release: with the default single pending slot, C still replaces B. Conversely, queue: max does not authorize C to begin before A ends. If your operational requirement is to retain each release attempt, write that requirement down and verify the configured queue rather than assuming that the word concurrency means serialization with unlimited backlog.
At high volume, the max queue still has a limit. When it fills, a later arrival is cancelled, so an operator or agent must observe and handle that cancellation. A retained queue can also accumulate candidates whose prerequisites finished in a different order from pushes. Reconcile each terminal job against the revision it was supposed to release. If an older candidate is no longer safe to deploy, retire it through an explicit policy and record the reason; do not rely on unspecified scheduling order to protect production.
Sources: Control the concurrency of workflows and jobs — GitHub Docs · Concurrency — GitHub Docs
Review the group boundary and prove the release
Before enabling either fragment, enumerate every workflow and job that could produce the same group string. Group names are case-insensitive, and a shared name can cancel or delay work from another workflow in the repository. A prefix with github.workflow helps keep unrelated workflows apart when that is the intent. For a production resource that intentionally shares a group across release workflows, use one documented name and review every participant. The value of the key, not the file boundary, defines who competes.
Environment protection is a separate layer. GitHub environments can restrict deployment branches and can require reviewers or wait rules where available. These controls decide whether a job may proceed to the environment; a concurrency group decides which matching job may run at one time. Neither alone tells you which revision is live after a deploy. Do not describe an environment approval as a release lock or a release queue as proof of successful deployment.
A practical review reads three records: the exact revision that passed required checks, the release job's terminal outcome for that revision, and the destination's observed live revision or equivalent release evidence. Cancellation deserves its own state in this ledger. If the release job was cancelled while pending, no deploy occurred from that attempt; if it was cancelled while active, inspect the target because partial effects may remain. A green check from A cannot close C's delivery, and a completed release step cannot establish live state without target evidence.
Choose latest-only for work whose old result is no longer actionable, and a retained serialized queue for work that must receive a deliberate disposition. Keep the policies in separate groups and review their interaction with the rest of the workflow. If the goal is simply to bound matrix job fan-out, GitHub's matrix max-parallel is a different control with a different purpose. Pick the smallest rule that matches the work, then test the real trigger sequence and inspect which runs were completed, replaced, or cancelled.
Sources: Control the concurrency of workflows and jobs — GitHub Docs · Workflow syntax for GitHub Actions — GitHub Docs · Managing environments for deployment — GitHub Docs
Sources and verification
- Workflow syntax for GitHub Actions — GitHub Docs
GitHub documents workflow- and job-level concurrency, queue: max, default pending replacement, invalid queue/cancel combination, and FIFO by time waiting. Verified .
- Concurrency — GitHub Docs
GitHub explains concurrency as control of simultaneous jobs and workflow runs. Verified .
- Control the concurrency of workflows and jobs — GitHub Docs
GitHub specifies single versus max pending queues, cancellation of active runs, case-insensitive groups, cross-workflow collisions, and waiting-time order. Verified .
- Managing environments for deployment — GitHub Docs
GitHub environments can configure deployment branch rules, required reviewers, wait timers, and protected environment secrets, subject to availability. Verified .