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.
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?
It proves the negative trigger case: the Git ref changed, but the path predicate was false. Absence of a run is expected evidence, not missing instrumentation.
The broken run generated smoke but the consumer
saw empty. Which layer failed?
The job-output contract/reference. Triggering, producer execution, and permissions succeeded.
Would granting contents: write fix the broken
output?
No. The failure is unrelated to authorization. Broadening permissions would add risk without correcting the data contract.
Why keep the broken run instead of deleting/rerunning it?
It preserves evidence of the original code/ref and failure mode. The fixed workflow should create a new run bound to the corrected SHA.
A new required PR workflow is filtered to src/**.
What governance question must be answered before shipping
it?
What happens to the required check when a PR changes only non-matching paths? GitHub documents that filter-skipped required checks can remain Pending and block merging.
What is the bridge from Chapter 14 to Chapter 15?
Chapter 14 defines when runs activate and how small values move through explicit dependencies; Chapter 15 expands the job/step/matrix/container dependency graph that consumes those contracts.
Further reading — current official GitHub sources
- GitHub Docs — Workflow syntax
- GitHub Docs — Events that trigger workflows
- GitHub Docs — Expressions
- GitHub Docs — Contexts reference
- GitHub Docs — Variables
- GitHub Docs — Pass job outputs
- GitHub Docs — Script injections
- GitHub Docs — Secure use reference
- GitHub CLI — gh workflow run
- GitHub CLI — gh run list
- GitHub CLI — gh variable delete
- GitHub REST — Workflow runs
- GitHub Docs — Context availability
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.