Chapter 19Lesson 05~240 minutes

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.

CheckpointPromotionProtected productionFailure evidenceRollback runbook

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-256 and embedded Source commit from 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

  1. Preflight: explicit host/repository/environment, environment rule snapshot, target health, retention of known-good artifact.
  2. Candidate packet: run ID, orchestration head SHA, artifact source SHA, artifact digest, change reason, test evidence.
  3. Approval: reviewer confirms exact candidate packet and target; production policy prevents self-review where separation is required.
  4. Concurrency: production state writers share one namespaced group; queue non-interruptible changes.
  5. Activation: verify artifact digest again immediately before target mutation; external target IAM remains authoritative.
  6. Verification: deployment status, logs, target health, target audit evidence.
  7. Rollback: choose compatible retained known-good digest; trigger a new governed deployment; preserve failed evidence.
  8. 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?

A production job is approved but its digest differs from staging. What should happen?

Why does the checkpoint use queueing for production instead of cancel-in-progress?

What is wrong with deleting the failed candidate run after rollback succeeds?

A typo environment has no reviewer but the real production environment does. Is the artifact digest alone enough to proceed?

What production improvement should replace the checkpoint’s self-reviewable reviewer configuration?

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.

Next chapter

Secrets, Configuration Variables, GITHUB_TOKEN, Fine-Grained Permissions, and OIDC: Concepts, Architecture, and Mental Model

Further reading

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.