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.
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
- Open Settings → Environments → New environment.
- Create
lab-staging. - Enable a 1 minute wait timer. This is the mandatory solo-user protection rule.
-
Under Environment secrets, add
LAB_DEPLOY_TOKENwith a synthetic value. -
Under Environment variables, add
LAB_TARGETwith valuetraining-slot-a. - 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-stagingenvironment 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.
Knowledge check
During the one-minute wait timer, should the deploy job be able
to read LAB_DEPLOY_TOKEN?
No. The environment rule must pass before the job starts and environment secrets become available.
Why is the fake target file written under
RUNNER_TEMP?
It bounds the side effect to the disposable hosted runner. The exercise can model target mutation without touching any real deployment system.
Run B waits behind Run A. Which mechanism caused that?
The shared target-specific concurrency group with
cancel-in-progress: false, not the environment
approval rule itself.
A temporary branch is blocked from lab-staging.
Should you increase token permissions?
No. The failure is an environment ref-policy denial. Permission widening would not address the causal layer and would weaken the workflow.
Why does the evidence packet record deployment history and fake target state separately?
The deployment record proves GitHub control-plane history; the target-state check proves the bounded deployment effect. Neither substitutes for the other.
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.
- GitHub Docs — Deployments and environments
- GitHub Docs — Managing environments for deployment
- GitHub Docs — Reviewing deployments
- GitHub Docs — Deploying with GitHub Actions
- GitHub Docs — Deploying to a specific environment
-
GitHub Docs — Workflow syntax: jobs.
.environment - GitHub Docs — REST API for deployment environments
- GitHub Docs — REST API for deployments
- GitHub Docs — REST API for deployment statuses
- GitHub Docs — Secrets reference
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.