Checkpoint Lab — Job Dependencies, needs, Concurrency, Cancellation, and Deployment Serialization
The checkpoint requires two kinds of proof: live GitHub Actions evidence for job graph and concurrency behavior, and a deterministic local shared-target simulation that makes the final fake deployment state independently inspectable. No cloud, secret, package, release or production system is required.
Learning objectives
- Predict every job edge and concurrency outcome before starting overlapping runs.
- Execute a queue-preserving serialized fake deployment and a separate cancel-old safe-work policy.
- Use a local deterministic simulator to prove final shared target state under overlapping serialized runs.
- Preserve a complete evidence packet with run IDs/attempts/SHAs, direct dependency results, group values, timestamps and target state.
- Document cleanup, limitations and the production controls that cancellation/concurrency cannot replace.
1. Scenario and invariants
A sample service has two validation jobs and one fake deployment. The staging target must accept only one deployment at a time, and every accepted staging request must execute. Separately, a long-running analysis job is disposable: a newer analysis run should cancel an older one.
State the invariants before writing YAML:
- Validation jobs may run in parallel.
- Deployment is eligible only after both validation jobs succeed.
- Staging deployments serialize and queue; they do not cancel active deployments.
- Analysis runs may cancel stale analysis work.
- Cancellation never counts as rollback of an already committed target state.
2. Preflight and assumptions
- Disposable GitHub repository under your control.
-
GitHub Actions enabled; standard hosted runner
ubuntu-24.04. - No secrets, environments, cloud accounts, packages or releases.
ghCLI optional; browser UI is sufficient.- Python 3 standard library for the local shared-target simulation.
- Version-sensitive concurrency behavior rechecked on 2026-09-09.
git status --short
git rev-parse HEAD
git branch --show-current
python --version
gh --version 2>/dev/null || true
3. Exact checkpoint workflow
Save as .github/workflows/ch11-checkpoint.yml.
name: ch11-checkpoint
on:
workflow_dispatch:
inputs:
policy:
description: Which concurrency policy to exercise?
required: true
type: choice
options: [queue-deploy, cancel-analysis]
target:
required: true
type: choice
options: [staging, qa]
hold_seconds:
required: true
type: number
default: 35
permissions: {}
jobs:
lint:
runs-on: ubuntu-24.04
steps:
- run: |
echo "lint run=$GITHUB_RUN_ID sha=$GITHUB_SHA"
sleep 2
test:
runs-on: ubuntu-24.04
steps:
- run: |
echo "test run=$GITHUB_RUN_ID sha=$GITHUB_SHA"
sleep 4
gate:
if: ${{ always() }}
needs: [lint, test]
runs-on: ubuntu-24.04
outputs:
ready: ${{ steps.gate.outputs.ready }}
steps:
- id: gate
shell: bash
run: |
echo "lint=${{ needs.lint.result }} test=${{ 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
queue_deploy:
if: ${{ inputs.policy == 'queue-deploy' && needs.gate.outputs.ready == 'true' }}
needs: [gate]
runs-on: ubuntu-24.04
concurrency:
group: ch11-checkpoint-deploy-${{ inputs.target }}
queue: max
steps:
- shell: bash
env:
HOLD: ${{ inputs.hold_seconds }}
TARGET: ${{ inputs.target }}
run: |
echo "LOCK_ENTER run=$GITHUB_RUN_ID target=$TARGET time=$(date -u +%FT%TZ)"
sleep "$HOLD"
echo "TARGET_COMMIT run=$GITHUB_RUN_ID sha=$GITHUB_SHA target=$TARGET"
echo "LOCK_EXIT run=$GITHUB_RUN_ID target=$TARGET time=$(date -u +%FT%TZ)"
cancel_analysis:
if: ${{ inputs.policy == 'cancel-analysis' && needs.gate.outputs.ready == 'true' }}
needs: [gate]
runs-on: ubuntu-24.04
concurrency:
group: ch11-checkpoint-analysis-${{ inputs.target }}
cancel-in-progress: true
steps:
- shell: bash
env:
HOLD: ${{ inputs.hold_seconds }}
run: |
echo "ANALYSIS_START run=$GITHUB_RUN_ID time=$(date -u +%FT%TZ)"
sleep "$HOLD"
echo "ANALYSIS_END run=$GITHUB_RUN_ID time=$(date -u +%FT%TZ)"
4. Prediction sheet
Before dispatching, create predictions.md with at least
these entries:
- lint/test overlap for each run.
- gate waits for both and reports direct results.
- queue-deploy A enters staging lock; queue-deploy B waits; neither is canceled.
- cancel-analysis C begins; D with same target cancels C and eventually runs.
- No GitHub job writes a real external target.
- The local simulator’s final target will equal the last serialized simulated commit.
5. Execute the queue-preserving policy
-
Dispatch run A:
policy=queue-deploy,target=staging,hold_seconds=45. - Wait until A’s
queue_deployenters the lock. -
Dispatch run B with the same target and
hold_seconds=10. - Record both run IDs, attempts and SHAs.
- Verify B’s deploy does not enter before A exits.
Then dispatch a qa run while staging is active; it
should use a different group and need not wait for staging.
6. Execute the cancel-old policy
-
Dispatch run C:
policy=cancel-analysis,target=staging,hold_seconds=45. -
After
ANALYSIS_START, dispatch run D with the same policy/target. - Preserve C’s run ID and canceled conclusion.
- Verify D reaches completion.
- State why this is safe: the analysis section has no external side effect.
7. Mandatory shared-target simulator
GitHub-hosted jobs do not share a local filesystem across runs. To
verify a concrete final target without introducing cloud credentials
or repository writes, run this faithful local simulator. It models
two accepted deployment runs waiting on one lock and writing a
shared target.json.
from pathlib import Path
from threading import Thread, Lock
from time import sleep, time
import json
lock = Lock()
evidence = Path("ch11-sim-evidence.jsonl")
target = Path("target.json")
for p in (evidence, target):
if p.exists(): p.unlink()
def emit(run, state, **data):
row = {"t": round(time(), 3), "run": run, "state": state, **data}
with evidence.open("a", encoding="utf-8") as f:
f.write(json.dumps(row) + "\n")
print(row)
def pipeline(run, sha, validation_delay, deploy_delay):
emit(run, "lint_start")
sleep(validation_delay / 2)
emit(run, "lint_success")
emit(run, "test_start")
sleep(validation_delay / 2)
emit(run, "test_success")
emit(run, "gate_ready", lint="success", test="success")
emit(run, "deploy_wait", group="staging")
with lock:
emit(run, "deploy_enter", group="staging")
sleep(deploy_delay)
target.write_text(json.dumps({"target": "staging", "run": run, "sha": sha}), encoding="utf-8")
emit(run, "target_commit", sha=sha)
emit(run, "deploy_exit", group="staging")
A = Thread(target=pipeline, args=("A", "sha-A", 0.6, 1.5))
B = Thread(target=pipeline, args=("B", "sha-B", 0.6, 0.2))
A.start(); sleep(0.4); B.start()
A.join(); B.join()
print("FINAL_TARGET", target.read_text(encoding="utf-8"))
python ch11_sim.py
cat ch11-sim-evidence.jsonl
cat target.json
Expected evidence: both runs complete validation; B reaches
deploy_wait while A owns the lock; A commits and exits;
B enters and commits; final target is run B / sha-B.
This is a faithful mutual-exclusion and shared-target simulation,
not a claim about GitHub’s external deployment API.
8. Cancellation thought experiment against target state
Take the simulator timeline and imagine B cancels A after A
writes target.json. Stopping A cannot erase that write.
The only valid rollback is another explicit target operation. Record
this as a limitation and production requirement.
9. Evidence packet
Create a folder containing:
-
workflow.yml— exact checkpoint workflow revision. -
predictions.md— predictions written before dispatch. -
runs.md— A/B/C/D run IDs, attempts, SHAs, statuses and conclusions. -
job-graph.md— lint/test → gate → policy-specific job. -
concurrency.md— resolved group keys, queue/cancel policy and observed wait/cancel timestamps. -
queue-logs.txt— lock enter/exit/target-commit lines. -
cancel-logs.txt— C cancellation and D completion evidence. -
ch11-sim-evidence.jsonlandtarget.json— shared-target proof. -
assumptions-limitations.md— no environment approval, cloud provider, package, artifact or real deployment was exercised.
10. Verification checklist
- Every run has exact ID, attempt and SHA.
-
The DAG is drawn from
needs, not YAML position. - Both direct dependency results are preserved.
- Staging queue runs never overlap their critical sections.
- QA can overlap staging because the group differs.
- Cancel-analysis old run is canceled and newer run completes.
- No privileged
always()job exists. - Shared-target simulator ends with the predicted final target.
- No real credential or production target is used.
11. Cleanup and rollback
Delete the disposable workflows/repository after preserving
evidence. Remove target.json and simulator logs from
the temporary local lab directory. No GitHub environment, runner,
secret, package, release or cloud resource was created, so there is
no hidden external cleanup.
12. What Chapter 11 adds to the operating model
Chapter 11 adds a job-graph and critical-section contract: dependencies are explicit, direct results are observable, concurrency keys encode the smallest real exclusion domain, queue/cancel policies are chosen from side-effect semantics, cancellation is never confused with rollback, and evidence correlates GitHub run state with the external target.
Chapter 12 expands the parallel side of this model. Instead of manually declaring a few peer jobs, you will use matrix strategies and dynamic matrices to generate controlled parallel work while preserving failure, cost and evidence semantics.
Knowledge check
In the checkpoint, which jobs are allowed to run in parallel?
lint and test, and deployments to different target groups such as staging and qa. Same-target queue deployments are serialized.
Why are queue-deploy and cancel-analysis separate jobs/groups?
They encode different delivery invariants; queueing preserves accepted deployment work, while cancel-old safely discards stale side-effect-free analysis.
What proves the final fake target state?
The local simulator evidence plus target.json, which provides a shared target without introducing external credentials.
If run A is canceled after TARGET_COMMIT, what must you assume?
The committed target state remains until an explicit compensation or later deployment changes it.
What is the bridge to Chapter 12?
The DAG now has correct dependency and exclusion semantics; matrices will generate larger controlled sets of parallel jobs within that model.
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. The checkpoint
deliberately separates GitHub concurrency evidence from a local
shared-target simulator so the mandatory path remains
credential-free and disposable.
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.