GitHub Actions Workflow Logic Explained | Filters, Contexts, Variables & Expressions
Controlling workflow execution logic is a cornerstone of enterprise CI/CD pipeline design. Uncontrolled triggers consume runner resources, incur unnecessary execution costs, and slow down developer feedback loops. This chapter explores how to precisely control GitHub Actions workflows using event filters, activity types, context objects, environment variables, and conditional expression logic.
1. The Problem with Blanket Triggers (Why Execution Filtering Matters)
In introductory CI/CD setups, workflows often rely on generic event triggers such as on: push. Under this blanket configuration, GitHub Actions executes the workflow whenever any commit is pushed to any branch in the repository.
# Naive / Uncontrolled Execution Trigger
name: Blanket Trigger Pipeline
on: push # Fires on every push to any branch for any file change
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
Real-Life Analogy
Imagine a busy toll booth or airport security lane where every single vehicle or traveler—regardless of whether they are carrying cargo, driving a bicycle, or just turning around in the parking lot—is forced through a deep, manual security screening. The security checkpoint quickly becomes completely clogged, causing massive delays for commercial trucks that actually need to depart on time. Blanket triggers operate the exact same way by forcing every minor change through your entire automated CI pipeline.
Operational Risks of Blanket Triggers
- Unnecessary Compute Costs: In enterprise environments with paid runner allocations, running full build and test suites for minor changes (e.g., editing a
README.mdor updated documentation) wastes budget. - Runner Queue Bottlenecks: When using self-hosted runners with limited capacity, blanket triggers queue up unnecessary jobs. Critical hotfixes or release builds get stuck behind non-essential builds.
- Slow Feedback Loops: Developers must wait longer for CI pipeline feedback because shared runner pools are overburdened executing irrelevant jobs.
- Wasted Resource Allocation: Compute runners spend time downloading dependencies and setting up environments for files that do not affect the compiled binary or application runtime.
2. Event Filters & Wildcard Path Logic
To stop blanket execution, GitHub Actions provides event filters. Event filters dictate the precise conditions under which an incoming event triggers a workflow.
Core Filter Types
branches/branches-ignore: Defines which target git branches trigger or bypass the workflow.paths/paths-ignore: Specifies which changed file paths trigger or bypass the workflow.tags/tags-ignore: Restricts workflow triggering based on Git release tag patterns.
Note: The
branchesfilter andbranches-ignorefilter are mutually exclusive. You cannot use both within the same event block. The same rule applies topathsandpaths-ignore.
Filter Combination Logic (AND vs. OR)
Understanding how conditions combine inside a GitHub Actions file is critical for production pipeline design:
- AND Condition (Within a Single Event Block): When multiple filters are defined under a single event (e.g., both
branchesandpathsunderpush), all conditions must be met simultaneously for the workflow to execute. - OR Condition (Across Multiple Events): When multiple top-level events are defined (e.g.,
pushandpull_request), the workflow executes if either event satisfies its respective criteria.
Wildcard Glob Matching Mechanics
GitHub Actions uses glob pattern matching to evaluate branch names, tags, and file paths:
Production Configuration Example
name: Refined Production Event Trigger
on:
push:
branches:
- develop
- 'release/*' # Matches release/v1, release/v2.0, etc.
paths:
- 'app/**' # Recursive match inside the app directory
- 'docker/**' # Matches docker configs
- '!docs/**' # Explicitly ignore changes strictly within docs
jobs:
validate:
runs-on: ubuntu-latest
steps:
- run: echo "Triggered only on code modifications in app or docker directories."
3. Activity Types & Event Precision
While event filters control where changes happen (branches and paths), activity types define what specific operational action occurred to fire the event.
Many GitHub events encompass multiple sub-actions. For example, a pull_request event occurs when a PR is opened, closed, labeled, assigned, or updated.
Pull Request Default Activity Types
If you specify on: pull_request without defining activity types, GitHub Actions automatically listens to three specific activity types:
opened: A new pull request is created.reopened: A previously closed pull request is re-opened.synchronize: New commits are pushed to the source branch of an existing open pull request.
# Default Behavior (Implicit)
on:
pull_request:
# Explicit Configuration with Additional Activity Types
on:
pull_request:
types:
- opened
- synchronize
- reopened
- closed # Fire workflow when PR is merged or closed
- ready_for_review # Fire workflow when draft PR becomes active
The synchronize Activity Type Explained
The synchronize activity type is a vital CI mechanism. When a developer opens a Pull Request, CI runs to validate the code. If tests fail or changes are requested, the developer pushes additional commits to their feature branch.
Instead of requiring a new PR, GitHub triggers the pull_request workflow again via the synchronize event, re-testing the updated PR payload.
4. Visualizing Workflow Trigger Evaluation Flow
The execution engine evaluates incoming GitHub webhook payloads against defined triggers using a strict top-down flow:
5. Walkthrough: Trigger Filtering & Dual-Event Behavior
To see how event filters and activity types behave in practice, consider a scenario where both push and pull_request events are defined on a single workflow.
Workflow Configuration (01-filter-demo.yaml)
name: 01 Production Workflow Trigger Demo
on:
push:
branches:
- 'feature/*'
- develop
paths:
- 'app/**'
- 'docker/**'
pull_request:
branches:
- main
types:
- opened
- synchronize
jobs:
validation:
runs-on: ubuntu-latest
steps:
- name: Print Workflow Context
run: |
echo "Event Name: ${{ github.event_name }}"
echo "Ref Name: ${{ github.ref_name }}"
echo "Repository Owner: ${{ github.repository_owner }}"
Key Execution Behaviors Observed
- Pushing directly to
mainwith changes inapp/:- Result: Workflow does not run.
- Reason: The
pushevent filter only listens tofeature/*anddevelopbranches.
- Pushing to
feature/demowith changes outsideapp/ordocker/:- Result: Workflow does not run.
- Reason: Path filter fails (AND condition between branch and path filter).
- Pushing to
feature/demowith modifications insideapp/app.py:- Result: Workflow runs once under the
pushevent.
- Result: Workflow runs once under the
- Opening a Pull Request from
feature/demotargetingmain:- Result: Workflow runs once under the
pull_requestevent (openedactivity type).
- Result: Workflow runs once under the
- Pushing a new commit to
feature/demowhile the PR tomainremains OPEN:- Result: Workflow runs TWICE simultaneously:
- Run #1: Triggered by the
pushevent (because the commit hitfeature/demoand alteredapp/**). - Run #2: Triggered by the
pull_requestevent (synchronizeactivity type targetingmain).
- Run #1: Triggered by the
- Result: Workflow runs TWICE simultaneously:
6. Variables, Contexts, and Expressions Deep Dive
Once an execution trigger passes, GitHub Actions relies on three components to handle operational logic during runtime: Variables, Contexts, and Expressions.
1. Variables
Variables represent values exposed inside the runner’s shell execution environment during job execution.
- Default Variables: Built-in variables injected by the runner environment (e.g.,
GITHUB_REF,GITHUB_REPOSITORY,RUNNER_OS). - Custom Variables: Defined directly inside the workflow using the
env:key, or defined at the Repository/Organization level. - Scope & Usage: Best used inside
run:shell command scripts ($VAR_NAMEin bash or%VAR_NAME%in PowerShell).
2. Contexts
Contexts are dynamic, structured JSON objects provided by the GitHub Actions engine. They allow you to access workflow metadata, secrets, runner details, matrix values, and step outcomes before or during step execution.
Common Context Objects:
github: Information about the workflow run, triggering event, ref, actor, and repository.env: Custom variables defined at the workflow, job, or step level.runner: Information about the machine running the job (e.g.,runner.os,runner.temp).secrets: Contains sensitive values defined in the repository/organization settings.vars: Contains non-sensitive variables defined in repository/organization settings.
3. Expressions
Expressions allow you to programmatically evaluate logic, string operations, and conditions within the workflow YAML file using the ${{ <expression> }} syntax.
Common Expression Functions:
startsWith( search_string, arg ): Returnstrueifsearch_stringstarts witharg.endsWith( search_string, arg ): Returnstrueifsearch_stringends witharg.contains( search_container, search_level ): Returnstrueifsearch_containercontainssearch_level.format(...),join(...),toJSON(...).
Comparative Synthesis
7. Contexts vs. Variables: Best Practices Rule of Thumb
In GitHub Actions, several properties are exposed as both a context property and an environment variable. For example:
- Context Property:
${{ github.event_name }} - Environment Variable:
$GITHUB_EVENT_NAME
steps:
- name: Variable vs Context Best Practice Demonstration
env:
DEPLOY_ENV: "production"
run: |
# RECOMMENDED: Use shell environment variable inside shell scripts
echo "Branch name via environment variable: $GITHUB_REF_NAME"
echo "Custom environment variable: $DEPLOY_ENV"
# DISCOURAGED: Injecting context expressions directly into shell lines
echo "Branch name via context expression: ${{ github.ref_name }}"
Rule of Thumb:
- Use Variables (
$VAR_NAME) withinrun:script commands whenever possible. This prevents script injection vulnerabilities and avoids escaping issues.- Use Contexts & Expressions (
${{ github.ref }}) for workflow structure, setting inputs (with:), definingenv:key-value pairs, and controlling conditional block evaluations (if:).
8. Variable Scoping Hierarchy
Environment variables can be declared at multiple levels within GitHub Actions. Lower-level declarations override higher-level declarations with the same key name.
- Organization Level: Accessible by all designated repositories across the entire enterprise org.
- Repository Level: Defined under Repository Settings -> Secrets and Variables -> Actions. Accessible across all workflows in that repository.
- Workflow Level: Declared at the root level of the workflow file under the
env:block. - Job Level: Declared inside a specific job under
jobs.<job_id>.env. - Step Level: Declared inside a single step under
jobs.<job_id>.steps[].env.
9. Walkthrough: Expressions, Contexts, and Step Conditionals
This walkthrough demonstrates variable scoping, context evaluation, expression functions, and conditional step logic.
Workflow Configuration (02-expressions-demo.yaml)
name: 02 Expressions Variables Contexts Demo
on:
push:
branches:
- main
- 'feature/*'
# Workflow-Level Custom Variables
env:
APPLICATION_NAME: "payment-service"
DEPLOYMENT_ENVIRONMENT: "production"
AWS_REGION: "ap-south-1"
jobs:
demo-job:
runs-on: ubuntu-latest
steps:
- name: Print Variables, Contexts, and Expressions
run: |
echo "--- DEFAULT VARIABLES ---"
echo "Event Name: $GITHUB_EVENT_NAME"
echo "Ref Name: $GITHUB_REF_NAME"
echo "Commit SHA: $GITHUB_SHA"
echo "--- CUSTOM WORKFLOW VARIABLES ---"
echo "App Name: $APPLICATION_NAME"
echo "Environment: $DEPLOYMENT_ENVIRONMENT"
echo "AWS Region: $AWS_REGION"
echo "--- CONTEXT VALUES ---"
echo "Repository: ${{ github.repository }}"
echo "Actor: ${{ github.actor }}"
echo "Runner OS: ${{ runner.os }}"
- name: Demonstrate Expression Evaluation
run: |
echo "Is Main Branch? ${{ github.ref == 'refs/heads/main' }}"
echo "Is Feature Branch? ${{ startsWith(github.ref_name, 'feature') }}"
# CONDITIONAL EXECUTION: Step runs ONLY on feature branches
- name: Demonstrate Feature Branch Condition
if: ${{ startsWith(github.ref_name, 'feature') }}
run: |
echo "Feature branch detected! Executing feature-specific build logic."
# CONDITIONAL EXECUTION: Step runs ONLY on main branch
- name: Demonstrate Main Branch Condition
if: ${{ github.ref == 'refs/heads/main' }}
run: |
echo "Main branch detected! Executing production deployment pre-checks."
Execution Behavior Analysis
Case A: Execution on Push to main Branch
github.refevaluates to'refs/heads/main'.github.ref_nameevaluates to'main'.- The expression
${{ startsWith(github.ref_name, 'feature') }}evaluates tofalse. - Step "Demonstrate Feature Branch Condition": SKIPPED by the GitHub Actions evaluation engine.
- The expression
${{ github.ref == 'refs/heads/main' }}evaluates totrue. - Step "Demonstrate Main Branch Condition": EXECUTED.
Case B: Execution on Push to feature/payment-gate Branch
github.ref_nameevaluates to'feature/payment-gate'.- The expression
${{ startsWith(github.ref_name, 'feature') }}evaluates totrue. - Step "Demonstrate Feature Branch Condition": EXECUTED.
- Step "Demonstrate Main Branch Condition": SKIPPED.
10. Summary and Key Takeaways
- Avoid Blanket Triggers: Blanket
on: pushconfigurations consume compute resources unnecessarily, delay CI feedback loops, and queue jobs on runners. - Use Precise Event Filters: Combine
branchesandpathsfilters to restrict workflow execution strictly to meaningful code modifications. - Understand Pattern Matching: Use single asterisk
*for single-level path matching, and double asterisk**for recursive directory matching. - Leverage Activity Types: Use specific activity types (such as
opened,synchronize, andclosedunderpull_request) to align pipeline runs with developer actions. - Multiple Triggers Fire Independently: Defining both
pushandpull_requesttop-level triggers can cause a single push to an open PR feature branch to trigger two concurrent workflow runs. - Distinguish Variables vs. Contexts vs. Expressions:
- Variables operate inside runner shell scripts (
run:). - Contexts provide engine-level JSON metadata objects (
github,env,runner). - Expressions
${{ ... }}evaluate operational, string, and conditional logic.
- Variables operate inside runner shell scripts (
- Scope Variables Correctly: Manage variable precedence hierarchically across Organization, Repository, Workflow (
env:), Job (env:), and Step (env:) levels. - Master Step Conditionals: Control flow precision using step-level
if:expressions combined with functions likestartsWith(),contains(), and equality operators.