Chapter 30Lesson 05~250 minutes

Checkpoint Lab — Codespaces, Dev Containers, GitHub Pages, Documentation, and Cloud Developer Environments

Integrate a tiny documentation project, devcontainer metadata, local verification, Pages publishing, controlled lifecycle failure, remediation, and a production cloud-development policy.

Checkpoint labDocumentation as codeDeployment evidenceFailure drillCloud-dev policy

Checkpoint outcomes

  • Build a clean documentation project and dev-container configuration, then record predictions before hosted changes.
  • Verify identical source content locally and through GitHub Pages, with the Pages build tied to an exact commit.
  • Inject a harmless lifecycle failure, preserve the original exit status/cause, repair it, and verify a clean command.
  • Optionally inspect the same commit in a Codespace without requiring paid quota or real development secrets.
  • Produce a cloud-development policy covering repository trust, secret scope, ports, machine/idle/retention cost, prebuilds, Pages provenance, and cleanup.

1. Scenario, preflight, and required assumptions

You are handing a small documentation service to another team. They need a portable dev-container definition, a public Pages site, and a policy that keeps cloud development from silently accumulating authority or cost. The mandatory path uses a disposable public personal repository; a Codespace remains optional after quota/billing verification.

Mandatory prerequisites: GitHub Free account, GitHub CLI authenticated, Git, Python 3, permission to create one disposable public repository, and owner/admin authority over that repository for the Pages configuration. No real secret, custom domain, organization, paid Codespaces usage, or self-hosted runner is required.
Destructive boundary: Codespace deletion and Pages unpublishing are cleanup actions only. Never perform the commands in this checkpoint against a valuable repository or an environment containing unpushed work.

2. Create the checkpoint repository and deterministic source

gh auth status
OWNER="$(gh api -H 'X-GitHub-Api-Version: 2026-03-10' user --jq .login)"
REPO="github-cloud-dev-checkpoint"
FULL="$OWNER/$REPO"

gh repo create "$FULL" --public --clone --description "Disposable Chapter 30 checkpoint"
cd "$REPO"
mkdir -p docs .devcontainer policy evidence

cat > docs/index.html <<'HTML'
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Chapter 30 checkpoint</title></head>
<body><main><h1>Chapter 30 checkpoint</h1><p>Repository-defined documentation environment.</p></main></body>
</html>
HTML

cat > .devcontainer/devcontainer.json <<'JSON'
{
  "name": "chapter-30-checkpoint",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "forwardPorts": [8000],
  "portsAttributes": {
    "8000": {"label": "docs preview", "onAutoForward": "notify"}
  },
  "postCreateCommand": "test -f docs/index.html && printf 'docs ready\n'"
}
JSON

python -m json.tool .devcontainer/devcontainer.json >/dev/null
git add docs .devcontainer
git commit -m "Create Chapter 30 checkpoint source"
git push -u origin HEAD
SOURCE_SHA="$(git rev-parse HEAD)"
printf '%s
' "$SOURCE_SHA" | tee evidence/source-sha.txt

This configuration deliberately contains no extra repository permission request, feature, privileged option, public port, or secret. The exercise is about traceability, not tool density.

3. Predict state transitions before performing them

Create evidence/predictions.md and write at least these predictions in your own words before continuing:

  1. Pages prediction: creating the Pages resource will add hosted configuration pointing to main:/docs; it will not change the Git commit. The latest completed Pages build should eventually reference SOURCE_SHA.
  2. Lifecycle-failure prediction: changing a local copy of postCreateCommand from docs/index.html to docs/missing.html will make that command exit nonzero without changing any hosted Codespaces resource.
  3. Codespace prediction (optional): creating a Codespace will allocate hosted compute/storage and a Codespace object; stopping it ends active compute but retained storage remains until deletion/retention expiry.
cat > evidence/predictions.md <<EOF
# Predictions
- Pages source will become main:/docs without changing Git source commit $SOURCE_SHA.
- A missing-file lifecycle check will fail locally with nonzero status and no hosted Codespace mutation.
- If I create a Codespace, it will create hosted runtime/storage state; stopping and deleting are separate lifecycle actions.
EOF

git add evidence/predictions.md
git commit -m "Record checkpoint predictions"
git push
SOURCE_SHA="$(git rev-parse HEAD)"
printf '%s
' "$SOURCE_SHA" > evidence/source-sha.txt

The second commit intentionally becomes the new source identity. Always recalculate the expected commit after committing evidence or policy files; otherwise your own evidence packet becomes stale.

4. Verify content and lifecycle locally

python -m json.tool .devcontainer/devcontainer.json >/dev/null

# Execute only the harmless command body used by postCreateCommand.
sh -lc "test -f docs/index.html && printf 'docs ready\n'"

python -m http.server 8000 --directory docs >evidence/local-server.log 2>&1 &
SERVER_PID=$!
sleep 1
curl --fail --silent http://127.0.0.1:8000/ > evidence/local-index.html
kill "$SERVER_PID"
grep -F "Chapter 30 checkpoint" evidence/local-index.html
sha256sum docs/index.html evidence/local-index.html | tee evidence/content-hashes.txt

The two hash values should match because the HTTP server returns the exact static file. This is a local content invariant that you can compare with the hosted page later.

5. Publish through Pages and prove source commit

cat > /tmp/pages-source.json <<'JSON'
{"source":{"branch":"main","path":"/docs"}}
JSON

gh api --method POST -H "X-GitHub-Api-Version: 2026-03-10"   "repos/$FULL/pages" --input /tmp/pages-source.json
rm -f /tmp/pages-source.json

gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$FULL/pages"   --jq '{status,html_url,build_type,source,https_enforced}' | tee evidence/pages-config.json

# Query manually after the build has had time to finish.
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$FULL/pages/builds/latest"   --jq '{status,commit,created_at,updated_at,error}' | tee evidence/pages-build.json

HOSTED_SHA="$(gh api -H 'X-GitHub-Api-Version: 2026-03-10' "repos/$FULL/pages/builds/latest" --jq .commit)"
EXPECTED_SHA="$(git rev-parse origin/main)"
printf 'expected=%s
hosted=%s
' "$EXPECTED_SHA" "$HOSTED_SHA"
test "$EXPECTED_SHA" = "$HOSTED_SHA"

PAGE_URL="$(gh api -H 'X-GitHub-Api-Version: 2026-03-10' "repos/$FULL/pages" --jq .html_url)"
curl --fail --silent --location "$PAGE_URL" > evidence/hosted-index.html
grep -F "Chapter 30 checkpoint" evidence/hosted-index.html

If status is still queued/building, do not mark the checkpoint failed; wait and query again. The acceptance criterion is the completed build/deployment identity plus content, not a fixed number of seconds.

6. Failure injection: break the lifecycle command without allocating a broken cloud machine

The mandatory path performs the failure on a temporary local copy. This teaches the causal signal without wasting Codespaces quota.

cp .devcontainer/devcontainer.json /tmp/devcontainer-good.json
python - <<'PY'
import json
p='.devcontainer/devcontainer.json'
data=json.load(open(p,encoding='utf-8'))
data['postCreateCommand']="test -f docs/missing.html && printf 'docs ready\n'"
json.dump(data,open('/tmp/devcontainer-broken.json','w',encoding='utf-8'),indent=2)
PY
BROKEN_CMD="$(python - <<'PY'
import json
print(json.load(open('/tmp/devcontainer-broken.json',encoding='utf-8'))['postCreateCommand'])
PY
)"
set +e
sh -lc "$BROKEN_CMD" > evidence/lifecycle-failure.txt 2>&1
BROKEN_RC=$?
set -e
printf 'broken_exit=%s
' "$BROKEN_RC" | tee -a evidence/lifecycle-failure.txt
test "$BROKEN_RC" -ne 0

# Repair by returning to the reviewed repository configuration.
GOOD_CMD="$(python - <<'PY'
import json
print(json.load(open('/tmp/devcontainer-good.json',encoding='utf-8'))['postCreateCommand'])
PY
)"
sh -lc "$GOOD_CMD" | tee evidence/lifecycle-repaired.txt
rm -f /tmp/devcontainer-good.json /tmp/devcontainer-broken.json

The original failure is preserved in evidence/lifecycle-failure.txt. The repair does not “hide” it; it proves that the reviewed command succeeds while the controlled broken command fails for the predicted missing-file cause. No cloud machine had to be created to teach this diagnostic boundary.

7. Optional Codespace parity check — only after quota, billing, and trust review

If your personal included usage is available and the repository/configuration is trusted, create the smallest suitable Codespace. Otherwise skip this section; the checkpoint remains complete.

# Read available machine choices first.
gh api -H "X-GitHub-Api-Version: 2026-03-10" "repos/$FULL/codespaces/machines"   --jq '.machines[] | {name,display_name,cpus,memory_in_bytes,storage_in_bytes}'

# Optional mutation: creates billable/included hosted compute + storage state.
gh codespace create -R "$FULL" -b main --idle-timeout 30m --retention-period 1d --status

CS="$(gh codespace list -R "$FULL" --json name,state --jq '.[0].name')"
gh codespace view -c "$CS" --json name,repository,state,machineName,idleTimeoutMinutes,retentionPeriodDays,retentionExpiresAt,devcontainerPath,prebuild
gh codespace ports -c "$CS" --json sourcePort,label,visibility,browseUrl

# Inspect only safe identity/source fields; never dump the whole environment.
gh codespace ssh -c "$CS" -- 'printf "repo=%s\nsha=%s\ncodespaces=%s\n" "$GITHUB_REPOSITORY" "$(git rev-parse HEAD)" "${CODESPACES:-}"'

The repository SHA inside the Codespace should equal the intended source commit after synchronization. Port 8000 may be forwarded if a server runs, but this lab never changes it to public visibility. If GitHub asks for unexpected cross-repository permissions, choose Continue without authorizing and diagnose the configuration before granting access.

8. Write the cloud-development and documentation policy

Create a policy that another operator can apply without reading your mind. The minimum useful policy is specific enough to test:

cat > policy/cloud-development.md <<'MD'
# Cloud development and documentation policy

## Repository trust
- Review devcontainer.json, Dockerfiles/features, lifecycle commands, dotfiles effects, and requested cross-repository permissions before creating a hosted environment.
- Do not run privileged setup from an untrusted fork/ref.

## Secrets and identity
- Codespaces development secrets are separate from Actions and Dependabot secrets.
- Scope each development secret to the smallest repository audience and never reuse production deployment credentials by default.
- Do not print environment-variable inventories or credential-bearing configuration in logs.

## Network
- Forward only required development ports. Keep ports private unless a documented review requires broader visibility.

## Cost and lifecycle
- Confirm the billing owner/quota before creation.
- Prefer the smallest adequate machine, a bounded idle timeout, and short retention for disposable work.
- Stop idle Codespaces; delete them only after verifying all work is pushed or intentionally discarded.
- Create prebuilds only when measured startup savings justify Actions/storage cost.

## Pages
- Record branch/path or workflow build type and verify the deployed commit.
- Use a custom Actions workflow only when build/test control is needed; pin actions to reviewed immutable SHAs.
- Verify custom domains and enforce HTTPS after DNS/certificate provisioning; never publish secrets or sensitive transactions.

## Cleanup and review
- Quarterly: review active Codespaces, retention/timeout defaults, secret scopes, prebuilds, Pages sources, domains, and deployment workflows.
- Incidents preserve source/configuration/deployment evidence before mutation.
MD

git add policy/cloud-development.md evidence
 git commit -m "Document Chapter 30 cloud-development policy and evidence"
git push
FINAL_SHA="$(git rev-parse HEAD)"
printf 'final_policy_sha=%s
' "$FINAL_SHA" | tee evidence/final-policy-sha.txt

The policy commit intentionally occurs after the Pages-source proof. That means the latest repository commit may be newer than the build commit you captured earlier. Evidence should say which commit was the published content and which later commit added governance records rather than pretending one SHA describes every state.

9. Independent verification checklist

  • .devcontainer/devcontainer.json parses as JSON and its normal lifecycle command exits zero.
  • The controlled broken lifecycle command exits nonzero and the original stderr/exit evidence is retained.
  • Local HTTP content contains the checkpoint marker and matches the repository file hash before hosted comparison.
  • Pages configuration reports the intended source/build type and the captured completed build commit equals the intended repository commit for that publication.
  • The hosted page contains the checkpoint marker.
  • If a Codespace was created, its repository, machine, source SHA, idle timeout, retention, ports, and prebuild state were recorded without dumping credentials.
  • No real development secret, Actions secret, Dependabot secret, PAT, private key, custom-domain credential, or public port was required.
  • The cloud-development policy names trust, secret, network, cost, prebuild, Pages provenance, custom-domain/TLS, cleanup, and review ownership.

10. Cleanup and rollback

First preserve or push anything you intend to keep. Then remove only the disposable hosted resources.

# Optional Codespace cleanup. Stop first; inspect Git before deletion.
if [ -n "${CS:-}" ]; then
  gh codespace stop -c "$CS"
  gh codespace ssh -c "$CS" -- 'git status --short' 2>/dev/null || true
  # DESTRUCTIVE: deletes the disposable Codespace and any unpushed state.
  gh codespace delete -c "$CS"
fi

# DESTRUCTIVE TO THE PUBLIC SITE ONLY: unpublish the disposable Pages site.
gh api --method DELETE -H "X-GitHub-Api-Version: 2026-03-10" "repos/$FULL/pages"

# Safer repository cleanup: archive instead of deleting the evidence repository.
gh repo archive "$FULL" --yes

If the Codespace has already been deleted, you cannot SSH into it to recover work. That is why source-state verification precedes deletion. Repository deletion is unnecessary for this checkpoint and is intentionally omitted.

Knowledge check

Why can the repository HEAD be newer than the Pages build SHA at the end of this lab?

The broken lifecycle test fails exactly as predicted. What proves the repair rather than merely hiding the error?

A Codespace is stopped. Has all Codespaces cost/state disappeared?

A teammate proposes copying an Actions deployment secret into Codespaces because “both are GitHub secrets.” What is wrong?

Pages serves the expected text but the latest completed build references an unexpected commit. Accept the deployment?

What does Chapter 30 add to the production GitHub operating model?

Checkpoint summary and Chapter 31 bridge

You now have a small documentation project whose development-environment metadata is reviewable in Git, whose mandatory verification works locally, whose Pages deployment can be traced to an exact commit, and whose optional Codespace path is constrained by trust, scope, lifecycle, and cost. The incident drill proved that environment failures should preserve evidence rather than trigger blind rebuilds. This adds a governed developer/documentation plane to the production GitHub operating model. Chapter 31 scales the same thinking to large repositories, monorepos, search, Actions cost controls, and platform performance.

Next chapter

Large Repositories, Monorepos, Search, Actions Cost Controls, and Platform Performance: Concepts, Architecture, and Mental Model

Further reading — current primary sources

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.