Chapter 19Lesson 02~220 minutes

Environments, Required Reviewers, Protection Rules, and Deployment Gates: Guided Hands-On Workflow

This hands-on lesson turns the Chapter 19 model into observable state. The mandatory path uses one public disposable repository and a one-minute environment wait timer, so it works without a paid plan or second reviewer. The “deployment” writes only a temporary synthetic state file; no cloud account, package registry, Kubernetes cluster, or production credential is involved.

Hands-onWait timerEnvironment secretConcurrencyDeployment evidence

Learning objectives

  • Create a disposable environment with free-compatible protection and synthetic configuration.
  • Route one fake deployment job through that environment and prove secret timing safely.
  • Use target-specific concurrency without cancellation of an in-progress state change.
  • Inspect the run, deployment history, environment URL, and bounded fake target state.
  • Clean up the environment, fake secret, and disposable repository without touching unrelated resources.

1. Preflight: keep every side effect disposable

Create a learner-owned public repository named gha-ch19-lab. Confirm that you are allowed to change its Settings → Environments page. Do not use a company repository, production environment, real deployment token, or real service URL.

Check Expected
Repository Learner-owned, disposable, public
Environment admin You can create/delete lab-staging
Credential Synthetic value only; no provider account
Runner GitHub-hosted ubuntu-24.04
Permissions Workflow default {}; only build needs contents: read
External side effect None; fake target file exists only under RUNNER_TEMP

The lab intentionally uses a synthetic environment secret value such as chapter19-synthetic-v1. Although the value is harmless, still treat it as a secret and never print it. That keeps the workflow shape transferable to real credentials.

2. Create the environment and gate

  1. Open Settings → Environments → New environment.
  2. Create lab-staging.
  3. Enable a 1 minute wait timer. This is the mandatory solo-user protection rule.
  4. Under Environment secrets, add LAB_DEPLOY_TOKEN with a synthetic value.
  5. Under Environment variables, add LAB_TARGET with value training-slot-a.
  6. Optionally restrict deployment branches/tags to your disposable default branch after the first baseline run.

If you have an eligible second collaborator, optionally replace or complement the timer with a required reviewer and enable Prevent self-review. The timer path is sufficient for the mandatory lab and requires no second account.

3. Create the staged fake deployment workflow

# .github/workflows/ch19-environment-lab.yml
name: chapter19-environment-lab

on:
  workflow_dispatch:
    inputs:
      release_label:
        description: Synthetic release label
        required: true
        default: lab-v1
        type: string

permissions: {}

jobs:
  build:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    outputs:
      build_sha256: ${{ steps.evidence.outputs.build_sha256 }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - id: evidence
        shell: bash
        env:
          RELEASE_LABEL: ${{ inputs.release_label }}
        run: |
          set -euo pipefail
          printf 'release=%s\nsource_sha=%s\n' "$RELEASE_LABEL" "$GITHUB_SHA" > build-evidence.txt
          digest=$(sha256sum build-evidence.txt | awk '{print $1}')
          echo "build_sha256=$digest" >> "$GITHUB_OUTPUT"
          echo "run=$GITHUB_RUN_ID attempt=$GITHUB_RUN_ATTEMPT sha=$GITHUB_SHA"
          echo "build_sha256=$digest"

  deploy:
    needs: build
    runs-on: ubuntu-24.04
    permissions: {}
    environment:
      name: lab-staging
      url: https://example.invalid/ch19/${{ github.run_id }}
    concurrency:
      group: deploy-${{ github.repository }}-lab-staging
      cancel-in-progress: false
    env:
      LAB_DEPLOY_TOKEN: ${{ secrets.LAB_DEPLOY_TOKEN }}
      LAB_TARGET: ${{ vars.LAB_TARGET }}
      BUILD_SHA256: ${{ needs.build.outputs.build_sha256 }}
    steps:
      - name: Prove gated configuration without disclosure
        shell: bash
        run: |
          set -euo pipefail
          test -n "$LAB_DEPLOY_TOKEN"
          test -n "$LAB_TARGET"
          echo "credential_state=present"
          echo "target=$LAB_TARGET"
          echo "build_sha256=$BUILD_SHA256"
      - name: Write bounded fake target state
        shell: bash
        run: |
          set -euo pipefail
          TARGET_FILE="$RUNNER_TEMP/ch19-target-state.txt"
          printf 'target=%s\nsource_sha=%s\nbuild_sha256=%s\nrun=%s\n' \
            "$LAB_TARGET" "$GITHUB_SHA" "$BUILD_SHA256" "$GITHUB_RUN_ID" > "$TARGET_FILE"
          cat "$TARGET_FILE"
      - name: Verify target state
        shell: bash
        run: |
          set -euo pipefail
          grep -F "source_sha=$GITHUB_SHA" "$RUNNER_TEMP/ch19-target-state.txt"
          grep -F "build_sha256=$BUILD_SHA256" "$RUNNER_TEMP/ch19-target-state.txt"

The workflow uses no write token and no external integration. GitHub itself creates deployment history because the deploy job references lab-staging. The fake target state is bounded to the hosted runner's temporary filesystem and disappears when the job ends.

4. Predict the state transitions before dispatch

Prediction Expected observation
Build job Starts immediately and records exact SHA/digest
Deploy job before gate Waiting; no privileged step has started
Environment secret Unavailable to protected steps until gate passes
After timer Deploy job starts and logs only credential_state=present
Deployment record Created for lab-staging because deployment defaults true
Target state Temporary file records SHA/digest and disappears with runner

Write down your predictions before clicking Run workflow. A good lab proves the model you expected; it does not merely collect screenshots after the fact.

5. Dispatch and observe the gate

Start one manual run with release_label=lab-v1. The build job should run immediately. The deploy job should enter a waiting state because the one-minute environment timer has not passed. At this point the protected deployment step has not started on a runner and cannot use LAB_DEPLOY_TOKEN.

After the timer expires, the deployment job becomes eligible, starts on a runner, and the environment secret/variable can be injected. The log should reveal only credential_state=present, the non-secret target name, build digest, source SHA, run ID, and bounded fake target content.

6. Inspect the deployment record

Open the repository's Deployments/Environments UI and locate lab-staging. Record the deployment's environment name, source ref/SHA, run link, environment URL, status, and timestamps. A workflow environment reference normally creates a deployment object and status history automatically.

# Optional from your authenticated local terminal
gh api -H "X-GitHub-Api-Version: 2026-03-10" \
  'repos/OWNER/REPO/deployments?environment=lab-staging&per_page=5' \
  --jq '.[] | {id,ref,sha,environment,created_at}'

This query is read-only. Do not use a personal token inside workflow YAML for a task that GitHub's UI or an existing authenticated CLI session can inspect.

7. Prove serialization with overlapping runs

Add a temporary sleep 45 immediately before the fake target write in the disposable branch, commit, and dispatch Run A. After its environment gate passes and the deployment job starts, dispatch Run B. Because both deployment jobs use the same target-specific concurrency group and cancel-in-progress: false, Run B's deployment must wait rather than cancel Run A.

Preserve both run IDs and source SHAs. Remove the temporary sleep in a new commit after the observation. Do not rewrite the first run or pretend the delay was production behavior.

8. Add one ref restriction and test a denial safely

After the baseline succeeds, configure the environment to allow only the disposable default branch. Create a temporary branch ch19-denied, enable workflow_dispatch there if necessary, and attempt the environment job from that branch. Preserve the run showing that the deployment is blocked by the environment ref policy. Then return to the default branch.

This proves that branch/tag restrictions are evaluated as environment authorization, not as build logic. Do not “fix” the denial by changing the runner or token permissions.

9. Optional real reviewer extension

If you have a trusted collaborator in the disposable repository, configure them as a required reviewer and optionally enable Prevent self-review. Dispatch a run and record the pending review state, approver identity, approval time, and subsequent job start. The approver must have at least read access. Only one configured required reviewer needs to approve.

If you do not have a second account, do not weaken account security or create a fake identity. The wait-timer lab already provides a faithful real environment gate; document the reviewer path as an unexecuted optional extension.

10. Compare deployment: false without confusing the model

environment:
  name: lab-staging
  deployment: false

In a separate disposable workflow, this still applies wait timers and required reviewers and still exposes the environment's secrets/variables after authorization, but it does not create a GitHub deployment object. This is useful for environment-scoped CI/configuration access. It is not the version to use for the checkpoint's real deployment-history exercise, and it cannot be combined with custom deployment protection rules.

11. Evidence packet

Evidence Record
Source/run event, ref, SHA, run ID, attempt
Environment name, URL, timer/reviewer/ref rules
Credential/config secret name only + presence result; LAB_TARGET value
Concurrency exact group, Run A/B queue behavior
Deployment deployment ID/status/environment URL if inspected
Target temporary state contents: target, SHA, digest, run ID
Assumptions public disposable repo; no external provider; current plan/visibility behavior

The packet deliberately avoids the secret value. “Credential present after gate” is sufficient evidence for this lab.

12. Cleanup and rollback

  • Remove the temporary sleep and denied-test branch if still present.
  • Delete only the lab-staging environment created for this lab after preserving evidence. Deleting an environment also deletes its associated environment secrets and protection rules, and any jobs still waiting on those rules will fail.
  • Delete the disposable repository when you no longer need it.
  • No cloud/resource credential revocation is required because the secret was synthetic and no external provider existed.

13. Small design challenge

You now need both staging and production. Staging may accept the default branch after a one-minute timer; production requires a human reviewer and must never overlap with another production rollout. Decide which controls belong in the trigger, environment branch/tag policy, required reviewers, concurrency group, and artifact-verification step. Explain why one global concurrency key for both targets would unnecessarily serialize unrelated work.

Next lesson

Environments, Required Reviewers, Protection Rules, and Deployment Gates: Configuration, Design Patterns, and Trade-Offs

Continue with the next lesson to build on the current concepts, evidence, security boundaries, and operational practices.

Knowledge check

During the one-minute wait timer, should the deploy job be able to read LAB_DEPLOY_TOKEN?

Why is the fake target file written under RUNNER_TEMP?

Run B waits behind Run A. Which mechanism caused that?

A temporary branch is blocked from lab-staging. Should you increase token permissions?

Why does the evidence packet record deployment history and fake target state separately?

Official references and version notes

Version-sensitive GitHub Actions behavior in this lesson was rechecked on 2026-09-10. Re-verify current plan, repository visibility, API, environment, and protection-rule behavior before production rollout.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.