Git, CI/CD & GitOpsXLVIII · Conditional ExecutionExpressions
Expressions and context — `${ }`, `github.event`, `github.ref`, and the operators
What you'll learn
- Read and write `${ }` expressions in GitHub Actions workflows
- Identify the most-used context properties: `github.event`, `github.ref`, `github.actor`
- Apply the comparison, logical, and containment operators correctly
- Test expressions with `act` or by running the workflow on a test pull request
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
An expression in a GitHub Actions workflow is a string
delimited by ${ }. The delimiters say evaluate
this; the content is a small expression language
applied to a context object - the live state of the
run.
The minimum grammar
jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to production"
The expression github.ref == 'refs/heads/main'
evaluates to true when the ref is main, false
otherwise. The job runs when the expression is true.
The expression is evaluated once, before the runner is
allocated.
The most-used context properties: github.event_name
(the trigger name), github.ref (the full ref),
github.ref_name (the short ref), github.actor
(the login), github.event.* (the event payload),
github.repository (the slug).
Operators
flowchart LR
A[Expression] --> B{Operator class}
B -- "comparison" --> C["== != < >"]
B -- "logical" --> D["&& || !"]
B -- "containment" --> E["contains() startsWith() endsWith()"]
Three operator classes carry most of the load:
comparison (==, !=, <, > - case-sensitive
strings, numeric strings required for numeric
comparison: '10' > '9' is false); logical
(&&, ||, ! - YAML may interpret ! as a tag
prefix; quote the expression); containment
(contains(haystack, needle) on strings or arrays).
The form
contains(github.event.pull_request.labels.*.name, 'release')
flattens an array of objects into an array of name
strings, then tests membership.
Label-driven deploys
on:
pull_request:
types: [labeled, synchronize, reopened]
jobs:
release:
if: contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest
steps:
- run: build-and-publish
The expression flattens pull_request.labels to
['release', 'docs'] and tests for 'release'. If
the label set is empty, the flattened array is [],
contains returns false, and the job skips. Put as
much of the gate as possible into the trigger block -
a trigger that fires only for the right events needs
no guard at the job level. The context object is built
at the start of the run and frozen.
Testing expressions
act pull_request -e pull_request-event.json
jq '.pull_request.labels[].name' pull_request-event.json
act reads a workflow file, evaluates the expressions
against a supplied event payload, and prints the
resolved job graph.
Production discipline
- Quote every string operand. YAML is not a friend; quotes are the fence.
- Keep expressions short. A guard that takes more than one line to read is a guard the next engineer will misread.
- Test before merging.
actwith a representative event payload. - Trust the context, not the environment. A
${ }expression is evaluated server-side.
Cross-course references
- Ansible for Production Sysadmins - Part XXVI covers the same context-style guards at the playbook-task level.
- Terraform for Production Sysadmins - Part XXII
covers expression evaluation in
countandfor_each.
Quiz
Knowledge check · 4 questions
Q1. An engineer writes `if: contains(github.event.pull_request.labels.*.name, release)` - without quotes around `release`. What is the most likely failure mode?
Q2. If a job's `if:` guard evaluates to false and the job is skipped, a later step in the same job can modify the event payload and re-trigger the guard.
Q3. Explain the role of the `.*` operator in `contains(github.event.pull_request.labels.*.name, 'release')` and why the expression would be incorrect without it.
Q4. Diagnose why a label-driven release job never runs even though engineers have applied the `release` label.
The release job has `if: contains(github.event.pull_request.labels.*.name, release)`. Engineers apply the label via the GitHub UI. The job is skipped every time. A grep shows the unquoted `release` token.
Passing score: 75%. Answers are checked in this browser.