GitHub Actions Expressions and Contexts
**Last Updated:** November 2025 **Source:** Official GitHub Actions documentation and Context7 verified examples
Overview
GitHub Actions Expressions and Contexts
Last Updated: November 2025 Source: Official GitHub Actions documentation and Context7 verified examples
Table of Contents
Expression Syntax
GitHub Actions expressions use ${{ }} syntax to evaluate values dynamically.
Basic Usage:
- name: Print environment
run: echo "Running on ${{ runner.os }}"
- name: Conditional step
if: ${{ github.ref == 'refs/heads/main' }}
run: echo "On main branch"
# Note: 'if' doesn't require ${{ }}, it's implicit
- name: Conditional step (preferred)
if: github.ref == 'refs/heads/main'
run: echo "On main branch"
Where Expressions Can Be Used:
ifconditionals (implicit${{ }}, can omit)envvalues (must use${{ }})withinputs (must use${{ }})- Step
namevalues (must use${{ }}) - Job
outputs(must use${{ }}) environment.name(can use expressions for dynamic environments)
Important Notes:
- Expressions are interpolated before the job is sent to the runner
- Use environment variables via
envcontext for safer variable handling - Avoid direct interpolation of untrusted input (use
envcontext instead)
Contexts
Contexts are objects containing information about workflow runs, variables, environments, and more.
github context
Contains information about the workflow run and triggering event.
Common Properties:
# Event information
${{ github.event_name }} # Event that triggered workflow (push, pull_request, etc.)
${{ github.event.action }} # Action that triggered event (opened, synchronize, etc.)
# Repository information
${{ github.repository }} # owner/repo
${{ github.repository_owner }} # Repository owner
${{ github.ref }} # Full ref (refs/heads/main, refs/tags/v1.0.0)
${{ github.ref_name }} # Short ref (main, v1.0.0)
${{ github.sha }} # Commit SHA that triggered workflow
# Actor information
${{ github.actor }} # Username that triggered workflow
${{ github.triggering_actor }} # User that initiated the workflow run
# Workflow information
${{ github.workflow }} # Workflow name
${{ github.run_id }} # Unique workflow run ID
${{ github.run_number }} # Workflow run number
${{ github.job }} # Current job ID
# Pull request information (when event is pull_request)
${{ github.event.pull_request.number }}
${{ github.event.pull_request.title }}
${{ github.event.pull_request.head.ref }} # Source branch
${{ github.event.pull_request.base.ref }} # Target branch
${{ github.event.pull_request.head.sha }}
# Push information (when event is push)
${{ github.event.head_commit.message }}
${{ github.event.head_commit.author.name }}
Examples:
# Build image tagged with commit SHA
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
# Deploy only on main branch
- name: Deploy
if: github.ref == 'refs/heads/main'
run: ./deploy.sh
# Different behavior for PR vs push
- name: Set environment
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
echo "ENV=preview" >> $GITHUB_ENV
else
echo "ENV=production" >> $GITHUB_ENV
fi
env context
Access environment variables defined in workflow, job, or step.
env:
NODE_VERSION: '20'
BUILD_TYPE: 'production'
jobs:
build:
env:
API_URL: 'https://api.example.com'
steps:
- name: Print variables
run: |
echo "Node: ${{ env.NODE_VERSION }}"
echo "API: ${{ env.API_URL }}"
runner context
Information about the runner executing the job.
Common Properties:
${{ runner.os }} # OS (Linux, Windows, macOS)
${{ runner.arch }} # Architecture (X64, ARM64)
${{ runner.name }} # Runner name
${{ runner.temp }} # Temp directory path
${{ runner.tool_cache }} # Tool cache directory path
Examples:
# OS-specific commands
- name: Install dependencies
run: |
if [ "${{ runner.os }}" == "Linux" ]; then
sudo apt-get update
elif [ "${{ runner.os }}" == "macOS" ]; then
brew update
fi
# Cache key with OS
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.cache
key: ${{ runner.os }}-cache-${{ hashFiles('**/lock.file') }}
secrets context
Access encrypted secrets defined in repository or organization settings.
- name: Deploy to production
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh
Security Notes:
- Secrets are automatically masked in logs
- Use
echo "::add-mask::$VALUE"to mask additional values - Pass secrets via environment variables, not command arguments
matrix context
Access matrix configuration values when using matrix strategy.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
include:
- os: ubuntu-latest
node: 20
experimental: true
steps:
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node }}
- name: Mark experimental
if: matrix.experimental
run: echo "Experimental build"
steps context
Access information about steps that have already run.
steps:
- name: Run tests
id: tests
run: npm test
- name: Upload results
if: steps.tests.outcome == 'success'
run: ./upload-results.sh
- name: Use step output
run: echo "Test result: ${{ steps.tests.outputs.result }}"
Step Properties:
steps.<step_id>.outputs.<output_name>: Output valuesteps.<step_id>.outcome: Result beforecontinue-on-error(success,failure,cancelled,skipped)steps.<step_id>.conclusion: Final result aftercontinue-on-error
job context
Access information about currently running job.
jobs:
build:
outputs:
build-id: ${{ steps.build.outputs.id }}
steps:
- name: Build
id: build
run: echo "id=build-123" >> $GITHUB_OUTPUT
deploy:
needs: build
steps:
- name: Deploy build
run: ./deploy.sh ${{ needs.build.outputs.build-id }}
inputs context
Access workflow or reusable workflow inputs.
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
type: choice
options: [dev, staging, production]
debug:
description: 'Enable debug mode'
required: false
type: boolean
default: false
jobs:
deploy:
steps:
- name: Deploy to ${{ inputs.environment }}
run: ./deploy.sh ${{ inputs.environment }}
- name: Enable debug
if: inputs.debug
run: echo "Debug mode enabled"
Functions
String Functions
contains()
# Check if string contains substring
if: contains(github.ref, 'refs/tags/')
if: contains(github.event.head_commit.message, '[skip ci]')
if: contains(fromJSON('["main", "develop"]'), github.ref_name)
startsWith()
# Check if string starts with prefix
if: startsWith(github.ref, 'refs/tags/v')
if: startsWith(github.event.pull_request.title, 'feat:')
endsWith()
# Check if string ends with suffix
if: endsWith(github.ref, '/main')
if: endsWith(github.event.pull_request.head.ref, '-hotfix')
format()
# Format string with placeholders
- name: Print message
run: echo "${{ format('Building {0} on {1}', github.ref_name, runner.os) }}"
Type Conversion Functions
toJSON()
# Convert object to JSON string
- name: Print context
run: echo '${{ toJSON(github) }}'
- name: Print matrix
run: echo '${{ toJSON(matrix) }}'
fromJSON()
# Parse JSON string to object
strategy:
matrix:
config: ${{ fromJSON('{"versions":[18,20,22]}') }}
# Use with dynamic matrix
jobs:
setup:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: echo 'matrix={"version":["18","20"]}' >> $GITHUB_OUTPUT
build:
needs: setup
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
Hash Functions
hashFiles()
# Generate hash of file contents (for cache keys)
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
# Multiple patterns
key: ${{ hashFiles('**/*.go', '**/go.sum') }}
Status Check Functions
success()
# Run only if all previous steps succeeded
- name: Deploy
if: success()
run: ./deploy.sh
failure()
# Run only if any previous step failed
- name: Notify on failure
if: failure()
run: ./notify-failure.sh
always()
# Run regardless of previous step status
- name: Cleanup
if: always()
run: ./cleanup.sh
cancelled()
# Run if workflow was cancelled
- name: Cleanup on cancel
if: cancelled()
run: ./cancel-cleanup.sh
Operators
Comparison Operators
# Equality
if: github.ref == 'refs/heads/main'
if: runner.os != 'Windows'
# Logical
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.event_name == 'pull_request' || github.event_name == 'push'
if: "!(github.event_name == 'pull_request')"
# Comparison
if: github.event.pull_request.changed_files < 10
if: matrix.node-version >= 20
Operator Precedence
()!<,<=,>,>===,!=&&||
Common Patterns
Branch-Based Conditions
# Main branch only
if: github.ref == 'refs/heads/main'
# Any branch except main
if: github.ref != 'refs/heads/main'
# Specific branches
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
# Tag pushes only
if: startsWith(github.ref, 'refs/tags/')
# Version tags only (v1.0.0 format)
if: startsWith(github.ref, 'refs/tags/v')
Event-Based Conditions
# Push event only
if: github.event_name == 'push'
# PR opened or synchronized
if: |
github.event_name == 'pull_request' &&
(github.event.action == 'opened' || github.event.action == 'synchronize')
# Manual dispatch only
if: github.event_name == 'workflow_dispatch'
# Scheduled run only
if: github.event_name == 'schedule'
Step Status Patterns
# Run if specific step succeeded
if: steps.tests.outcome == 'success'
# Run if step failed but continue
if: steps.tests.outcome == 'failure'
# Run cleanup always
if: always()
# Run only on failure
if: failure()
Matrix Patterns
# Specific matrix combination
if: matrix.os == 'ubuntu-latest' && matrix.node == 20
# Exclude certain combinations
strategy:
matrix:
os: [ubuntu, windows, macos]
node: [18, 20, 22]
exclude:
- os: windows
node: 18
Dynamic Values
# Build tags with multiple values
tags: |
myapp:latest
myapp:${{ github.sha }}
myapp:${{ github.ref_name }}
# Conditional environment
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
# Dynamic timeout
timeout-minutes: ${{ github.event_name == 'schedule' && 120 || 30 }}
Combining Contexts
# Artifact name with context values
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: build-${{ runner.os }}-${{ github.sha }}
path: dist/
# Cache key with multiple factors
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.cache
key: ${{ runner.os }}-${{ hashFiles('**/*.lock') }}-${{ github.ref_name }}
Safe String Interpolation
# ❌ UNSAFE: Direct interpolation of user input
- run: echo "Title: ${{ github.event.pull_request.title }}"
# ✅ SAFE: Use environment variables
- name: Print PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Title: $PR_TITLE"
JSON Manipulation
# Create dynamic matrix
- id: set-matrix
run: |
if [ "${{ github.event_name }}" == "push" ]; then
echo 'matrix={"os":["ubuntu","windows","macos"]}' >> $GITHUB_OUTPUT
else
echo 'matrix={"os":["ubuntu"]}' >> $GITHUB_OUTPUT
fi
# Use matrix
strategy:
matrix: ${{ fromJSON(steps.set-matrix.outputs.matrix) }}
Debugging and Troubleshooting
Dumping Contexts to Logs
The most effective way to debug workflow issues is to dump context information to logs:
Dump All Contexts:
name: Context Debugging
on: push
jobs:
dump_contexts:
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- name: Dump job context
env:
JOB_CONTEXT: ${{ toJson(job) }}
run: echo "$JOB_CONTEXT"
- name: Dump steps context
env:
STEPS_CONTEXT: ${{ toJson(steps) }}
run: echo "$STEPS_CONTEXT"
- name: Dump runner context
env:
RUNNER_CONTEXT: ${{ toJson(runner) }}
run: echo "$RUNNER_CONTEXT"
- name: Dump strategy context
env:
STRATEGY_CONTEXT: ${{ toJson(strategy) }}
run: echo "$STRATEGY_CONTEXT"
- name: Dump matrix context
env:
MATRIX_CONTEXT: ${{ toJson(matrix) }}
run: echo "$MATRIX_CONTEXT"
Example Runner Context Output:
{
"os": "Linux",
"arch": "X64",
"name": "GitHub Actions 2",
"tool_cache": "/opt/hostedtoolcache",
"temp": "/home/runner/work/_temp"
}
Safe Variable Interpolation
Best Practice - Use env context:
# ✅ SAFE: Interpolate before runner execution
- name: Greet user
env:
GREETING: ${{ env.Greeting }}
FIRST_NAME: ${{ env.First_Name }}
DAY: ${{ env.DAY_OF_WEEK }}
run: echo "$GREETING $FIRST_NAME. Today is $DAY!"
This approach ensures variables are resolved by GitHub Actions before execution, providing consistent behavior.
Tips and Best Practices
-
Implicit vs Explicit
${{ }}:ifconditions don't need${{ }}(implicit)- Other contexts require explicit
${{ }}
-
String Comparisons:
- Always use quotes for string literals
- Case-sensitive by default
-
Boolean Values:
- Use
trueandfalsewithout quotes - Empty strings evaluate to
false
- Use
-
Default Values:
# Use || for default values environment: ${{ inputs.environment || 'dev' }} -
Multi-line Expressions:
if: | github.event_name == 'push' &