2.2.2. Environment Variables and Workflow Commands
💡 First Principle: A step can talk back to the runner — setting variables, masking values, grouping logs, creating annotations — through two channels: environment files (append a line to a special file) and workflow commands (echo a specially formatted string). Knowing which channel does what is the difference between data that persists and data that vanishes.
Environment variables come from a three-level hierarchy, with the innermost winning: workflow-level env:, job-level env:, then step-level env:. Layered on top are default variables GitHub always provides — GITHUB_REPOSITORY, GITHUB_REF, GITHUB_SHA, GITHUB_WORKSPACE, GITHUB_RUN_ID, RUNNER_OS and friends — which you may read but must not overwrite.
To create a variable that outlives the current step, append to the GITHUB_ENV file:
- name: Compute version
run: echo "APP_VERSION=1.4.$GITHUB_RUN_NUMBER" >> "$GITHUB_ENV"
- name: Use it
run: echo "Building $APP_VERSION" # available here, not in the step above
The environment-file family is the modern, supported mechanism, and each file has a distinct job:
| File / command | Purpose |
|---|---|
$GITHUB_ENV | Set an env var for subsequent steps in this job |
$GITHUB_OUTPUT | Set a step output consumed via steps.<id>.outputs.<name> |
$GITHUB_PATH | Prepend a directory to PATH for subsequent steps |
$GITHUB_STEP_SUMMARY | Append Markdown to the run's summary page |
::error file=…,line=…::msg | Create an error annotation on the run and in the diff |
::warning:: / ::notice:: | Lower-severity annotations |
::group:: / ::endgroup:: | Collapsible log sections |
::add-mask::value | Register a value so it is ***-redacted in all logs |
::debug:: | Message shown only when ACTIONS_STEP_DEBUG is enabled |
For multiline values, use the delimiter form, because a bare KEY=line1\nline2 corrupts the file:
- run: |
{
echo 'NOTES<<EOF'
cat release-notes.md
echo 'EOF'
} >> "$GITHUB_ENV"
⚠️ Exam Trap: A variable written to $GITHUB_ENV is not visible in the step that wrote it — environment files are processed between steps. Within the same step you must use an ordinary shell variable. Equally: ::set-output and ::set-env were deprecated in 2022 and disabled in 2023; any answer using them is wrong on a current exam.
Reflection Question: ::add-mask:: redacts a value from logs, but GitHub warns it isn't a substitute for treating the value as secret. What can still leak a masked value out of a run?