Contexts, Expressions, Functions, Conditionals, and Evaluation Semantics: Guided Hands-On Workflow
This guided workflow turns the mental model into observable
evidence. A disposable manual workflow exposes typed inputs, records
selected context fields, creates string outputs, converts them
deliberately, branches at both job and step scope, and then induces
a controlled failure so success(),
failure(), and !cancelled() can be
compared from the same run history.
Learning objectives
-
Create typed
workflow_dispatchinputs and prove the difference betweeninputsandgithub.event.inputs. -
Inspect
github,runner,job, andstepscontext fields without whole-context dumps. -
Produce string outputs and normalize them with
fromJSONbefore Boolean/numeric decisions. -
Use
toJSONonly on synthetic bounded data rather than secrets or whole contexts. - Observe status functions and step conclusions after an intentional failure while preserving run evidence.
1. Disposable lab and exact assumptions
| Item | Chapter 04 lab assumption |
|---|---|
| Repository | Learner-owned disposable repository; workflow on default branch so manual dispatch is available. |
| Runner | ubuntu-24.04 GitHub-hosted runner. |
| Permissions | permissions: {}; no GitHub API mutation. |
| Actions | None; built-in shell steps only. |
| Secrets | None. All values are synthetic. |
| Side effects | Actions run history and logs only. |
Record the repository, workflow path, workflow commit SHA, and expected input values before dispatch. The lab is intentionally small so every expression can be explained.
2. Build the workflow progressively
name: Chapter 04 expression lab
on:
workflow_dispatch:
inputs:
enabled:
description: 'Run the main job'
type: boolean
required: true
default: true
force_failure:
description: 'Fail one step intentionally'
type: boolean
required: true
default: false
mode:
description: 'Synthetic operating mode'
type: choice
required: true
options: [quick, full]
threshold:
description: 'Numeric threshold'
type: number
required: true
default: 3
permissions: {}
defaults:
run:
shell: bash
jobs:
inspect:
if: ${{ inputs.enabled }}
runs-on: ubuntu-24.04
steps:
- name: Record typed and string views
env:
TYPED_ENABLED: ${{ inputs.enabled }}
EVENT_ENABLED: ${{ github.event.inputs.enabled }}
MODE: ${{ inputs.mode }}
THRESHOLD: ${{ inputs.threshold }}
EVENT_NAME: ${{ github.event_name }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
RUNNER_OS: ${{ runner.os }}
run: |
printf 'typed_enabled=%s event_enabled=%s\n' "$TYPED_ENABLED" "$EVENT_ENABLED"
printf 'mode=%s threshold=%s event=%s\n' "$MODE" "$THRESHOLD" "$EVENT_NAME"
printf 'ref=%s sha=%s runner_os=%s\n' "$REF" "$SHA" "$RUNNER_OS"
- name: Produce string outputs
id: producer
run: |
echo 'count=4' >> "$GITHUB_OUTPUT"
echo 'flag=true' >> "$GITHUB_OUTPUT"
echo 'payload={"source":"chapter04","retries":2,"safe":true}' >> "$GITHUB_OUTPUT"
- name: Use explicit conversion
if: ${{ fromJSON(steps.producer.outputs.count) >= inputs.threshold }}
env:
COUNT_TEXT: ${{ steps.producer.outputs.count }}
FLAG_TYPED: ${{ fromJSON(steps.producer.outputs.flag) }}
PAYLOAD_JSON: ${{ toJSON(fromJSON(steps.producer.outputs.payload)) }}
run: |
printf 'count_text=%s flag_typed=%s\n' "$COUNT_TEXT" "$FLAG_TYPED"
printf 'synthetic_payload=%s\n' "$PAYLOAD_JSON"
- name: Deliberate failure
id: deliberate
if: ${{ inputs.force_failure }}
run: |
echo 'intentional failure for status-function evidence'
exit 7
- name: Failure-specific evidence
if: ${{ failure() && steps.deliberate.conclusion == 'failure' }}
run: echo 'failure path observed deliberately'
- name: Happy-path evidence
if: ${{ success() }}
run: echo 'all previous effective steps succeeded'
- name: Preserve final bounded evidence unless cancelled
if: ${{ !cancelled() }}
env:
PRODUCER_OUTCOME: ${{ steps.producer.outcome }}
PRODUCER_CONCLUSION: ${{ steps.producer.conclusion }}
DELIBERATE_OUTCOME: ${{ steps.deliberate.outcome }}
DELIBERATE_CONCLUSION: ${{ steps.deliberate.conclusion }}
run: |
printf 'producer=%s/%s deliberate=%s/%s\n' \
"$PRODUCER_OUTCOME" "$PRODUCER_CONCLUSION" \
"$DELIBERATE_OUTCOME" "$DELIBERATE_CONCLUSION"
The workflow has no checkout step because no repository file is needed. That keeps the exercise centered on dataflow and avoids introducing an action dependency merely for convention.
3. Run A — successful path
Dispatch with enabled=true,
force_failure=false, mode=quick,
threshold=3. Before pressing Run, predict:
-
The job is admitted because
inputs.enabledis Boolean true. - The conversion step runs because numeric 4 is greater than or equal to numeric 3.
- The deliberate-failure step is skipped.
- The failure-specific step is skipped.
- The
success()step runs. - The final
!cancelled()evidence step runs.
Record the run ID/attempt, exact workflow SHA, input values, runner OS, producer output strings, step conclusions, and final job conclusion.
4. Prove typed input behavior
The workflow prints both views of the same manual Boolean.
inputs.enabled preserves Boolean typing inside
expressions. github.event.inputs.enabled is the
event-payload representation and its Boolean value is represented as
a string. The shell log may render both as the text
true, so the log text alone does not prove the
expression type; the different conditional behavior in the next
lessons is stronger evidence.
inputs value
exists, use it as the contract. Do not downgrade it to a string and
reconstruct meaning later unless an integration requires that
representation.
5. Outputs are strings: normalize at the decision boundary
GITHUB_OUTPUT is a data-transfer mechanism. The
steps.producer.outputs.count expression is a string
even though its text is 4. The lab converts it with
fromJSON before comparing it to the typed numeric
manual input.
The same principle applies to Boolean text. A producer may write
flag=true; the downstream expression should use
fromJSON(steps.producer.outputs.flag) when it requires
Boolean semantics. Do not rely on non-empty-string truthiness.
6. Safe fromJSON/toJSON: serialize
synthetic bounded data
The producer emits a hard-coded JSON object.
fromJSON converts that string into an expression object
and toJSON serializes the bounded object for readable
evidence. This demonstrates the functions without touching
secrets or the whole github context.
github context can contain sensitive values
such as the token, while event fields can contain
attacker-controlled text. Serialization is not sanitization.
7. Run B — intentional failure and status functions
Dispatch the same workflow with force_failure=true.
Predict the divergence before execution:
- The producer and conversion steps run normally.
- The deliberate step exits 7 and records a failure.
-
A later step whose condition is only a business expression would
inherit the default
success()status gate and be suppressed. -
The explicit
failure() && steps.deliberate.conclusion == 'failure'step runs. - The explicit
success()step does not run. -
The
!cancelled()evidence step still runs because the run failed but was not cancelled. - The job remains failed; observing the failure does not rewrite it into success.
8. steps context: outcome, conclusion, and temporal
availability
A named step appears in the steps context after it has
executed or been skipped. Its outcome and
conclusion let later steps reason about what happened.
If continue-on-error is used, the distinction can
matter because conclusion reflects post-policy handling. This
chapter does not hide failures with continue-on-error;
it keeps the causal failure visible.
Referencing a step output before its producer is another lifecycle error. The YAML parser cannot make future runtime data exist earlier.
9. Mini challenge — locate the owning layer
Predict the result of each change before trying it:
-
Set
threshold=6. Which step is skipped and why? -
Set
enabled=false. Does a runner get assigned toinspect? -
Replace
fromJSON(steps.producer.outputs.count)with the raw output. Is the condition still reviewable even if loose coercion makes it appear to work? -
Move
if: runner.os == 'Linux'to the job level. Which phase makes that invalid?
Write your answer in terms of context availability/type/evaluation phase, not “GitHub is weird.”
Knowledge check
Run A has force_failure=false. Why is the
deliberate step skipped without failing the job?
Its step-level if evaluates to Boolean false, so
the step is skipped; no failing command executes.
Why does the conversion step use
fromJSON(steps.producer.outputs.count)?
Step outputs are strings; conversion makes the numeric comparison explicit and type-correct.
After the deliberate failure, why does the failure-evidence step run?
Its condition contains failure(), which overrides
the default implicit success() gate, and it also
verifies the named step conclusion.
Why does !cancelled() still run after a normal
failure?
The run failed but was not cancelled; the condition is designed to preserve bounded evidence after success or failure while respecting cancellation.
Does printing true from both
inputs.enabled and
github.event.inputs.enabled prove they have the
same type?
No. Shell output is text. Current GitHub semantics preserve the
Boolean in inputs while the event-input
representation uses strings.
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 lab uses only synthetic outputs and bounded
context fields. No whole-context dump, secret, repository
checkout, or external action is necessary.
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.