Chapter 04Lesson 05~190 minutes

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.

CheckpointPath matrixTyped manual inputsPrior step resultsCausal repair

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 inputs and string github.event.inputs with 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"
The 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-gate is admitted.
  • threshold_branch runs because string output '4' is explicitly converted and compared to numeric input 3.
  • test is 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/conclusion for measure, 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 inputs in the correct job.
  • Failure diagnosis includes failure().
  • Final evidence uses !cancelled(), not blanket always().
  • 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

  1. Keep the four evidence runs until checkpoint review is complete.
  2. Keep or revert the repaired workflow as desired in the disposable repository.
  3. Delete only the learner-owned disposable repository/branch after evidence retention needs are satisfied.
  4. 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.

Next lesson

Environment Variables, Configuration Variables, Outputs, and Data Passing

Move from deciding with data to transporting data deliberately across workflow, job, step, runner, and downstream-job boundaries.

Knowledge check

In Run C, why does typed-gate skip while wrong-string-gate runs?

Why must the broken Run C be preserved before changing the expression?

Why does the threshold branch convert steps.measure.outputs.observed?

Run B ends with a successful failure-diagnostic step. Is the job therefore successful?

What is the smallest causal repair for the deliberately wrong job?

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/inputs semantics.
  • Workflow syntax for GitHub Actions — current if, workflow_dispatch input 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 and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.