Chapter 13Lesson 05~195 minutes

Checkpoint Lab — GitHub Actions Foundations: Workflows, Events, YAML, Permissions, and Execution Model

This checkpoint converts the chapter into an operating habit. You will build a tiny CI workflow that checks out an exact source revision, prints only safe identity fields, validates a deterministic file, and then prove that an unnecessary write operation is blocked by least privilege.

Checkpoint labCI workflowPermission failureRun identity

Learning objectives

  • Build a minimal CI workflow with explicit contents: read, immutable checkout dependency identity, safe run metadata, and deterministic validation.
  • Predict and verify the workflow resource, run, Git ref/SHA, job token permission, and log changes created by each lab action.
  • Inject one controlled permission failure, diagnose the 403 from preserved evidence, and repair the workflow without normalizing broad write access.
  • Document the run identity and trust boundary in a short handoff checklist suitable for production review.
  • Disable the lab workflow and archive the disposable repository while preserving historical run evidence.

Checkpoint assumptions: GitHub.com, GitHub Free, a disposable public personal repository, GitHub CLI authenticated to the intended account, Git, and Bash/Git Bash/zsh (PowerShell equivalents may create the same files). Use a standard GitHub-hosted Ubuntu runner. Do not use self-hosted runners or real secrets.

1. Setup and preflight: create an evidence-only CI repository

This lab intentionally avoids package managers, external clouds, secrets, and production dependencies. One text file is the “application.” That keeps every failure attributable to GitHub Actions behavior rather than an unrelated ecosystem.

gh --version
git --version
gh auth status --active --hostname github.com

OWNER="$(gh api user \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  --jq .login)"
LAB="actions-checkpoint-$(date +%Y%m%d-%H%M%S)"
REPO="$OWNER/$LAB"

gh repo create "$REPO" --public --add-readme \
  --description "Disposable Chapter 13 Actions checkpoint"
gh repo clone "$REPO" "$LAB-work"
cd "$LAB-work"
DEFAULT_BRANCH="$(gh repo view "$REPO" --json defaultBranchRef --jq .defaultBranchRef.name)"

gh workflow list -R "$REPO" --all --json id,name,path,state
gh run list -R "$REPO" --limit 5 --json databaseId,event,status,conclusion,headSha,url

Expected before state: no workflows, no runs, one default-branch commit created by --add-readme. If Actions is disabled by policy, stop the live lab and use the fixture-based reasoning steps instead of changing valuable account policy.

2. Predict two state changes before you create them

Write these predictions in a local note before continuing:

  1. Committing .github/workflows/ch13-ci.yml and src/value.txt creates new Git objects/ref state and should create a push workflow run because the commit changes src/** and the workflow file, both of which match the workflow's path-scoped push trigger.
  2. The validation job receives contents: read but no Issues write permission. If a later step attempts to create an Issue with GITHUB_TOKEN, the request should fail and create no Issue.

These predictions are the acceptance criteria. The lab is not complete until you independently verify them.

3. Create the minimal CI workflow with immutable checkout

The workflow prints selected metadata through environment variables, checks out the event revision with the current verified actions/checkout v7.0.1 commit SHA, and validates one exact line. persist-credentials: false removes the checkout credential after fetch because this job does not need authenticated Git writes.

mkdir -p .github/workflows src
printf 'chapter-13-ok\n' > src/value.txt
cat > .github/workflows/ch13-ci.yml <<'YAML'
name: Chapter 13 checkpoint CI

on:
  workflow_dispatch:
  push:
    paths:
      - 'src/**'
      - '.github/workflows/ch13-ci.yml'

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Record safe run identity
        env:
          EVENT_NAME: ${{ github.event_name }}
          REPOSITORY: ${{ github.repository }}
          RUN_REF: ${{ github.ref }}
          RUN_SHA: ${{ github.sha }}
          ACTOR: ${{ github.actor }}
          WORKFLOW_REF: ${{ github.workflow_ref }}
          WORKFLOW_SHA: ${{ github.workflow_sha }}
          RUNNER_OS_SAFE: ${{ runner.os }}
          RUNNER_ARCH_SAFE: ${{ runner.arch }}
        run: |
          printf 'event=%s\nrepo=%s\nref=%s\nsha=%s\nactor=%s\n' \
            "$EVENT_NAME" "$REPOSITORY" "$RUN_REF" "$RUN_SHA" "$ACTOR"
          printf 'workflow_ref=%s\nworkflow_sha=%s\nrunner=%s/%s\n' \
            "$WORKFLOW_REF" "$WORKFLOW_SHA" "$RUNNER_OS_SAFE" "$RUNNER_ARCH_SAFE"

      - name: Check out exact event source
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          persist-credentials: false

      - name: Deterministic validation
        run: |
          test -f src/value.txt
          grep -qx 'chapter-13-ok' src/value.txt
YAML

git diff -- .github/workflows/ch13-ci.yml src/value.txt
git add .github/workflows/ch13-ci.yml src/value.txt
git commit -m "ci: add Chapter 13 checkpoint"
GOOD_SHA="$(git rev-parse HEAD)"
git push origin "$DEFAULT_BRANCH"
printf 'expected first CI sha=%s\n' "$GOOD_SHA"

Security note: the action is pinned to a full SHA verified from the official actions/checkout repository. A comment beside a pinned SHA can record the human-readable release (v7.0.1) in a production workflow, and dependency automation can propose future updates.

4. Verify the successful run from CLI, logs, Git, and REST

GOOD_RUN=""
for attempt in {1..12}; do
  GOOD_RUN="$(gh run list -R "$REPO" \
    --workflow ch13-ci.yml --event push --commit "$GOOD_SHA" --limit 1 \
    --json databaseId --jq '.[0].databaseId // empty')"
  [[ -n "$GOOD_RUN" ]] && break
  sleep 5
done
[[ -n "$GOOD_RUN" ]] || { echo 'No successful run appeared for GOOD_SHA after bounded polling' >&2; exit 1; }

gh run watch "$GOOD_RUN" -R "$REPO" --exit-status
gh run view "$GOOD_RUN" -R "$REPO" \
  --json databaseId,attempt,event,headBranch,headSha,status,conclusion,jobs,url
gh run view "$GOOD_RUN" -R "$REPO" --log

REMOTE_SHA="$(git rev-parse "origin/$DEFAULT_BRANCH")"
RUN_SHA="$(gh run view "$GOOD_RUN" -R "$REPO" --json headSha --jq .headSha)"
printf 'local=%s\nremote=%s\nrun=%s\n' "$GOOD_SHA" "$REMOTE_SHA" "$RUN_SHA"

gh api \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  "repos/$REPO/actions/runs/$GOOD_RUN" \
  --jq '{id,event,status,conclusion,head_sha,run_attempt,path,html_url}'

Required observation: all three commit identities match for this push run. The log’s workflow_sha should also identify the workflow definition used. The validation step succeeds without any write permission.

5. Inject a controlled permission failure

Add one step that attempts an Issue write even though the workflow still declares only contents: read. This is intentionally incorrect. Do not add issues: write.

cat > .github/workflows/ch13-ci.yml <<'YAML'
name: Chapter 13 checkpoint CI

on:
  workflow_dispatch:
  push:
    paths:
      - 'src/**'
      - '.github/workflows/ch13-ci.yml'

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Record safe run identity
        env:
          EVENT_NAME: ${{ github.event_name }}
          REPOSITORY: ${{ github.repository }}
          RUN_REF: ${{ github.ref }}
          RUN_SHA: ${{ github.sha }}
          ACTOR: ${{ github.actor }}
          WORKFLOW_REF: ${{ github.workflow_ref }}
          WORKFLOW_SHA: ${{ github.workflow_sha }}
          RUNNER_OS_SAFE: ${{ runner.os }}
          RUNNER_ARCH_SAFE: ${{ runner.arch }}
        run: |
          printf 'event=%s\nrepo=%s\nref=%s\nsha=%s\nactor=%s\n' \
            "$EVENT_NAME" "$REPOSITORY" "$RUN_REF" "$RUN_SHA" "$ACTOR"
          printf 'workflow_ref=%s\nworkflow_sha=%s\nrunner=%s/%s\n' \
            "$WORKFLOW_REF" "$WORKFLOW_SHA" "$RUNNER_OS_SAFE" "$RUNNER_ARCH_SAFE"

      - name: Check out exact event source
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
        with:
          persist-credentials: false

      - name: Deterministic validation
        run: |
          test -f src/value.txt
          grep -qx 'chapter-13-ok' src/value.txt

      - name: Intentionally forbidden Issue write
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh api --method POST \
            -H "X-GitHub-Api-Version: 2026-03-10" \
            "repos/$GITHUB_REPOSITORY/issues" \
            -f title='Chapter 13 permission probe' \
            -f body='This request should be rejected by least privilege.'
YAML

git diff -- .github/workflows/ch13-ci.yml
git add .github/workflows/ch13-ci.yml
git commit -m "test: prove CI token cannot write issues"
FAIL_SHA="$(git rev-parse HEAD)"
git push origin "$DEFAULT_BRANCH"

FAIL_RUN=""
for attempt in {1..12}; do
  FAIL_RUN="$(gh run list -R "$REPO" \
    --workflow ch13-ci.yml --event push --commit "$FAIL_SHA" --limit 1 \
    --json databaseId --jq '.[0].databaseId // empty')"
  [[ -n "$FAIL_RUN" ]] && break
  sleep 5
done
[[ -n "$FAIL_RUN" ]] || { echo 'No permission-probe run appeared for FAIL_SHA after bounded polling' >&2; exit 1; }

# This command is expected to return non-zero because the run should fail.
if gh run watch "$FAIL_RUN" -R "$REPO" --exit-status; then
  echo 'ERROR: permission probe unexpectedly succeeded' >&2
  exit 1
else
  echo 'Expected: run failed; inspect the forbidden write step.'
fi

gh run view "$FAIL_RUN" -R "$REPO" --log-failed
gh issue list -R "$REPO" --state all \
  --search '"Chapter 13 permission probe" in:title' \
  --json number,title,state,url

Expected: the write step receives a permission error/403, the run conclusion is failure, and the issue query returns no matching Issue. This is an authorization failure, not a YAML, runner, checkout, or validation failure.

6. Repair the model—not by granting unnecessary write access

The CI workflow’s purpose is validation, not work-item creation. Therefore the least-destructive fix is to remove the forbidden write step and leave permissions: contents: read unchanged.

git show HEAD^:.github/workflows/ch13-ci.yml > .github/workflows/ch13-ci.yml
git diff -- .github/workflows/ch13-ci.yml
git add .github/workflows/ch13-ci.yml
git commit -m "fix: restore read-only CI purpose"
FIX_SHA="$(git rev-parse HEAD)"
git push origin "$DEFAULT_BRANCH"

FIX_RUN=""
for attempt in {1..12}; do
  FIX_RUN="$(gh run list -R "$REPO" \
    --workflow ch13-ci.yml --event push --commit "$FIX_SHA" --limit 1 \
    --json databaseId --jq '.[0].databaseId // empty')"
  [[ -n "$FIX_RUN" ]] && break
  sleep 5
done
[[ -n "$FIX_RUN" ]] || { echo 'No repaired run appeared for FIX_SHA after bounded polling' >&2; exit 1; }

gh run watch "$FIX_RUN" -R "$REPO" --exit-status
gh run view "$FIX_RUN" -R "$REPO" \
  --json event,headSha,status,conclusion,url

Production reasoning: if the business requirement truly needed an Issue write, create a separate reporting job with issues: write and only the data it needs. Do not hand the test process a broad token “because it is easier.”

7. Document the run identity and trust boundary

Create a short local runbook note. Do not commit secrets or tokens. Your note should answer:

Handoff question Checkpoint answer
What can trigger it? A push that changes src/** or .github/workflows/ch13-ci.yml, plus manual dispatch.
What source does it validate? The event-associated SHA; verify with gh run view headSha.
Which workflow definition? Record github.workflow_ref and github.workflow_sha from safe log fields.
Where does it run? GitHub-hosted Ubuntu runner; no self-hosted infrastructure.
What repository authority does it have? contents: read only.
What external executable dependency? actions/checkout pinned to full v7.0.1 SHA.
What is untrusted? Repository content/event payload can be influenced by contributors; no context values are embedded directly as shell source.
What proves success? Run conclusion + deterministic validation + SHA equality evidence.

8. Verification checklist and cleanup/rollback

  • Successful initial run is bound to GOOD_SHA.
  • Permission-probe run failed and created no Issue.
  • Fixed run succeeds while permissions remains contents: read.
  • Logs contain selected metadata only—no token, secret dump, full github context, or authorization header.
  • Checkout is pinned to the verified full SHA.
gh workflow disable ch13-ci.yml -R "$REPO"
gh workflow list -R "$REPO" --all --json id,name,path,state

# Preserve historical run URLs/IDs, then archive the disposable repository.
gh repo archive "$REPO" --yes
gh repo view "$REPO" --json nameWithOwner,isArchived,url

Cleanup boundary: Archiving is reversible and keeps Git/run evidence. Permanent repository deletion is not required. If you later unarchive the lab repository, keep the workflow disabled unless you intentionally want it to execute again.

9. What this chapter adds to a production GitHub operating model

Chapters 7–12 established how change is proposed, reviewed, merged, governed, and released. Chapter 13 adds the execution boundary: every automated result must be attributable to an event, source SHA, workflow definition, runner, token permission set, dependency set, and logs. That is the minimum evidence model for CI/CD.

Chapter 14 now expands the trigger/expression layer: filters, contexts, variables, inputs, and outputs. The mental model from this checkpoint is the guardrail that keeps those features from becoming opaque YAML magic.

10. Checkpoint summary

You built a real, free-compatible workflow, used immutable action dependency identity, verified source/run causality, proved least privilege with a controlled 403, corrected the design without broadening authority, documented trust boundaries, disabled the workflow, and archived the repository. That is a complete foundations loop: predict → execute → observe → diagnose → repair → verify → clean up.

Knowledge check

Why was the permission-probe failure a successful security test?

Why did the repair remove the write step instead of adding write-all?

What three identities must match for the first push-run evidence in this lab?

Why use a full commit SHA for actions/checkout?

A teammate proposes moving this public workflow to a long-lived self-hosted server with internal-network access. What is the first concern?

Does archiving the repository delete historical workflow runs?

Next lesson

Next: Workflow Triggers, Filters, Expressions, Contexts, Variables, Inputs, and Outputs: Concepts, Architecture, and Mental Model

Further reading — current official GitHub 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.