Checkpoint Lab — Contexts, Expressions, Functions, Conditionals, and Evaluation Semantics
The checkpoint turns Chapter 04 into a reviewable decision matrix.
One disposable manual workflow uses typed Boolean/number/choice
inputs plus string-valued step outputs. You predict every
admitted/skipped/executed path before each run, preserve the
resulting run and step evidence, then expose a deliberately wrong
legacy condition that treats the string 'false' as
truthy—without printing any secret or broad context.
Learning objectives
- Build a conditional workflow driven by typed manual inputs and prior step outputs.
- Predict job admission, numeric conversion, success/failure branches, and final evidence behavior before dispatch.
-
Prove the difference between typed
inputsand stringgithub.event.inputswith an intentionally wrong condition. - Diagnose the wrong path using type/evaluation evidence rather than trial-and-error YAML edits.
- Produce a complete evidence packet and bridge cleanly to Chapter 05 environment variables, outputs, and data passing.
1. Checkpoint scope and safety contract
| Item | Required state |
|---|---|
| Repository | Disposable learner-owned repository; workflow file on default branch. |
| Runner | ubuntu-24.04. |
| Permissions | permissions: {}. |
| External actions | None. |
| Secrets/credentials | None; do not create one for the checkpoint. |
| Side effects | Run history/logs only; no package, API mutation, deployment, cloud, or runner registration. |
| Evidence retention | Keep all three run IDs/attempts and workflow SHAs until review is complete. |
2. Final checkpoint workflow
name: Chapter 04 checkpoint
on:
workflow_dispatch:
inputs:
run_checks:
description: 'Admit the main evaluation job'
type: boolean
required: true
default: true
simulate_failure:
description: 'Fail the test step intentionally'
type: boolean
required: true
default: false
threshold:
description: 'Minimum observed count'
type: number
required: true
default: 3
mode:
description: 'Synthetic mode'
type: choice
required: true
options: [quick, full]
permissions: {}
defaults:
run:
shell: bash
jobs:
typed-gate:
if: ${{ inputs.run_checks }}
runs-on: ubuntu-24.04
steps:
- name: Record bounded identity
env:
EVENT: ${{ github.event_name }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
MODE: ${{ inputs.mode }}
THRESHOLD: ${{ inputs.threshold }}
run: |
printf 'event=%s ref=%s sha=%s workflow_sha=%s\n' "$EVENT" "$REF" "$SHA" "$WORKFLOW_SHA"
printf 'mode=%s threshold=%s run=%s attempt=%s\n' "$MODE" "$THRESHOLD" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"
- name: Produce prior-step data
id: measure
run: |
echo 'observed=4' >> "$GITHUB_OUTPUT"
echo 'healthy=true' >> "$GITHUB_OUTPUT"
echo 'payload={"source":"checkpoint","kind":"synthetic"}' >> "$GITHUB_OUTPUT"
- name: Numeric branch
id: threshold_branch
if: ${{ fromJSON(steps.measure.outputs.observed) >= inputs.threshold }}
env:
OBSERVED: ${{ steps.measure.outputs.observed }}
run: printf 'threshold branch admitted observed=%s\n' "$OBSERVED"
- name: Quick-mode branch
if: ${{ inputs.mode == 'quick' }}
run: echo 'quick mode path'
- name: Deliberate test failure
id: test
if: ${{ inputs.simulate_failure }}
run: exit 4
- name: Failure branch
if: ${{ failure() && steps.test.conclusion == 'failure' }}
run: echo 'controlled failure confirmed'
- name: Success branch
if: ${{ success() }}
run: echo 'success path confirmed'
- name: Final evidence unless cancelled
if: ${{ !cancelled() }}
env:
MEASURE_OUTCOME: ${{ steps.measure.outcome }}
THRESHOLD_CONCLUSION: ${{ steps.threshold_branch.conclusion }}
TEST_CONCLUSION: ${{ steps.test.conclusion }}
SYNTHETIC_JSON: ${{ toJSON(fromJSON(steps.measure.outputs.payload)) }}
run: |
printf 'measure=%s threshold=%s test=%s\n' \
"$MEASURE_OUTCOME" "$THRESHOLD_CONCLUSION" "$TEST_CONCLUSION"
printf 'payload=%s\n' "$SYNTHETIC_JSON"
wrong-string-gate:
# Intentionally wrong for diagnosis: event input is a string.
if: ${{ github.event.inputs.run_checks }}
runs-on: ubuntu-24.04
steps:
- name: Demonstrate string truthiness
env:
EVENT_INPUT_TEXT: ${{ github.event.inputs.run_checks }}
TYPED_INPUT: ${{ inputs.run_checks }}
run: |
printf 'event_input_text=%s typed_input=%s\n' \
"$EVENT_INPUT_TEXT" "$TYPED_INPUT"
wrong-string-gate job is deliberately
incorrect.
It exists only in the disposable checkpoint to prove that the
non-empty string 'false' is truthy. The repair is to
use if: ${{ inputs.run_checks }}, not to compare
arbitrary strings or add token/runner privileges.
3. Prediction matrix — fill this before dispatch
| Run | Inputs | Typed gate | Numeric branch | Failure branch | Success branch | Wrong string gate |
|---|---|---|---|---|---|---|
| A |
run_checks=true,
simulate_failure=false,
threshold=3, mode=quick
|
Runs | Runs (4≥3) | Skips | Runs | Runs |
| B |
true, true, 3,
full
|
Runs | Runs | Runs after failure | Skips | Runs |
| C |
run_checks=false,
simulate_failure=false,
threshold=5, mode=quick
|
Skips before runner | Not reached | Not reached | Not reached |
Unexpectedly runs because string
'false' is non-empty
|
Run C is the deliberate defect. Predicting it before execution proves you understand the type contract rather than discovering the issue by accident.
4. Execute Run A — clean success path
Record run ID/attempt, workflow SHA, input values, typed-gate job conclusion, every named step conclusion, and wrong-string-gate conclusion. Verify:
typed-gateis admitted.-
threshold_branchruns because string output'4'is explicitly converted and compared to numeric input 3. testis skipped.success()branch runs.- final
!cancelled()evidence runs.
5. Execute Run B — controlled failure path
Verify that the original failing step stays failed. The failure
branch must run because it explicitly includes
failure(). The success branch must skip. The final
evidence step must still run because the workflow was not cancelled.
The job conclusion remains failure.
This is a key governance point: a diagnostic step succeeding after a failure does not prove the job succeeded and must not overwrite the original technical outcome.
6. Execute Run C — expose the string-truthiness defect
Set run_checks=false. Predict that
typed-gate is skipped before runner assignment because
it consumes the Boolean value. Then observe that
wrong-string-gate is admitted because
github.event.inputs.run_checks is the non-empty string
'false', which is truthy in a conditional.
Preserve the run before fixing it. The evidence packet should contain:
- the workflow revision with the wrong condition;
- the dispatch input showing false;
- typed-gate skipped state;
- wrong-string-gate runner/job state proving it nevertheless ran;
- the bounded log line showing event-input text versus typed input representation.
7. Repair the deliberate wrong condition
Create a new workflow revision changing only:
wrong-string-gate:
if: ${{ inputs.run_checks }}
Rerun the equivalent false-input scenario. Do not change the runner, event, permissions, or other inputs. Compare the new run ID/workflow SHA with Run C and verify both jobs now skip before runner allocation. This is a causal repair because the only changed variable is the type-correct condition.
8. Optional threshold variation
With run_checks=true and threshold=5, the
numeric branch should skip because converted observed value 4 is
below typed numeric 5. The job can still succeed if no failure is
simulated. This proves that a skipped conditional step is not
synonymous with a failed workflow.
9. Required evidence packet
- Workflow path and SHA for each revision (broken and repaired).
- Prediction matrix completed before execution.
- Run IDs/attempts for A, B, C, and the repaired C-equivalent rerun.
- Exact typed manual input values for each run.
- Job admission/skipped state for both jobs.
-
Step
outcome/conclusionformeasure, threshold branch, deliberate test, failure branch, success branch, and final evidence. -
Raw producer outputs
observed=4,healthy=true, and bounded synthetic JSON; note that outputs are strings. - Runner OS/arch and event/ref/SHA/workflow SHA evidence.
-
Broken-condition explanation: non-empty event-input string
'false'is truthy. - Repair evidence showing the typed Boolean condition skips correctly.
- Assumptions/limitations: no checkout, secret, API mutation, artifact, cache, environment approval, OIDC, deployment, self-hosted runner, or external action.
10. Verification checklist
- All sensitive/broad contexts remain unprinted.
- No untrusted event text is interpolated directly into a shell script.
-
Numeric step output is explicitly converted with
fromJSON. -
Manual Boolean contract uses
inputsin the correct job. - Failure diagnosis includes
failure(). -
Final evidence uses
!cancelled(), not blanketalways(). - Run C preserves the broken evidence before repair.
- The repair changes only the expression, not runner/permissions/event.
- Skipped job/step, failed job/step, and no-run states are not conflated.
11. Cleanup / rollback
- Keep the four evidence runs until checkpoint review is complete.
- Keep or revert the repaired workflow as desired in the disposable repository.
- Delete only the learner-owned disposable repository/branch after evidence retention needs are satisfied.
- No token, package, deployment, cloud, runner, or external infrastructure cleanup is required.
12. What Chapter 04 adds to the production operating model
Chapter 04 adds the expression contract: every conditional decision records its data source/context, type, lifecycle availability, conversion/coercion rule, trust sensitivity, status-check semantics, and observable outcome. It also establishes a security rule: attacker-controlled context text remains data and never becomes generated shell code.
Chapter 05 now has a clean foundation: environment variables, configuration variables, outputs, and data passing can be taught as explicit channels whose scope/type/ownership are already understood.
Knowledge check
In Run C, why does typed-gate skip while
wrong-string-gate runs?
The typed job consumes Boolean false, while the wrong job
consumes the non-empty string "false", which is
truthy.
Why must the broken Run C be preserved before changing the expression?
It is first-failure evidence proving the original type/coercion bug. The repaired run is comparison evidence, not a replacement.
Why does the threshold branch convert
steps.measure.outputs.observed?
Step outputs are strings. fromJSON makes the
numeric comparison to the typed number input explicit.
Run B ends with a successful failure-diagnostic step. Is the job therefore successful?
No. The original test failure remains the technical outcome; diagnostic success does not rewrite the job failure.
What is the smallest causal repair for the deliberately wrong job?
Replace its condition with
if: ${{ inputs.run_checks }} and rerun the same
false-input scenario without changing unrelated variables.
Official references and version notes
-
Evaluate expressions in workflows and actions
— current literals, truthiness, loose equality/coercion, built-in
functions,
fromJSON/toJSON, and status-check functions. -
Contexts reference
— current context objects, missing-property behavior, per-key
context availability, and
steps/runner/inputssemantics. -
Workflow syntax for GitHub Actions
— current
if,workflow_dispatchinput types, environment scopes, job/step syntax, and permission placement. - Script injections — why attacker-controlled context text must not be substituted directly into generated shell scripts.
- Secure use reference — current safe handling guidance for secrets, untrusted context values, permissions, and workflow code.
- Variables reference — distinction between default runner environment variables and expression contexts.
Version-sensitive expression/context behavior was rechecked
against current primary GitHub documentation on
2026-09-09. Executable labs use a disposable
repository, ubuntu-24.04, built-in shell steps only,
and explicit permissions: {}; no external action,
secret, package, deployment, self-hosted runner, or cloud account
is required. At verification time, expression literals include
booleans/null/numbers/strings; conditionals coerce documented
falsy values to false; equality is loose and can numerically
coerce mismatched types; nonexistent context properties evaluate
to an empty string; steps.*.outputs.* values are
strings; manual-workflow values in the inputs context
preserve Boolean typing while
github.event.inputs represents Boolean values as
strings; and a default success() status check applies
to if expressions unless a status-check function is
present. Current docs also caution against broad
always() use for critical tasks and recommend
!cancelled() when appropriate. GitHub Actions is
continuously delivered, so these semantics and
context-availability tables must be rechecked when regenerating
the lesson. The checkpoint is fully free/disposable-compatible and
has no credential path. Its only intentional defect is
string-truthiness logic in a manual-input condition; the
executable workflow contains no untrusted fork payload or external
side effect.
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.