Job Dependencies, needs, Concurrency, Cancellation, and Deployment Serialization: Guided Hands-On Workflow
This lab uses a disposable repository and no credentials beyond the automatic read-only job context. You will first prove fan-out/fan-in, then run two overlapping fake deployments that wait instead of collide, and finally compare that behavior with a separate cancel-old CI workflow.
Learning objectives
- Create independent validation jobs and a read-only aggregator that inspects direct dependency results.
-
Use a job-level concurrency group with
queue: maxto serialize fake deployments without dropping accepted runs. - Create overlapping manual runs and capture evidence of running, pending/queued and completed states.
-
Compare queueing with a separate workflow-level
cancel-in-progressexperiment. - Preserve run IDs, attempts, SHAs, job conclusions and first cancellation evidence before cleanup.
1. Preflight: use a disposable repository
Create or select a repository that exists only for this lab. The
workflows use permissions: {}, no checkout action, no
secrets and no external deployment. Record the default branch and
current SHA before adding files.
git status --short
git rev-parse --show-toplevel
git rev-parse HEAD
git branch --show-current
Do not test cancellation or concurrency by pointing these examples at a real deployment. The “deployment” below is only a timed log-producing critical section.
2. Workflow A: parallel validation → fan-in → serialized fake deployment
Save this as .github/workflows/ch11-serialized.yml. The
validation jobs are independent. The aggregator runs even after a
validation failure because it only records evidence. The deployment
runs only when both validation results are successful.
name: ch11-serialized
on:
workflow_dispatch:
inputs:
target:
description: Fake deployment target
required: true
type: choice
options: [staging, qa]
hold_seconds:
description: Seconds to hold the fake deployment lock
required: true
type: number
default: 30
permissions: {}
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- shell: bash
run: |
echo "lint run=$GITHUB_RUN_ID sha=$GITHUB_SHA"
sleep 3
echo "lint=success"
test:
runs-on: ubuntu-24.04
steps:
- shell: bash
run: |
echo "test run=$GITHUB_RUN_ID sha=$GITHUB_SHA"
sleep 5
echo "test=success"
aggregate:
if: ${{ always() }}
needs: [lint, test]
runs-on: ubuntu-24.04
outputs:
ready: ${{ steps.decision.outputs.ready }}
steps:
- id: decision
shell: bash
run: |
printf 'lint=%s\n' '${{ needs.lint.result }}'
printf 'test=%s\n' '${{ needs.test.result }}'
if [[ '${{ needs.lint.result }}' == success && '${{ needs.test.result }}' == success ]]; then
echo 'ready=true' >> "$GITHUB_OUTPUT"
else
echo 'ready=false' >> "$GITHUB_OUTPUT"
fi
deploy:
needs: [aggregate]
if: ${{ needs.aggregate.outputs.ready == 'true' }}
runs-on: ubuntu-24.04
concurrency:
group: ch11-deploy-${{ inputs.target }}
queue: max
steps:
- name: Enter serialized fake deployment
shell: bash
env:
HOLD_SECONDS: ${{ inputs.hold_seconds }}
TARGET: ${{ inputs.target }}
run: |
echo "DEPLOY_START run=$GITHUB_RUN_ID target=$TARGET time=$(date -u +%FT%TZ)"
sleep "$HOLD_SECONDS"
echo "FAKE_TARGET_STATE target=$TARGET revision=$GITHUB_SHA run=$GITHUB_RUN_ID"
echo "DEPLOY_END run=$GITHUB_RUN_ID target=$TARGET time=$(date -u +%FT%TZ)"
The concurrency group is scoped to a fake target, not the whole
repository. staging and qa can proceed
independently; two staging deployments cannot overlap.
3. Predict before dispatch
Write down these predictions before creating any run:
-
Within one run,
lintandtestcan overlap because neither needs the other. -
aggregatestarts only after both direct dependencies finish. -
deploybecomes eligible only whenready=true. -
If two runs target
staging, only onedeployjob can be active in groupch11-deploy-staging. -
With
queue: max, the second eligible deploy should wait rather than cancel the active deploy.
4. Create two overlapping runs
Dispatch run A with target=staging and
hold_seconds=45. As soon as A’s deploy job starts,
dispatch run B with the same target. Use the UI or
gh to record both run IDs.
gh workflow run ch11-serialized.yml -f target=staging -F hold_seconds=45
sleep 10
gh workflow run ch11-serialized.yml -f target=staging -F hold_seconds=10
gh run list --workflow ch11-serialized.yml --limit 5
Do not rely on dispatch order alone. Record when each deploy job actually began waiting on the group and when it entered the critical section.
5. Prove serialization from timestamps
For both runs, capture job status and logs. The critical evidence is
that the second DEPLOY_START is not earlier than the
first DEPLOY_END.
gh run view RUN_A --json databaseId,attempt,headSha,status,conclusion,jobs
gh run view RUN_B --json databaseId,attempt,headSha,status,conclusion,jobs
# After completion:
gh run view RUN_A --log | grep -E 'DEPLOY_(START|END)|FAKE_TARGET_STATE'
gh run view RUN_B --log | grep -E 'DEPLOY_(START|END)|FAKE_TARGET_STATE'
A green result alone is insufficient. Preserve the two run IDs, both SHAs, deploy job timestamps and resolved target/group value.
6. Negative control: different targets should not block one another
Run one staging and one qa deployment with
the same hold time. Their groups resolve to different values, so
overlap is allowed. This proves the lock is scoped to the delivery
invariant rather than globally serializing all deployments.
7. Workflow B: safe cancel-old experiment
Save a second workflow as
.github/workflows/ch11-cancel-old.yml. It represents
disposable validation, not deployment.
name: ch11-cancel-old
on:
workflow_dispatch:
inputs:
hold_seconds:
required: true
type: number
default: 45
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
stale-safe-work:
runs-on: ubuntu-24.04
steps:
- shell: bash
env:
HOLD_SECONDS: ${{ inputs.hold_seconds }}
run: |
echo "START run=$GITHUB_RUN_ID attempt=$GITHUB_RUN_ATTEMPT"
sleep "$HOLD_SECONDS"
echo "END run=$GITHUB_RUN_ID"
Dispatch A, then B before A finishes. Preserve A’s cancellation conclusion and first log. The lesson is not “cancellation is good”; it is “cancellation is safe here because the job has no external side effect.”
8. Interpret cancellation causally
The old run may show a canceled job and an interrupted sleep. GitHub re-evaluates conditions, signals the runner process, escalates termination if needed and eventually enforces a cancellation timeout. Do not treat a 403, runner failure or deployment error as “just cancellation” unless the run/job status supports that conclusion.
9. Challenge: choose the correct layer
A team says: “Only one production deployment may execute at a time, but every approved release must eventually deploy. CI for feature branches should cancel stale runs.” Choose a design without copying the examples verbatim.
Reveal one defensible design
Use job-level production concurrency with a production-specific
group and queue: max for the deployment job. Use a
separate workflow-level branch-scoped group with
cancel-in-progress: true for disposable feature CI.
Do not use the same group for both policies.
10. Cleanup
Delete the disposable workflows/repository when finished. No external target or secret was created. Preserve the run IDs/log excerpts needed for your evidence packet before deleting the lab repository.
Knowledge check
Why does aggregate use always() while deploy does not?
aggregate only records dependency evidence after failures; deploy is a side-effect boundary and should run only when its explicit readiness condition is true.
Two staging deploy jobs have the same group and queue: max. What should the second do?
Wait in the concurrency queue until the active staging deployment leaves the group.
Why run staging and qa simultaneously as a negative control?
It proves the concurrency key is target-scoped rather than accidentally serializing unrelated environments.
What does the canceled validation experiment prove?
That cancel-old semantics can safely discard stale side-effect-free work; it does not prove cancellation is safe for deployments.
What evidence proves serialization better than two green runs?
Resolved group/target plus run IDs and timestamps showing the second critical section began only after the first ended.
Official references and version notes
-
GitHub Docs — workflow syntax:
needs— explicit job dependencies, skip propagation and job-level conditions. -
GitHub Docs —
needscontext — direct-dependency results and outputs. -
GitHub Docs — control workflow/job concurrency
— concurrency groups, cancellation,
queuebehavior and expression contexts. - GitHub Docs — concurrency concepts — simultaneous execution, pending replacement and serialized queues.
- GitHub Docs — workflow cancellation reference — server condition re-evaluation, runner signals and forced termination.
- GitHub Docs — reusable workflow configuration — caller/called-workflow concurrency interaction.
Version-sensitive behavior was rechecked against current
GitHub-maintained documentation on 2026-09-09.
Current documentation states that needs contains only
direct dependencies and exposes result as
success, failure,
cancelled or skipped. A failed or
skipped dependency normally skips downstream jobs unless an
explicit job condition permits continuation. Concurrency groups
are repository-wide and case-insensitive. By default at most one
item may be running and one pending in a group; a newer pending
item replaces an older pending item. Current GitHub Actions also
supports queue: max to allow up to 100 pending items,
processed FIFO by time waiting on the group; this mode cannot be
combined with cancel-in-progress: true. Cancellation
re-evaluates job/step conditions, so always() can
keep work running during cancellation and must not be used
casually for privileged or irreversible operations. Mandatory labs
use ubuntu-24.04, permissions: {}, no
Marketplace action and no real credential.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.