Checkpoint Lab — Environments, Deployment Protection Rules, Approvals, Concurrency, and Rollbacks
The checkpoint turns Chapter 19 into an operating model. You will create two disposable environments, generate and hash a deployable payload once, promote those same bytes through staging and protected production, deliberately fail a new candidate, prove exactly what failed, and roll back by redeploying the earlier retained payload through the same governance path.
Learning objectives
- Build a complete two-environment deployment simulation with explicit source/run/artifact identity.
- Predict and verify environment, deployment, approval, concurrency, and artifact state changes.
- Preserve a controlled failed deployment and diagnose it from independent evidence.
- Execute rollback as a new deployment of retained known-good bytes rather than a rebuild or history rewrite.
- Write a deployment/approval/rollback runbook with free and plan-dependent alternatives.
Availability: Required: GitHub.com account, GitHub CLI, Git, and a public disposable personal repository on GitHub Free or higher. The learner must own/admin the lab repository to configure environments. Production protection uses the learner as reviewer with self-review permitted only for the single-user simulation; a real production policy should use a different reviewer/team and prevent self-review. No real cloud/runtime, secret, package, or self-hosted runner is required.
1. Setup and preflight
Use a fresh repository even if Lesson 2 was completed. This makes every state transition reproducible.
gh auth status
OWNER="$(gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq .login)"
REPO="$OWNER/c19-deployment-checkpoint"
gh repo create "$REPO" --public --clone --add-readme
cd c19-deployment-checkpoint
gh repo view "$REPO" --json nameWithOwner,visibility,defaultBranchRef --jq '{repo:.nameWithOwner,visibility,default_branch:.defaultBranchRef.name}'
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/environments" --jq '{total:.total_count,names:[.environments[].name]}'
Prediction A: environment count begins at zero. Prediction B: after the workflow is configured and run, staging and production appear in deployment history as distinct environment names. Prediction C: production steps cannot begin before required review. Record these predictions.
2. Configure the two environments
Set the checkpoint repository variable, then create the same environment policy used in the guided lab. The variable exists only for your shell; it is not a GitHub secret.
REPO="OWNER/c19-deployment-checkpoint"
export ACTOR_ID="$(gh api -H "X-GitHub-Api-Version: 2026-03-10" user --jq .id)"
gh api --method PUT -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/environments/staging" --input - <<'JSON'
{}
JSON
python - <<'PYJSON' > /tmp/production-env.json
import json, os
print(json.dumps({
"wait_timer": 0,
"prevent_self_review": False,
"reviewers": [{"type": "User", "id": int(os.environ["ACTOR_ID"])}],
"deployment_branch_policy": None
}))
PYJSON
gh api --method PUT -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/environments/production" --input /tmp/production-env.json
rm -f /tmp/production-env.json
Verify the production gate before creating the workflow:
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/environments/production" --jq '{name,protection_rules,deployment_branch_policy}'
3. Install the governed deployment workflow
Use the same workflow contract as Lesson 2. Its invariant is:
staging and production must verify the same SHA-256 value emitted
by acquire. Rollback changes the source of the payload (prior run rather than
rebuild) but not that invariant.
name: Governed deployment lab
on:
workflow_dispatch:
inputs:
source_run_id:
description: "Prior successful run ID for rollback; leave empty for a new build"
required: false
type: string
expected_sha256:
description: "Required when rolling back: SHA-256 of payload.txt from the source run"
required: false
type: string
inject_failure:
description: "Fail the production activation after identity verification"
required: true
default: false
type: boolean
permissions:
contents: read
actions: read
jobs:
acquire:
runs-on: ubuntu-24.04
outputs:
payload_sha: ${{ steps.identity.outputs.payload_sha }}
source_sha: ${{ steps.identity.outputs.source_sha }}
steps:
- name: Checkout current source for a new deployment
if: ${{ inputs.source_run_id == '' }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Build deterministic lab payload
if: ${{ inputs.source_run_id == '' }}
shell: bash
run: |
set -euo pipefail
mkdir -p deploy
printf 'source_sha=%s\n' "$GITHUB_SHA" > deploy/payload.txt
printf 'payload_schema=c19-v1\n' >> deploy/payload.txt
- name: Download retained known-good payload for rollback
if: ${{ inputs.source_run_id != '' }}
env:
GH_TOKEN: ${{ github.token }}
SOURCE_RUN_ID: ${{ inputs.source_run_id }}
shell: bash
run: |
set -euo pipefail
mkdir -p deploy
gh run download "$SOURCE_RUN_ID" -R "$GITHUB_REPOSITORY" --name c19-deployable --dir deploy
- name: Verify and expose immutable payload identity
id: identity
env:
EXPECTED_SHA: ${{ inputs.expected_sha256 }}
IS_ROLLBACK: ${{ inputs.source_run_id != '' }}
shell: bash
run: |
set -euo pipefail
test -f deploy/payload.txt
actual="$(sha256sum deploy/payload.txt | awk '{print $1}')"
source_sha="$(sed -n 's/^source_sha=//p' deploy/payload.txt)"
test -n "$source_sha"
if [[ "$IS_ROLLBACK" == "true" ]]; then
test -n "$EXPECTED_SHA"
test "$actual" = "$EXPECTED_SHA"
fi
echo "payload_sha=$actual" >> "$GITHUB_OUTPUT"
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "Payload SHA-256: $actual"
echo "Source commit: $source_sha"
- name: Retain deployable payload in this run
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: c19-deployable
path: deploy/payload.txt
if-no-files-found: error
retention-days: 7
staging:
needs: acquire
runs-on: ubuntu-24.04
environment: staging
concurrency:
group: c19-staging-deploy
cancel-in-progress: true
steps:
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3
with:
name: c19-deployable
path: deploy
- name: Verify then simulate staging activation
env:
EXPECTED: ${{ needs.acquire.outputs.payload_sha }}
shell: bash
run: |
set -euo pipefail
actual="$(sha256sum deploy/payload.txt | awk '{print $1}')"
test "$actual" = "$EXPECTED"
echo "staging activated payload=$actual"
production:
needs: [acquire, staging]
runs-on: ubuntu-24.04
environment: production
concurrency:
group: c19-production-deploy
queue: max
steps:
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3
with:
name: c19-deployable
path: deploy
- name: Verify approved payload identity
env:
EXPECTED: ${{ needs.acquire.outputs.payload_sha }}
SOURCE_SHA: ${{ needs.acquire.outputs.source_sha }}
shell: bash
run: |
set -euo pipefail
actual="$(sha256sum deploy/payload.txt | awk '{print $1}')"
test "$actual" = "$EXPECTED"
grep -Fx "source_sha=$SOURCE_SHA" deploy/payload.txt
echo "approved payload=$actual source=$SOURCE_SHA"
- name: Simulate production activation
env:
INJECT_FAILURE: ${{ inputs.inject_failure }}
shell: bash
run: |
set -euo pipefail
if [[ "$INJECT_FAILURE" == "true" ]]; then
echo "Intentional deployment failure after identity verification" >&2
exit 42
fi
echo "production activation succeeded"
mkdir -p .github/workflows
# Save the YAML above as .github/workflows/governed-deploy.yml
git add .github/workflows/governed-deploy.yml
git commit -m "Add Chapter 19 checkpoint deployment"
git push origin HEAD
gh workflow view governed-deploy.yml -R "$REPO"
4. Baseline release: prove promotion of one artifact
Dispatch:
gh workflow run governed-deploy.yml -R "$REPO" -f source_run_id='' -f expected_sha256='' -f inject_failure=false
Open the run. Before approval, inspect acquire and
staging. Capture:
- run ID and workflow
headSha; -
Payload SHA-256and embeddedSource commitfrom acquire; - successful staging digest check;
- production environment name and required-review waiting state.
Only then approve production. After success:
GOOD_RUN="$(gh run list -R "$REPO" --workflow governed-deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run view "$GOOD_RUN" -R "$REPO" --log > checkpoint-good.log
GOOD_SHA="$(grep -m1 'Payload SHA-256:' checkpoint-good.log | awk '{print $NF}')"
GOOD_SOURCE="$(grep -m1 'Source commit:' checkpoint-good.log | awk '{print $NF}')"
printf 'GOOD_RUN=%s
GOOD_SOURCE=%s
GOOD_SHA=%s
' "$GOOD_RUN" "$GOOD_SOURCE" "$GOOD_SHA"
Independent verification: download the artifact outside the workflow and hash it:
rm -rf verify-good && mkdir verify-good
gh run download "$GOOD_RUN" -R "$REPO" --name c19-deployable --dir verify-good
sha256sum verify-good/payload.txt
grep -Fx "source_sha=$GOOD_SOURCE" verify-good/payload.txt
The external SHA-256 must equal GOOD_SHA. This proves
the persisted artifact matches the logged identity.
5. Candidate 2: inject a failure and preserve it
Create a new commit so the candidate cannot be confused with the baseline:
printf 'candidate=2
' > candidate.txt
git add candidate.txt
git commit -m "Create checkpoint candidate 2"
git push origin HEAD
gh workflow run governed-deploy.yml -R "$REPO" -f source_run_id='' -f expected_sha256='' -f inject_failure=true
Inspect candidate 2's digest/source before approval, approve it, and let the production activation fail intentionally. Do not rerun it until green and do not delete the failed run. Record its run ID, head SHA, payload digest, embedded source SHA, environment, and exit-42 message.
Prediction D: the failed deployment's artifact
digest differs from GOOD_SHA because the source commit
changed. Verify from logs.
6. Execute rollback as a new governed deployment
Dispatch a fresh run that names the retained known-good run and digest:
gh workflow run governed-deploy.yml -R "$REPO" -f source_run_id="$GOOD_RUN" -f expected_sha256="$GOOD_SHA" -f inject_failure=false
Before approving production, confirm the rollback
acquire log prints GOOD_SHA and
GOOD_SOURCE. Approve the new production deployment,
then verify success.
ROLLBACK_RUN="$(gh run list -R "$REPO" --workflow governed-deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run view "$ROLLBACK_RUN" -R "$REPO" --log > checkpoint-rollback.log
grep -F "Payload SHA-256: $GOOD_SHA" checkpoint-rollback.log
grep -F "Source commit: $GOOD_SOURCE" checkpoint-rollback.log
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/deployments?environment=production&per_page=20" --jq '[.[] | {id,environment,sha,ref,created_at}]'
Notice the subtle but correct identity model: the rollback
workflow's current headSha identifies the orchestration
revision, while the retained payload says
source_sha=$GOOD_SOURCE. The deployment is new; the
payload is old and verified. That is rollback without pretending
time moved backward.
7. Write a deployment/approval test matrix
| Case | Expected gate/concurrency | Expected artifact behavior | Expected result |
|---|---|---|---|
| New baseline | Staging may supersede stale staging; production waits reviewer and queues. | Build once; same SHA-256 in staging and production. | Success. |
| New candidate with injected failure | Same protection applies. | New commit → new payload SHA-256; identity verifies before failure. | Production failure preserved. |
| Rollback | Same production review/queue unless documented incident policy says otherwise. | Download prior run; required expected digest; no rebuild. | New successful deployment of old bytes. |
| Environment typo | Unexpected environment object may be created without production rules. | Artifact identity may still be valid but governance boundary is wrong. | Treat as policy failure; correct name. |
| Concurrent production dispatches | Same c19-production-deploy group. |
Each candidate retains separate identity. | Queued/serialized; do not overlap target mutation. |
8. Production-style runbook
- Preflight: explicit host/repository/environment, environment rule snapshot, target health, retention of known-good artifact.
- Candidate packet: run ID, orchestration head SHA, artifact source SHA, artifact digest, change reason, test evidence.
- Approval: reviewer confirms exact candidate packet and target; production policy prevents self-review where separation is required.
- Concurrency: production state writers share one namespaced group; queue non-interruptible changes.
- Activation: verify artifact digest again immediately before target mutation; external target IAM remains authoritative.
- Verification: deployment status, logs, target health, target audit evidence.
- Rollback: choose compatible retained known-good digest; trigger a new governed deployment; preserve failed evidence.
- Incident exception: if bypass is truly necessary, record actor/reason/candidate identity and restore normal controls afterward.
Plan-dependent alternative: when private/internal repository protections are unavailable on the current plan, do not silently omit them. Use a public disposable learning lab; in production, choose an eligible plan or implement an equivalent external approval/IAM system with explicit evidence. Custom protection rules are optional/public preview and never required for this checkpoint.
9. Verification checklist
- Exactly two intended environment names exist; no typo environment exists.
- Production has the expected required-reviewer protection rule.
- Baseline staging and production logs verify the same payload digest.
- Candidate 2 failure occurs only after identity verification and remains visible.
-
Rollback downloads the baseline artifact by prior run ID and
requires
GOOD_SHA. - Rollback production creates a new deployment record rather than rewriting Git history.
- No secrets, tokens, whole contexts, or external production credentials appear in logs/artifacts.
- Production concurrency uses a namespaced serialized queue; unrelated workflows do not share the group.
10. Cleanup / rollback of lab infrastructure
Preserve your evidence files locally if you want them, then remove environment resources only in this disposable repository and archive it:
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/environments" --jq '.environments[] | {name,protection_rules}'
gh api --method DELETE -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/environments/staging"
gh api --method DELETE -H "X-GitHub-Api-Version: 2026-03-10" "repos/$REPO/environments/production"
gh repo archive "$REPO" --yes
Environment deletion removes environment secrets/rules and would fail jobs still waiting on those rules, so never use this cleanup sequence on production infrastructure.
Knowledge checks
During rollback, why can the workflow head SHA be newer than the source SHA embedded in the artifact?
The current workflow revision orchestrates the rollback, while the retained artifact was built from the prior known-good source SHA. Both identities are valid and must be recorded separately.
A production job is approved but its digest differs from staging. What should happen?
Do not activate it. Treat it as a different artifact, preserve the evidence, and require a corrected candidate/promotion path whose digest matches the approved/tested bytes.
Why does the checkpoint use queueing for production instead of cancel-in-progress?
Production changes are modeled as state transitions that should complete serially; canceling an in-flight mutation can leave partial state. Queueing preserves serialized intent.
What is wrong with deleting the failed candidate run after rollback succeeds?
It removes operational evidence needed to explain the incident. The failed run, attempted digest/source, approval, and logs are part of the audit/learning record.
A typo environment has no reviewer but the real production environment does. Is the artifact digest alone enough to proceed?
No. Artifact identity and environment authorization are separate invariants. Correct the environment name and re-run through the intended protection boundary.
What production improvement should replace the checkpoint’s self-reviewable reviewer configuration?
Use a separate reviewer/team and enable prevent-self-review so initiation and approval are different authorities; keep emergency exceptions documented rather than normalizing self-approval.
Chapter 19 production model
You can now model deployment as a governed transition rather than an opaque workflow step: candidate identity is explicit, environments gate target access, production writers are serialized, approvals bind to inspectable evidence, failures remain observable, and rollback is another controlled deployment of known-good bytes.
Bridge to Chapter 20: Chapter 19 intentionally used
no real deployment secrets. Chapter 20 now deepens the identity
plane: secrets, configuration variables,
GITHUB_TOKEN permissions, fine-grained access, and
OIDC-based short-lived cloud credentials.
Further reading
- GitHub Docs — Deployments and environments
- GitHub Docs — Managing environments for deployment
- GitHub Docs — Reviewing deployments
- GitHub Docs — Deploying with GitHub Actions
- GitHub Docs — Control workflow/job concurrency
- GitHub REST — Deployment environments (2026-03-10)
- GitHub REST — Deployments
- GitHub REST — Deployment statuses
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.