Chapter 14Lesson 05~205 minutes

Checkpoint Lab — Workflow Triggers, Filters, Expressions, Contexts, Variables, Inputs, and Outputs

This checkpoint combines activation and data-flow reasoning into one repeatable lab. You will predict a manual run and a branch/path-filtered push run, prove what happened, intentionally break one output contract, repair it without adding privileges, and leave behind a small trigger-test matrix that a production team could use during workflow review.

Checkpoint labTyped inputsTrigger matrixneeds outputs

Learning objectives

  • Build one workflow with typed manual behavior and a branch/path activation contract.
  • Predict at least two state changes before running them and verify run/ref/SHA/input behavior independently.
  • Pass a generated value from a producer step through a job output to a dependent job.
  • Intentionally break one output reference, diagnose the empty value from logs and graph scope, and repair only the contract.
  • Write a trigger-test matrix and operating policy covering activation, data classes, untrusted input, and cleanup.

Checkpoint assumptions: GitHub.com + GitHub Free + one disposable public personal repository; repository owner/write access; GitHub CLI and Git; standard GitHub-hosted runners. No paid environments, organization policy, cloud credentials, real secrets, self-hosted runners, or external dispatch credentials are required.

1. Scenario: Atlas Signal release-readiness gate

Atlas Signal wants one small workflow that can be run manually for smoke or full validation and can also respond automatically when source code changes on a dedicated integration branch. The team needs a generated mode identifier passed to a second job. Your job is to prove activation and data flow—not to build an application.

The checkpoint has four evidence targets: (1) manual false and true inputs change step behavior; (2) branch/path filters reject one commit and accept another; (3) a producer output reaches a dependent job; (4) a broken output reference causes an explainable failure and is repaired without adding permissions.

2. Preflight and before-state inspection

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

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

gh repo create "$REPO" --public --add-readme   --description "Disposable Chapter 14 checkpoint"
gh repo view "$REPO" --json nameWithOwner,visibility,defaultBranchRef,viewerPermission,url
gh workflow list -R "$REPO" --all --json id,name,path,state
gh run list -R "$REPO" --limit 5 --json databaseId,event,headSha,status,conclusion,url

Required observation: no workflows or runs exist yet. If the repository name collided and GitHub reused an existing repository, stop and choose a fresh disposable name so previous runs cannot corrupt your evidence.

3. Write predictions before changing state

Prediction Expected change Independent verification
P1 — manual false A workflow run is created on the default-branch selected ref; the extended step is skipped; generated mode is smoke. gh run view --json + log step conclusions/output.
P2 — docs-only push Remote branch advances, but no Actions run is created because src/** does not match. Git remote SHA changes + gh run list --commit empty.
P3 — source push Same branch plus src/** match creates one push run bound to the source commit SHA. Compare git rev-parse, remote ref, and Actions headSha.
P4 — broken consumer A run exists but downstream contract assertion fails because requested job-output property is empty. Run log shows producer value plus consumer empty-value failure.

Record these in your notes before running commands. Prediction makes the lab a causal test instead of a sequence of clicks.

4. Commit the workflow and a non-sensitive configuration variable

Set one visible repository variable, then commit the workflow to the default branch. The workflow permissions remain read-only.

Shell note: The checkpoint command blocks use Bash/Git Bash/zsh syntax, including command substitution and a here-document. PowerShell learners can use $() for subexpressions where appropriate and a single-quoted here-string with Set-Content to create the YAML; do not translate the YAML expressions themselves.

gh variable set CH14_POLICY --body "training" -R "$REPO"
gh repo clone "$REPO" "$LAB-work"
cd "$LAB-work"
DEFAULT_BRANCH="$(gh repo view "$REPO" --json defaultBranchRef --jq .defaultBranchRef.name)"
mkdir -p .github/workflows
cat > .github/workflows/ch14-checkpoint.yml <<'YAML'
name: Chapter 14 checkpoint

on:
  workflow_dispatch:
    inputs:
      extended:
        description: Run extended validation
        required: true
        type: boolean
        default: false
      target:
        description: Validation target
        required: true
        type: choice
        options: [smoke, full]
        default: smoke
  push:
    branches: [ch14-integration]
    paths: ['src/**']

permissions:
  contents: read

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      generated_mode: ${{ steps.generate.outputs.mode }}
    steps:
      - name: Safe identity evidence
        env:
          EVENT_NAME: ${{ github.event_name }}
          RUN_REF: ${{ github.ref }}
          RUN_SHA: ${{ github.sha }}
          POLICY: ${{ vars.CH14_POLICY }}
        run: |
          printf 'event=%s\nref=%s\nsha=%s\npolicy=%s\n' \
            "$EVENT_NAME" "$RUN_REF" "$RUN_SHA" "$POLICY"

      - name: Generate mode
        id: generate
        env:
          EVENT_NAME: ${{ github.event_name }}
          TARGET: ${{ inputs.target }}
        run: |
          if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
            mode="${TARGET:-smoke}"
          else
            mode="push-validation"
          fi
          printf 'mode=%s\n' "$mode" >> "$GITHUB_OUTPUT"
          printf 'generated=%s\n' "$mode"

      - name: Extended branch
        if: ${{ github.event_name == 'workflow_dispatch' && inputs.extended }}
        run: echo "extended=true"

  consume:
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - name: Verify output contract
        env:
          MODE: ${{ needs.prepare.outputs.generated_mode }}
        run: |
          test -n "$MODE"
          printf 'consumed=%s\n' "$MODE"
YAML

git add .github/workflows/ch14-checkpoint.yml
git commit -m "ci: add Chapter 14 checkpoint workflow"
git push origin "$DEFAULT_BRANCH"
BASE_WORKFLOW_SHA="$(git rev-parse HEAD)"
printf 'workflow_sha=%s\n' "$BASE_WORKFLOW_SHA"

Because the push is to the default branch rather than ch14-integration, the automatic push trigger should not create a run. The workflow is now on the default branch, so manual dispatch is eligible.

5. Test typed input behavior twice

Run with extended=false, then with extended=true. Use JSON input so Boolean intent is explicit.

printf '%s
' '{"extended":false,"target":"smoke"}' |   gh workflow run ch14-checkpoint.yml -R "$REPO" --ref "$DEFAULT_BRANCH" --json
RUN_A="$(gh run list -R "$REPO" --workflow ch14-checkpoint.yml   --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$RUN_A" -R "$REPO" --exit-status
gh run view "$RUN_A" -R "$REPO" --log

printf '%s
' '{"extended":true,"target":"full"}' |   gh workflow run ch14-checkpoint.yml -R "$REPO" --ref "$DEFAULT_BRANCH" --json
RUN_B="$(gh run list -R "$REPO" --workflow ch14-checkpoint.yml   --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$RUN_B" -R "$REPO" --exit-status
gh run view "$RUN_B" -R "$REPO"   --json event,headBranch,headSha,conclusion,jobs,url
gh run view "$RUN_B" -R "$REPO" --log

Verify P1: run A should consume smoke and skip Extended branch. Run B should consume full and execute it. No workflow permission changed between the runs.

6. Test one branch/path non-match and one match

Create the integration branch. The docs commit tests the negative matrix cell; the source commit tests the positive cell.

git switch -c ch14-integration
mkdir -p docs
printf 'checkpoint docs
' > docs/checkpoint.txt
git add docs/checkpoint.txt
git commit -m "docs: checkpoint non-match"
DOC_SHA="$(git rev-parse HEAD)"
git push -u origin ch14-integration

gh run list -R "$REPO" --workflow ch14-checkpoint.yml --event push   --commit "$DOC_SHA" --limit 5   --json databaseId,event,headBranch,headSha,status,conclusion,url

mkdir -p src
printf 'checkpoint source
' > src/checkpoint.txt
git add src/checkpoint.txt
git commit -m "test: checkpoint trigger match"
SRC_SHA="$(git rev-parse HEAD)"
git push origin ch14-integration

RUN_PUSH="$(gh run list -R "$REPO" --workflow ch14-checkpoint.yml --event push   --commit "$SRC_SHA" --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$RUN_PUSH" -R "$REPO" --exit-status
gh run view "$RUN_PUSH" -R "$REPO"   --json event,headBranch,headSha,conclusion,url
gh run view "$RUN_PUSH" -R "$REPO" --log

git ls-remote origin refs/heads/ch14-integration

Verify P2/P3: the remote branch contains both commits, but only the source commit creates a run. The run headSha must equal SRC_SHA, and the log should consume push-validation.

7. Intentionally break the output reference—without changing permissions

Return to the default branch, change only the consumer’s output property name, and dispatch a manual run. This creates a deterministic data-contract failure.

git switch "$DEFAULT_BRANCH"
git pull --ff-only origin "$DEFAULT_BRANCH"
python - <<'PY2'
from pathlib import Path
p=Path('.github/workflows/ch14-checkpoint.yml')
s=p.read_text()
s=s.replace('needs.prepare.outputs.generated_mode', 'needs.prepare.outputs.missing_mode')
p.write_text(s)
PY2

git diff -- .github/workflows/ch14-checkpoint.yml
git add .github/workflows/ch14-checkpoint.yml
git commit -m "test: break Chapter 14 output contract"
git push origin "$DEFAULT_BRANCH"
BROKEN_SHA="$(git rev-parse HEAD)"

printf '%s
' '{"extended":false,"target":"smoke"}' |   gh workflow run ch14-checkpoint.yml -R "$REPO" --ref "$DEFAULT_BRANCH" --json
BROKEN_RUN="$(gh run list -R "$REPO" --workflow ch14-checkpoint.yml   --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"

# Expected non-zero run conclusion; preserve it instead of hiding the failure.
gh run watch "$BROKEN_RUN" -R "$REPO" || true
gh run view "$BROKEN_RUN" -R "$REPO"   --json event,headSha,status,conclusion,jobs,url
gh run view "$BROKEN_RUN" -R "$REPO" --log

Interpretation: the producer logs generated=smoke, so output generation succeeded. The consumer asks for a nonexistent job-output property; context dereference yields an empty value and test -n fails. This is not a permission, runner, or trigger failure.

8. Repair the smallest contract and independently verify

Restore the declared name. Do not add a variable store, artifact, extra token scope, or retry loop.

python - <<'PY2'
from pathlib import Path
p=Path('.github/workflows/ch14-checkpoint.yml')
s=p.read_text().replace('needs.prepare.outputs.missing_mode', 'needs.prepare.outputs.generated_mode')
p.write_text(s)
PY2

git add .github/workflows/ch14-checkpoint.yml
git commit -m "fix: restore Chapter 14 output contract"
git push origin "$DEFAULT_BRANCH"
FIX_SHA="$(git rev-parse HEAD)"

printf '%s
' '{"extended":false,"target":"smoke"}' |   gh workflow run ch14-checkpoint.yml -R "$REPO" --ref "$DEFAULT_BRANCH" --json
FIX_RUN="$(gh run list -R "$REPO" --workflow ch14-checkpoint.yml   --event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId')"
gh run watch "$FIX_RUN" -R "$REPO" --exit-status
gh run view "$FIX_RUN" -R "$REPO" --log

gh api -H "X-GitHub-Api-Version: 2026-03-10"   "repos/$REPO/actions/runs/$FIX_RUN"   --jq '{id,event,head_branch,head_sha,status,conclusion,run_attempt,html_url}'

Verify P4 repair: the new run succeeds and its head_sha equals FIX_SHA. The broken run remains historical evidence and was not rerun into a different code state.

9. Write the trigger-test matrix and operating policy

Case Expected run? Expected behavior / evidence
Manual: extended=false, target=smoke Yes Extended step skipped; output smoke.
Manual: extended=true, target=full Yes Extended step executes; output full.
Push: ch14-integration + docs/** only No Remote ref advances; no matching run.
Push: ch14-integration + src/** Yes Run SHA equals source commit; output push-validation.
Push: other branch + src/** No Branch predicate false.
Broken consumer output name Yes, failure Producer succeeds; consumer receives empty value and fails assertion.

Operating policy: trigger changes require matrix review; required checks must not be silently filtered away; event text is untrusted by default; secrets never serve as ordinary configuration; non-sensitive shared values use vars; calculated metadata uses outputs; files use artifact/package mechanisms; every consumer output has an explicit producer and needs edge; whole contexts are never dumped to logs.

10. Final verification and cleanup/rollback

  • Workflow is on the default branch and all manual runs identify the expected SHA.
  • Branch/path non-match and match behave according to the matrix.
  • Broken run is preserved and correctly classified as an output-contract failure.
  • Fixed run succeeds without any permission broadening.
  • No secret values, whole contexts, external credentials, artifacts, or deployments were created.
gh workflow disable ch14-checkpoint.yml -R "$REPO"
gh variable delete CH14_POLICY -R "$REPO"
gh workflow list -R "$REPO" --all --json id,name,path,state
gh variable list -R "$REPO" --json name,value

gh repo archive "$REPO" --yes
gh repo view "$REPO" --json nameWithOwner,isArchived,url

Rollback: If you need to repeat the lab, create a fresh disposable repository rather than unarchiving and reusing old run history. Fresh state makes causal verification much easier.

11. What Chapter 14 adds to the production GitHub operating model

Chapter 13 established execution identity and least privilege. Chapter 14 adds activation contracts and data contracts. A production workflow should now answer: Which events can create runs? Which branch/path/activity predicates must be true? Which values are caller-controlled? Which contexts are available and trusted at each key? Which values are source configuration, repository configuration, secrets, inputs, or outputs? Which dependency edge guarantees a producer ran before a consumer?

Chapter 15 builds on this by scaling the execution graph itself: jobs, steps, matrices, containers, service containers, and dependency graphs. The trigger/output discipline from this checkpoint is what keeps that larger graph explainable.

12. Checkpoint summary

You built a free-compatible trigger/data-flow workflow, tested typed inputs, proved branch+path AND semantics, passed a generated value across jobs, preserved and diagnosed an intentional broken output, repaired only the violated contract, wrote a trigger-test matrix, disabled the workflow, removed its non-sensitive variable, and archived the lab repository. That is the complete Chapter 14 control loop.

Knowledge check

Why is the docs-only push still useful even though it creates no run?

The broken run generated smoke but the consumer saw empty. Which layer failed?

Would granting contents: write fix the broken output?

Why keep the broken run instead of deleting/rerunning it?

A new required PR workflow is filtered to src/**. What governance question must be answered before shipping it?

What is the bridge from Chapter 14 to Chapter 15?

Next lesson

Next: Jobs, Steps, Matrices, Containers, Service Containers, and Dependency Graphs: 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.