2.2.6. Contexts and Expressions
💡 First Principle: Contexts are the run's data model — structured objects describing the event, the repository, the runner, and the state of prior steps — and ${{ }} expressions are the only way to read them, evaluated by GitHub before the shell ever runs.
That last clause is the crux of both correct behavior and the security lesson in 6.1.2. ${{ }} is textual substitution performed by the workflow engine at evaluation time; by the time your run: script executes, the expression is gone and its value is sitting in the script as literal text.
The contexts you must recognize on sight:
| Context | Holds | Availability note |
|---|---|---|
github | Event payload, repo, ref, sha, actor, run_id | Everywhere |
env | Variables from env: blocks | Not available in env: itself at workflow level |
vars | Configuration variables (org/repo/env) | Everywhere |
secrets | Encrypted secrets | Not in runs-on, not in composite action steps directly |
inputs | workflow_dispatch / workflow_call / action inputs | Only when inputs are defined |
matrix | Current variant's values | Inside matrix jobs only |
needs | Upstream jobs' outputs and result | Jobs with needs: |
steps | Prior steps' outputs, outcome, conclusion | After the step has an id: |
job / runner | Job status, services; runner OS, arch, temp dirs | Job scope / step scope |
strategy | job-index, fail-fast, job-total | Matrix jobs |
Expression syntax supports comparison (==, !=, <), logic (&&, ||, !), and a small function library that shows up constantly: contains(), startsWith(), endsWith(), format(), join(), toJSON(), fromJSON(), hashFiles(), plus the four status functions from 2.2.1.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
- run: echo "Key ${{ hashFiles('**/package-lock.json') }}"
Static vs. runtime evaluation is the concept the objectives name directly. Some keys are resolved when the run is created — runs-on, services, job-level if — so they cannot see values produced later in the run (this is why secrets isn't allowed in runs-on). Step-level expressions resolve as the job progresses, so they can read steps.*.outputs from earlier steps. When a scenario shows an expression that "always evaluates to empty," the answer is usually that it was evaluated before the value existed.
⚠️ Exam Trap: Secrets are masked in logs, but masking only catches exact matches of the registered value. Transforming a secret — base64-encoding it, embedding it in a URL, printing it character-spaced — defeats masking. Never interpolate a secret into a place it can be reshaped, and never pass one through an expression into a shell command line where it lands in process listings.
Reflection Question: Why can runs-on: not reference secrets or a step output, while if: on a step can reference both? Tie your answer to when each key must be resolved.