Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXLVIII · Conditional ExecutionExpressions

Expressions and context — `${ }`, `github.event`, `github.ref`, and the operators

Intermediate⏱ ~20 mingit

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

Not yet marked complete on this device.

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

  1. Quote every string operand. YAML is not a friend; quotes are the fence.
  2. Keep expressions short. A guard that takes more than one line to read is a guard the next engineer will misread.
  3. Test before merging. act with a representative event payload.
  4. 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 count and for_each.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer writes `if: contains(github.event.pull_request.labels.*.name, release)` - without quotes around `release`. What is the most likely failure mode?

  2. 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.

  3. 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.

  4. 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.