GitHub Actions / Identity, coordination, and governance / Lesson 15
Concurrency groups, cancellation, and release freshness
Coordinate related work without assuming cancellation undoes an external operation.
On this page
Lesson goals
- Choose a concurrency key that represents the resource being coordinated.
- Separate pending selection, running cancellation, and target-side freshness.
A group coordinates a particular namespace
Actions concurrency can restrict jobs or runs sharing a group in the repository. A useful group expresses the resource being coordinated: one proposal’s test feedback or one environment’s adoption path. Include enough context to avoid unrelated workflows canceling each other accidentally. Conversely, adding a unique run identifier defeats coordination because every run belongs to a different group. Parcel’s validation and release paths need different policies. Replacing outdated test work can be reasonable, while interrupting a state-changing operation requires careful target-side semantics.
Read the queue behavior for the selected platform
The reviewed GitHub.com documentation describes a default single pending slot: a new pending member replaces an existing pending member in the same group. It also documents an optional larger queue mode with its own limit and restrictions. These capabilities must be checked for the actual service or Enterprise Server version. Queue waiting order is not source revision order, because jobs reach the group after different preparation times. A concurrency group therefore does not prove that every commit will execute or that the newest source will be adopted last.
Cancel obsolete validation deliberately
The following workflow-level fragment groups validation by workflow name and ref, then cancels an in-progress member when newer work arrives. It is a non-runnable illustration for replaceable validation, not a release prescription. A pull-request ref and a branch ref have event-specific forms, so review which events should share a group. Group names are case-insensitive under documented behavior. When a shared reusable workflow participates, review its group expression too; an accidental collision can cancel the caller or another legitimate job.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
This fragment contains no event or jobs and is never installed. Canceling validation does not prove that an external operation was canceled, and this setting should not be copied to adoption work without a reviewed state contract.
Recheck eligibility after waiting
Candidate r5 can finish preparation before a slower r4 candidate even if r4 started first. Serial adoption can still regress the target if r4 proceeds afterward. Recheck eligibility against an authoritative generation or release policy after obtaining coordination. Do not sort human labels as strings to infer chronology. The complete local model below rejects a smaller declared generation and deduplicates the same request. It deliberately omits transactions, concurrent workers, crashes, and distributed fencing, so it is not a production coordination implementation.
observed_generation = 5
completed = set()
def accept(request, generation):
global observed_generation
if request in completed:
return "duplicate"
if generation < observed_generation:
return "stale"
observed_generation = generation
completed.add(request)
return "accepted"
print(accept("lab-a", 4))
print(accept("lab-b", 6))
print(accept("lab-b", 6))
stale
accepted
duplicate
The generation numbers are synthetic policy inputs. This program performs no job scheduling, lock acquisition, API call, or deployment.
Client cancellation can leave work behind
Canceling an Actions job stops or requests interruption of runner work according to platform behavior; it does not universally reverse an operation already accepted by an external target. Preserve request identity and inspect authoritative completion state before retrying. Separate submission, convergence, user verification, and reporting so one report failure does not repeat an adoption. Cleanup also needs ownership: a canceled job must not remove shared state now belonging to a newer request. Document the recovery path for unknown outcomes before enabling cancellation around side effects.
Common errors and fixes
Every group includes github.run_id.
- Why it happens
- A unique key prevents related runs from sharing coordination.
- How to fix it
- Choose a key representing the intended proposal or target resource.
- Prevent it next time
- Review examples of intended sharing and intended separation.
A lock is assumed to prevent an older release adopting later.
- Why it happens
- Mutual exclusion was confused with source freshness.
- How to fix it
- Recheck eligibility after waiting using authoritative release state.
- Prevent it next time
- Keep ordering and supersession policy explicit.
A canceled job is immediately retried as a new external request.
- Why it happens
- Client cancellation was treated as proof that no target effect occurred.
- How to fix it
- Observe the original operation and retain stable request identity.
- Prevent it next time
- Define cancellation, retry, and cleanup boundaries before release.
Check your understanding
1. Should independent staging and production adoptions always share one global group?
Show worked solution
Not necessarily. Use a target-specific boundary when the resources and policies are independent, while coordinating any truly shared state separately. A global group can create unnecessary waiting; an overly specific group can permit unsafe overlap.
2. Can the default pending replacement policy guarantee every migration executes?
Show worked solution
No. A replaced pending member may never run, and preparation timing differs from source order. A migration chain requiring every transition needs a reviewed ordering and state model rather than an assumption that concurrency queues are a durable migration ledger.
Key takeaways
- Coordinate the actual resource and review queue semantics.
- Cancellation, freshness, and idempotency are separate contracts.