Mastering GitHub Actions Workflow Triggers (2025): Common Mistakes and How to Avoid Them
Featured Snippet Definition
GitHub Actions workflow triggers define the events that start a workflow. They are specified in the on: section of a workflow file and can include events like push, pull requests, schedules, or manual triggers. Properly designed triggers prevent wasted runs and ensure workflows execute at the right time.
Quick Formula:
Event ➜ Trigger (on:) ➜ Workflow ➜ Jobs ➜ Steps
Table of Contents
Introduction: Why Workflow Triggers Matter in Modern CI/CD
GitHub Actions workflow triggers are the foundation of your CI/CD automation strategy. They determine when your pipelines execute, which directly impacts build times, costs, and the overall efficiency of your development workflow.
The role of triggers in CI/CD automation extends beyond simple event detection. Well-designed GitHub Actions workflow triggers ensure:
- Builds run only when relevant code changes
- Resources aren’t wasted on unnecessary pipeline executions
- Security boundaries are maintained for untrusted code
- Teams can manually intervene when automation needs human oversight
Pitfalls of poorly designed triggers include:
- Unexpected workflow runs: A single push triggering multiple overlapping workflows, burning through GitHub Actions minutes
- Wasted compute resources: Building the entire monorepo when only documentation changed
- Security vulnerabilities: Running workflows with secrets on pull requests from forks
- Development friction: Slow feedback loops because triggers fire too broadly or too narrowly
This guide goes beyond basic syntax documentation. You’ll learn trigger design principles, debugging strategies, and security best practices that separate production-grade workflows from hobbyist configurations. Every YAML snippet is annotated and tested, ready for you to adapt to your infrastructure.
know more about what is github Actions
Trigger Fundamentals: Understanding Events & the on: Key
The on: section in your workflow YAML defines which GitHub events will start your workflow. Think of it as the contract between GitHub’s event system and your automation pipeline.
Basic Structure
# .github/workflows/example.yml
name: Example Workflow
on:
# Single event (string)
push
# OR multiple events (array)
on: [push, pull_request]
# OR detailed configuration (object)
on:
push:
branches: [main]
pull_request:
branches: [main]
Core GitHub Actions Events Overview
Here are the most commonly used GitHub Actions workflow triggers:
1. Push Event
Triggers when commits are pushed to the repository.
on:
push:
branches:
- main
- 'release/**' # Glob pattern: any branch starting with release/
tags:
- 'v*' # Any tag starting with v (e.g., v1.0.0)
2. Pull Request Event
Triggers on pull request activity.
on:
pull_request:
types: [opened, reopened, synchronize] # Default types
branches:
- main
paths:
- 'src/**' # Only trigger if src/ files change
- '!src/docs/**' # Exclude docs subdirectory
3. Schedule (Cron)
Runs workflows on a time-based schedule using cron syntax.
on:
schedule:
# Run every day at 2 AM UTC
- cron: '0 2 * * *'
# Run every Monday at 9 AM UTC
- cron: '0 9 * * 1'
4. Workflow Dispatch (Manual Trigger)
Allows manual workflow execution via UI or API.
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
debug:
description: 'Enable debug logging'
required: false
type: boolean
5. Repository Dispatch (Webhook)
Triggers workflows via external webhook events.
on:
repository_dispatch:
types: [deploy-backend, deploy-frontend]
Complete event reference: GitHub Docs: Events that trigger workflows

Filters & Conditions: Controlling Trigger Scope
GitHub Actions filters let you narrow when workflows execute. This is critical for optimizing build times and reducing costs.
Branch Filters
Control which branches trigger the workflow:
on:
push:
branches:
- main
- develop
- 'feature/**' # All feature branches
- '!feature/experimental' # Exclude specific branch
branches-ignore:
- 'dependabot/**' # Alternative: ignore pattern
Important: You cannot use both branches and branches-ignore for the same event. Choose one approach.
Tag Filters
Trigger on specific tag patterns:
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+' # Semantic versions like v1.2.3
- 'release-*' # Release tags
tags-ignore:
- 'beta-*' # Ignore beta tags
Path Filters
Execute workflows only when specific files change:
on:
push:
paths:
- 'src/**' # Any file in src directory
- 'package.json' # Specific file
- '**.js' # All JavaScript files (recursive)
paths-ignore:
- 'docs/**' # Ignore documentation changes
- '**.md' # Ignore markdown files
- '.github/**' # Ignore workflow changes
Real-world example: Monorepo with separate frontend and backend:
# .github/workflows/backend.yml
name: Backend CI
on:
pull_request:
paths:
- 'backend/**'
- 'shared/**'
- 'package.json'
paths-ignore:
- 'backend/docs/**'
- '**.md'
Event-Specific Type Filters
Many GitHub Actions events have subtypes. Filter to specific activity types:
on:
pull_request:
types:
- opened # New PR created
- reopened # Closed PR reopened
- synchronize # New commits pushed to PR
- ready_for_review # Draft converted to ready
issues:
types: [opened, labeled]
release:
types: [published, created]
Combining Multiple Filters
Filters combine with AND logic within an event, OR logic across events:
on:
push:
branches: [main] # AND
paths: ['src/**'] # Must match BOTH conditions
pull_request:
branches: [main] # OR (separate event)
paths: ['src/**']
Complex example: Only build when Go code changes in main or release branches:
name: Go Build
on:
push:
branches:
- main
- 'release/**'
paths:
- '**.go'
- 'go.mod'
- 'go.sum'
- '.github/workflows/go-build.yml' # Re-run if workflow changes
Glob Pattern Syntax & Caveats
GitHub Actions uses minimatch glob patterns with some quirks:
| Pattern | Matches | Example |
|---|---|---|
* | Any string except / | *.js matches app.js but not src/app.js |
** | Any string including / | src/** matches all files under src |
? | Single character | file?.txt matches file1.txt |
[abc] | Character set | [Mm]akefile matches Makefile or makefile |
! | Negation (must be first) | !*.md excludes markdown |
Common mistakes:
# ❌ WRONG: Doesn't match subdirectories
paths: ['src/*.js']
# ✅ CORRECT: Matches all JS in src tree
paths: ['src/**.js']
# ❌ WRONG: Negation not at start of pattern
paths: ['src/!test/**']
# ✅ CORRECT: Use paths-ignore instead
paths: ['src/**']
paths-ignore: ['src/test/**']
Advanced Trigger Patterns & Workflow Chaining
Advanced GitHub Actions workflow triggers enable sophisticated CI/CD architectures.
Workflow Run: Chaining Workflows
The workflow_run event triggers a workflow after another workflow completes:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
workflow_run:
workflows: ["CI Build"] # Name of the prerequisite workflow
types: [completed]
branches: [main]
jobs:
deploy:
# Only deploy if the CI workflow succeeded
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Deploy application
run: |
echo "CI passed, deploying..."
# Deployment logic here
Use case: Separate CI and CD concerns. Run tests in one workflow, deploy in another only if tests pass.
Security benefit: The deployment workflow can use elevated secrets that aren’t exposed during the test phase.
Workflow Call: Reusable Workflow Triggers
The workflow_call event makes workflows reusable as functions:
# .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
environment:
required: true
type: string
version:
required: false
type: string
default: 'latest'
outputs:
artifact-url:
description: "URL of the built artifact"
value: ${{ jobs.build.outputs.url }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
url: ${{ steps.upload.outputs.artifact-url }}
steps:
- name: Build for ${{ inputs.environment }}
run: echo "Building version ${{ inputs.version }}"
Calling the reusable workflow:
# .github/workflows/caller.yml
name: Main Pipeline
on: [push]
jobs:
staging-build:
uses: ./.github/workflows/reusable-build.yml
with:
environment: staging
version: '1.2.3'
production-build:
uses: ./.github/workflows/reusable-build.yml
with:
environment: production
secrets: inherit # Pass secrets to reusable workflow
Repository Dispatch: Cross-Repo Triggers
Trigger workflows in one repository from events in another:
# Repository A: .github/workflows/notify.yml
name: Notify Other Repos
on: [push]
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Trigger workflow in Repository B
run: |
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
https://api.github.com/repos/org/repo-b/dispatches \
-d '{"event_type":"deploy-backend","client_payload":{"version":"1.0.0"}}'
Repository B: Listening for the dispatch:
# Repository B: .github/workflows/deploy.yml
name: Deploy Backend
on:
repository_dispatch:
types: [deploy-backend]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy version ${{ github.event.client_payload.version }}
run: echo "Deploying backend"
Use case: Microservices architecture where service A changes trigger deployments in dependent service B.
Conditional Workflow Execution
Use if: conditions at the job level for fine-grained control:
name: Conditional Pipeline
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Run tests
run: npm test
deploy:
needs: test
# Only deploy on push to main, not on PRs
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: echo "Deploying..."
Manual & Parameterized Triggers: workflow_dispatch Deep Dive
The workflow_dispatch event is essential for manual interventions, ad-hoc testing, and parameterized deployments.
Basic Manual Trigger
name: Manual Deployment
on:
workflow_dispatch: # No inputs, just a manual trigger button
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Manually triggered deployment"
Triggering via GitHub UI: Go to Actions tab → Select workflow → “Run workflow” button
Triggering via GitHub CLI:
gh workflow run "Manual Deployment" \
--ref main
Parameterized Workflows with Inputs
name: Parameterized Deploy
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- development
- staging
- production
default: staging
version:
description: 'Version to deploy'
required: true
type: string
dry_run:
description: 'Perform dry run only'
required: false
type: boolean
default: false
log_level:
description: 'Logging verbosity'
type: choice
options:
- debug
- info
- warning
default: info
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy ${{ inputs.version }} to ${{ inputs.environment }}
run: |
echo "Environment: ${{ inputs.environment }}"
echo "Version: ${{ inputs.version }}"
echo "Dry run: ${{ inputs.dry_run }}"
echo "Log level: ${{ inputs.log_level }}"
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "DRY RUN MODE - No actual deployment"
else
echo "Deploying for real..."
fi
Triggering with parameters via CLI:
gh workflow run "Parameterized Deploy" \
--ref main \
-f environment=production \
-f version=v2.5.0 \
-f dry_run=false \
-f log_level=debug
Combining workflow_dispatch with Other Triggers
name: Flexible Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
skip_tests:
description: 'Skip test suite'
type: boolean
default: false
jobs:
test:
# Skip tests if manually triggered with skip_tests=true
if: github.event_name != 'workflow_dispatch' || !inputs.skip_tests
runs-on: ubuntu-latest
steps:
- name: Run tests
run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Build application
run: npm run build
Potential Pitfall: Multiple Trigger Overlap
Problem: A workflow with both pull_request and workflow_dispatch triggers might run twice unintentionally:
# ❌ Can cause duplicate runs
on:
pull_request:
workflow_dispatch:
# If you manually trigger this workflow while a PR is open,
# you might get two concurrent runs
Solution: Use concurrency groups to prevent overlapping runs:
on:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # Cancel older runs
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building..."
Trigger Design Best Practices for GitHub Actions
Designing efficient GitHub Actions workflow triggers requires balancing automation with resource optimization.
1. Avoid Overly Broad Triggers
Bad practice:
# ❌ Triggers on EVERY push to ANY branch
on: push
Better practice:
# ✅ Specific branches and paths
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- 'package.json'
paths-ignore:
- '**.md'
- 'docs/**'
2. Use Descriptive Workflow Names
Names appear in logs, pull request checks, and the Actions UI.
# ❌ Generic name
name: CI
# ✅ Descriptive name
name: Backend API - Test & Build
3. Implement Skip Logic with Commit Messages
GitHub Actions supports [skip ci] and [skip actions] in commit messages:
git commit -m "Update README [skip ci]"
git push
This prevents the workflow from running for documentation-only changes.
Caveat: This works for push events but not pull_request events (which are based on PR activity, not individual commits).
4. Use Concurrency Control
Prevent resource waste from overlapping workflow runs:
name: Deploy Pipeline
on:
push:
branches: [main]
concurrency:
group: production-deploy # Only one production deploy at a time
cancel-in-progress: false # Wait for current deploy to finish
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
For PR workflows, use dynamic grouping:
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true # Cancel outdated PR builds
5. Guard Job Execution with Conditionals
Add safety checks within jobs:
jobs:
deploy-production:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Deploying to production"
notify-failure:
if: failure() # Only runs if previous jobs failed
runs-on: ubuntu-latest
steps:
- name: Send alert
run: echo "Deployment failed!"
6. Optimize for Monorepos
Use path filters to trigger only affected services:
# .github/workflows/service-a.yml
name: Service A - CI/CD
on:
push:
branches: [main]
paths:
- 'services/service-a/**'
- 'shared/**'
- 'package.json'
pull_request:
paths:
- 'services/service-a/**'
- 'shared/**'
7. Separate CI and CD Workflows
Don’t mix testing and deployment in one workflow:
# ✅ Separate workflows
# .github/workflows/ci.yml - runs on all PRs
# .github/workflows/cd.yml - runs on main branch only
# Use workflow_run to chain them
Trigger Design Checklist
Before finalizing your on: section:
- [ ] Correct event type selected for the use case
- [ ] Branch filters applied to prevent unwanted runs
- [ ] Path filters used to skip irrelevant changes
- [ ] Event-specific
typesconfigured appropriately - [ ] Concurrency groups prevent resource waste
- [ ] Job-level
if:conditions add safety checks - [ ] Security implications reviewed (especially for forks)
- [ ] Workflow name is descriptive and specific
- [ ] Manual trigger (
workflow_dispatch) available if needed - [ ] Tested with a dry-run or test branch
Pitfalls, Quirks & Debugging Workflow Triggers
Even experienced engineers encounter unexpected GitHub Actions workflow trigger behavior.
Why a Workflow Didn’t Run
Common reasons:
- Filter mismatch: Branch or path filters excluded the event
# Workflow only runs on main, but you pushed to develop
on:
push:
branches: [main]
- Default branch assumptions: Some events default to the default branch (usually
main)
# This only monitors the default branch for changes
on:
schedule:
- cron: '0 0 * * *'
- Workflow file location: Must be in
.github/workflows/and be valid YAML - Event type not included: Pull request workflow only runs on
[opened, synchronize, reopened]by default
# Won't run when PR is labeled unless you add it
on:
pull_request:
types: [opened, synchronize, reopened, labeled]
- Workflow not on the target branch: For
pull_requestevents, the workflow file must exist in the base branch
Why Unexpected Runs Happened
Common causes:
- Overlapping triggers: Multiple events matching the same action
# Both triggers fire on a push to main
on:
push:
branches: [main]
pull_request:
branches: [main]
# Solution: Be more specific or use concurrency groups
- Glob pattern too broad:
# ❌ Matches ALL files (double star matches everything)
paths: ['**']
# ✅ Be specific
paths: ['src/**/*.js']
- Scheduled workflows on all branches: Cron jobs can run on multiple branches if workflow files differ
Common Glob Syntax Mistakes
| Mistake | Issue | Fix |
|---|---|---|
paths: ['*.js'] | Only matches root-level JS files | Use paths: ['**.js'] |
branches: [feature-*] | Missing quotes, treated as YAML comment | Use branches: ['feature-*'] |
!docs/** in middle of list | Negation must be first | Move to start or use paths-ignore |
Scheduled Trigger (Cron) Delays
GitHub Actions scheduled workflows can be delayed or dropped during high load:
- Scheduled workflows are queued, not guaranteed
- Delays of 15-60 minutes are common during peak times
- Workflows may be skipped entirely if the queue is too long
Mitigation:
# Add a check to see if the job is too delayed
jobs:
scheduled-task:
runs-on: ubuntu-latest
steps:
- name: Check if we're on time
run: |
SCHEDULED_TIME="0 2 * * *" # Expected 2 AM
CURRENT_HOUR=$(date +%H)
if [ $CURRENT_HOUR -gt 3 ]; then
echo "::warning::Scheduled job ran late (expected 2 AM, now ${CURRENT_HOUR}:00)"
fi
Real-World Example: Double Trigger Investigation
A Stack Overflow user reported that their workflow ran twice on every pull request. Here’s the scenario:
# Their configuration
on:
pull_request:
workflow_dispatch:
# What happened:
# 1. PR opened → workflow runs (correct)
# 2. They manually triggered the workflow via UI to debug
# 3. Two workflows ran concurrently on the same commit
Solution: Use concurrency groups to prevent overlapping runs:
on:
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
Debugging Strategies
1. Inspect the event payload:
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
run: echo '${{ toJSON(github) }}'
- name: Show event name
run: echo "Triggered by: ${{ github.event_name }}"
- name: Show ref
run: echo "Ref: ${{ github.ref }}"
2. Check workflow logs: GitHub shows why a workflow ran in the workflow run summary
3. Use the GitHub CLI to inspect recent runs:
gh run list --workflow="CI Build"
gh run view <run-id>
4. Test with a dedicated test branch:
git checkout -b test-workflow-triggers
# Make test changes
git push origin test-workflow-triggers
# Observe which workflows trigger
5. Validate YAML syntax locally:
# Install actionlint
brew install actionlint
# Validate workflow files
actionlint .github/workflows/*.yml
Security Considerations for GitHub Actions Workflow Triggers
GitHub Actions workflow triggers can expose secrets and create vulnerabilities if misconfigured.
Risks When Running Workflows from Forks
The pull_request event from forks runs with restricted permissions:
on:
pull_request: # Runs from fork, NO write access to base repo
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Run tests
run: npm test
# ✅ Safe: No secrets exposed, read-only access
Problem: The pull_request_target event runs in the context of the base repository with full access to secrets:
on:
pull_request_target: # ⚠️ Runs with base repo permissions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # Checking out PR code
- name: Run untrusted code
run: npm test # ❌ DANGER: Running potentially malicious code with secrets
Attack vector: A malicious contributor could modify package.json to exfiltrate secrets during npm install.
Safe Use of pull_request_target
Only use pull_request_target when you need to write back to the PR (comments, labels) and ensure untrusted code doesn’t run:
on:
pull_request_target:
jobs:
label-pr:
runs-on: ubuntu-latest
steps:
# ✅ Safe: Not checking out PR code
- name: Add label
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['needs-review']
})
Limiting Secret Exposure
1. Use environment secrets for sensitive workflows:
jobs:
deploy:
environment: production # Requires manual approval
runs-on: ubuntu-latest
steps:
- name: Deploy with secrets
run: |
echo "Deploying with ${{ secrets.PROD_API_KEY }}"
2. Restrict workflows to specific branches:
on:
push:
branches: [main] # Only main branch can trigger deployment
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.PROD_API_KEY }}
run: ./deploy.sh
3. Use workflow_dispatch for privileged operations:
on:
workflow_dispatch: # Manual approval required
jobs:
privileged-task:
runs-on: ubuntu-latest
steps:
- name: Access sensitive resources
env:
SECRET: ${{ secrets.ADMIN_TOKEN }}
run: echo "Performing privileged operation"
Security Checklist
- [ ]
pull_request_targetworkflows don’t check out or execute PR code - [ ] Secrets are scoped to specific environments with protection rules
- [ ] Deployment workflows limited to trusted branches (main, production)
- [ ] Manual approval required for production deployments (
workflow_dispatch+ environments) - [ ] Third-party actions pinned to specific SHA (not
@v1tags) - [ ] Fork PRs cannot trigger workflows that access secrets
Trigger Performance & Scaling Best Practices
Inefficient GitHub Actions workflow triggers can lead to high costs and slow development velocity.
Cost of Excessive Workflow Runs
Example calculation: A team with 50 developers, each creating 5 PRs per week:
50 developers × 5 PRs/week × 3 commits/PR = 750 commits/week
If each commit triggers 3 workflows (lint, test, build):
750 × 3 = 2,250 workflow runs/week
At 5 minutes per workflow:
2,250 × 5 = 11,250 minutes/week ≈ 45,000 minutes/month
For private repos: $0.008/minute = $360/month
Optimization impact: Adding path filters to skip irrelevant changes:
on:
pull_request:
paths:
- 'src/**' # Only source code
- 'package.json'
paths-ignore:
- '**.md' # Skip documentation
Could reduce runs by 30-40%, saving $100+/month.
Deduplication Strategies
1. Skip redundant runs with path filters (shown above)
2. Use concurrency groups to cancel stale runs:
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true # Cancel older runs when new commits pushed
3. Implement smart caching:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- run: npm install
- run: npm test
4. Combine multiple checks into one workflow:
# ❌ Inefficient: 3 separate workflows
# - lint.yml
# - test.yml
# - build.yml
# ✅ Efficient: 1 workflow with 3 jobs
name: CI Pipeline
on:
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
runs-on: ubuntu-latest
needs: [lint, test] # Only build if lint & test pass
steps:
- run: npm run build
Scaling Triggers in Large Repositories
Monorepo strategies:
# .github/workflows/service-matrix.yml
name: Monorepo CI
on:
pull_request:
paths:
- 'services/**'
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.filter.outputs.changes }}
steps:
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
service-a:
- 'services/service-a/**'
service-b:
- 'services/service-b/**'
service-c:
- 'services/service-c/**'
build:
needs: detect-changes
if: needs.detect-changes.outputs.services != '[]'
strategy:
matrix:
service: ${{ fromJSON(needs.detect-changes.outputs.services) }}
runs-on: ubuntu-latest
steps:
- name: Build ${{ matrix.service }}
run: echo "Building ${{ matrix.service }}"
Queue management for high-traffic repos:
concurrency:
# Limit concurrent runs per branch
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15 # Prevent stuck jobs from blocking queue
steps:
- run: npm test
Real-World Use Cases & Examples
Let’s explore practical GitHub Actions workflow trigger patterns used in production environments.
Use Case 1: Monorepo – Build Only Changed Modules
Scenario: A monorepo with multiple services where each service should build independently.
# .github/workflows/monorepo-ci.yml
name: Monorepo CI/CD
on:
pull_request:
paths:
- 'services/**'
- 'shared/**'
push:
branches: [main]
paths:
- 'services/**'
- 'shared/**'
jobs:
# Detect which services changed
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
api: ${{ steps.filter.outputs.api }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
frontend:
- 'services/frontend/**'
- 'shared/**'
backend:
- 'services/backend/**'
- 'shared/**'
api:
- 'services/api/**'
- 'shared/**'
# Build frontend only if changed
build-frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build frontend
run: |
cd services/frontend
npm install
npm run build
# Build backend only if changed
build-backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build backend
run: |
cd services/backend
go build ./...
# Build API only if changed
build-api:
needs: changes
if: needs.changes.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build API
run: |
cd services/api
mvn clean package
Use Case 2: Cross-Repo Microservices Deployment
Scenario: Service A changes should trigger builds in dependent Service B.
# Repository: service-a
# .github/workflows/deploy.yml
name: Deploy Service A
on:
push:
branches: [main]
paths:
- 'src/**'
- 'Dockerfile'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and deploy Service A
run: |
docker build -t service-a:${{ github.sha }} .
# Deployment logic
- name: Trigger Service B rebuild
run: |
curl -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.DISPATCH_TOKEN }}" \
https://api.github.com/repos/myorg/service-b/dispatches \
-d '{
"event_type": "service-a-updated",
"client_payload": {
"service_a_version": "${{ github.sha }}",
"triggered_by": "${{ github.actor }}"
}
}'
# Repository: service-b
# .github/workflows/rebuild.yml
name: Rebuild Service B
on:
repository_dispatch:
types: [service-a-updated]
push:
branches: [main]
jobs:
rebuild:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update Service A dependency
if: github.event_name == 'repository_dispatch'
run: |
echo "Service A updated to: ${{ github.event.client_payload.service_a_version }}"
# Update dependency reference
- name: Build Service B
run: docker build -t service-b:${{ github.sha }} .
Use Case 3: Tag-Based Release Pipeline
Scenario: Pushing a semantic version tag triggers production deployment.
# .github/workflows/release.yml
name: Release Pipeline
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+' # Matches v1.0.0, v2.3.4, etc.
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate tag format
run: |
TAG="${{ github.ref_name }}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid tag format: $TAG"
exit 1
fi
- name: Check if tag matches package.json version
run: |
TAG="${{ github.ref_name }}"
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if [ "$TAG" != "v$PACKAGE_VERSION" ]; then
echo "Tag $TAG doesn't match package.json version v$PACKAGE_VERSION"
exit 1
fi
build:
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build release artifact
run: npm run build
- name: Create release archive
run: tar -czf release-${{ github.ref_name }}.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: release-artifact
path: release-${{ github.ref_name }}.tar.gz
deploy-production:
needs: build
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: release-artifact
- name: Deploy to production
run: |
echo "Deploying version ${{ github.ref_name }} to production"
# Deployment commands
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
draft: false
prerelease: false
Triggering the release:
# Ensure version is updated in package.json
git tag v1.2.3
git push origin v1.2.3
Use Case 4: Scheduled Maintenance Tasks
Scenario: Daily database cleanup and weekly dependency updates.
# .github/workflows/maintenance.yml
name: Scheduled Maintenance
on:
schedule:
# Daily at 2 AM UTC - database cleanup
- cron: '0 2 * * *'
# Weekly on Monday at 6 AM UTC - dependency updates
- cron: '0 6 * * 1'
workflow_dispatch: # Allow manual runs
inputs:
task:
description: 'Maintenance task to run'
type: choice
options:
- database-cleanup
- dependency-update
- both
default: both
jobs:
database-cleanup:
# Run daily or when manually triggered
if: |
(github.event.schedule == '0 2 * * *') ||
(github.event_name == 'workflow_dispatch' &&
(inputs.task == 'database-cleanup' || inputs.task == 'both'))
runs-on: ubuntu-latest
steps:
- name: Clean old database records
run: |
echo "Cleaning records older than 90 days"
# Cleanup logic
dependency-update:
# Run weekly or when manually triggered
if: |
(github.event.schedule == '0 6 * * 1') ||
(github.event_name == 'workflow_dispatch' &&
(inputs.task == 'dependency-update' || inputs.task == 'both'))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update dependencies
run: |
npm update
npm audit fix
- name: Create PR if changes exist
run: |
if [[ -n $(git status -s) ]]; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b dependency-update-$(date +%Y%m%d)
git add package.json package-lock.json
git commit -m "chore: update dependencies"
git push origin dependency-update-$(date +%Y%m%d)
gh pr create \
--title "chore: weekly dependency update" \
--body "Automated dependency update" \
--base main
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Use Case 5: Matrix Strategy with Path Filters
Scenario: Test multiple Node.js versions, but only when relevant files change.
# .github/workflows/node-matrix.yml
name: Node.js Multi-Version Tests
on:
pull_request:
paths:
- '**.js'
- '**.ts'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/node-matrix.yml'
push:
branches: [main]
paths:
- '**.js'
- '**.ts'
- 'package.json'
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18.x, 20.x, 22.x]
# Exclude some combinations to reduce runs
exclude:
- os: windows-latest
node-version: 18.x
- os: macos-latest
node-version: 18.x
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Report coverage
if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20.x'
run: npm run coverage
Conclusion & Trigger Design Checklist
Mastering GitHub Actions workflow triggers is essential for building efficient, secure, and cost-effective CI/CD pipelines. Well-designed triggers ensure your workflows run at the right time, on the right events, consuming only the resources necessary.
Key Takeaways
- Triggers control when workflows execute – The
on:section is the contract between GitHub events and your automation - Filters reduce waste – Branch, path, and type filters prevent unnecessary runs
- Advanced patterns enable complex workflows – Use
workflow_run,workflow_call, andrepository_dispatchfor sophisticated architectures - Security requires careful design – Be cautious with
pull_request_targetand secret exposure - Performance optimization saves money – Path filters and concurrency groups can reduce costs by 30-50%
Pre-Deployment Trigger Checklist
Before committing your workflow file, verify:
Event Selection
- [ ] Correct event type chosen for your use case (
push,pull_request,schedule, etc.) - [ ] Event-specific
types:configured if needed (e.g.,[opened, synchronize]) - [ ] Multiple events don’t overlap unintentionally
Filters & Scope
- [ ] Branch filters applied to target specific branches
- [ ] Path filters prevent runs on irrelevant file changes
- [ ] Glob patterns tested and validated (remember
**vs*) - [ ]
paths-ignoreused for documentation-only changes
Performance & Resources
- [ ] Concurrency groups configured to prevent duplicate runs
- [ ]
cancel-in-progressset appropriately for PR workflows - [ ] Workflow name is descriptive and specific
- [ ] Job-level
if:conditions guard expensive operations
Security
- [ ] Secrets not exposed to untrusted pull requests
- [ ]
pull_request_targetused only when necessary and safely - [ ] Deployment workflows restricted to protected branches
- [ ] Environment protection rules configured for production
- [ ] Third-party actions pinned to commit SHA
Testing & Debugging
- [ ] Workflow tested on a feature branch first
- [ ] YAML syntax validated with
actionlintor similar - [ ] Event payload inspection added for debugging if needed
- [ ] Manual trigger (
workflow_dispatch) available for ad-hoc runs
Documentation
- [ ] Workflow purpose documented in comments
- [ ] Complex filters explained with inline comments
- [ ] Team knows how to manually trigger if needed
Next Steps
- Audit your existing workflows – Review current triggers for optimization opportunities
- Implement path filters – Start with high-frequency workflows to maximize savings
- Add concurrency controls – Prevent wasted runs from overlapping executions
- Secure fork workflows – Review any
pull_request_targetusage - Monitor usage – Track GitHub Actions minutes and optimize accordingly
GitHub Actions workflow triggers are powerful when used correctly. Take the time to design them thoughtfully, and you’ll build CI/CD pipelines that are fast, cost-effective, and secure.
How to Configure GitHub Actions Workflow Triggers Step by Step
Follow these steps to configure GitHub Actions workflow triggers correctly:
- Open or create a workflow YAML file in
.github/workflows/directory (e.g.,.github/workflows/ci.yml) - Define the
on:section with the appropriate event type for your use case (e.g.,push,pull_request,schedule,workflow_dispatch) - Apply branch and tag filters to scope the trigger to specific branches (e.g.,
branches: [main, develop]) or tags (e.g.,tags: ['v*']) - Add path filters to trigger only when relevant files change (e.g.,
paths: ['src/**']) and exclude documentation changes (e.g.,paths-ignore: ['**.md']) - Configure event-specific
types:filters for fine-grained control (e.g.,types: [opened, reopened, synchronize]for pull requests) - Test the trigger by creating a test branch, making changes, and pushing or creating a pull request to verify the workflow runs as expected
- Inspect workflow logs for the
github.event_namevalue to confirm which event triggered the workflow and debug any issues - Refine filters iteratively based on observed behavior to prevent unnecessary runs and optimize resource usage
Comparison Tables
When to Use: Push vs Pull Request vs Workflow Dispatch
| Trigger Type | Best Use Cases | Pros | Cons | Example Scenario |
|---|---|---|---|---|
| push | CI on merged code, deployments after merge | Runs on actual repository state, good for CD | Runs after code is merged, can’t block bad code | Deploy to staging when code merges to main |
| pull_request | PR validation, tests, linting | Provides feedback before merge, safe for forks | Doesn’t run on merged state | Run tests and lint checks on all PRs |
| workflow_dispatch | Manual deployments, ad-hoc tasks, troubleshooting | Full control over execution, parameterizable | Requires manual intervention, not automated | Deploy to production with specific version |
| schedule | Periodic tasks, maintenance, reports | Automated time-based execution | Can be delayed/dropped, no guarantees | Daily database cleanup at 2 AM |
| repository_dispatch | Cross-repo automation, external integrations | Enables complex multi-repo workflows | Requires API token management | Service A triggers build in Service B |
Workflow_run vs Workflow_call
| Feature | workflow_run | workflow_call |
|---|---|---|
| Purpose | Chain workflows sequentially | Reuse workflow logic as a function |
| Trigger | Automatic after another workflow completes | Called explicitly by other workflows |
| Context | New workflow run with different context | Runs within caller’s context |
| Secrets | Requires explicit passing | Can inherit from caller |
| Use Case | Separate CI and CD concerns | DRY – share common build steps |
| Example | Deploy only after CI passes | Reusable build workflow for multiple services |
workflow_run example:
on:
workflow_run:
workflows: ["CI"]
types: [completed]
workflow_call example:
on:
workflow_call:
inputs:
environment:
required: true
type: string
GitHub-Hosted vs Self-Hosted Trigger Handling
| Aspect | GitHub-Hosted Runners | Self-Hosted Runners |
|---|---|---|
| Trigger Latency | ~5-30 seconds | ~1-10 seconds (depends on runner availability) |
| Concurrent Runs | High capacity, auto-scaled | Limited by your infrastructure |
| Cost Model | Pay per minute (free tier available) | Pay for infrastructure, unlimited minutes |
| Trigger Security | Isolated ephemeral environments | Persistent state, requires careful security |
| Best For | Public repos, standard workflows | Private repos with high usage, custom hardware needs |
| Trigger Considerations | No special configuration needed | May need concurrency limits to prevent overload |
Appendix: Reference Materials
Full Event Trigger List
Comprehensive list of GitHub Actions events (link to official docs for full details):
Repository Events:
push– Commits pushed to repositorypull_request– PR opened, updated, or closedpull_request_target– PR from fork (runs with base repo permissions)create– Branch or tag createddelete– Branch or tag deletedfork– Repository forkedwatch– Repository starred
Issue & PR Events:
issues– Issue opened, edited, closed, etc.issue_comment– Comment on issue or PRpull_request_review– PR review submittedpull_request_review_comment– Comment on PR review
Release & Package Events:
release– Release created, published, editedregistry_package– Package published or updated
Workflow Events:
workflow_dispatch– Manual trigger via UI or APIworkflow_run– Another workflow completesworkflow_call– Called by another workflowrepository_dispatch– External webhook trigger
Schedule & Other:
schedule– Cron-based time triggerpage_build– GitHub Pages buildstatus– Commit status updatedcheck_run/check_suite– CI check updates
Full documentation: Events that trigger workflows
Glob & Filter Pattern Cheat Sheet
| Pattern | Description | Example | Matches | Doesn’t Match |
|---|---|---|---|---|
* | Any chars except / | *.js | app.js | src/app.js |
** | Any chars including / | src/** | src/app.js, src/lib/util.js | app.js |
? | Single character | file?.txt | file1.txt, fileA.txt | file10.txt |
[abc] | Character set | [Mm]akefile | Makefile, makefile | MAKEFILE |
[0-9] | Character range | test[0-9].js | test1.js, test9.js | test10.js |
{a,b} | Alternatives | {README,LICENSE} | README, LICENSE | readme |
! | Negation (at start) | !*.md | Excludes all .md files | N/A |
Common patterns:
# All JavaScript files recursively
paths: ['**.js']
# Only root-level JavaScript
paths: ['*.js']
# Everything in src except tests
paths:
- 'src/**'
paths-ignore:
- 'src/**/*.test.js'
- 'src/__tests__/**'
# Multiple file types
paths:
- '**.js'
- '**.ts'
- '**.jsx'
- '**.tsx'
# Specific directories
paths:
- 'services/*/src/**' # Any service's src directory
Key Documentation Links
- Events that trigger workflows: https://docs.github.com/actions/using-workflows/events-that-trigger-workflows
- Workflow syntax reference: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
- Filter pattern cheat sheet: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
- Security hardening: https://docs.github.com/actions/security-guides/security-hardening-for-github-actions
- Using concurrency: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
- Workflow commands: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions
ASCII Workflow Trigger Flow Diagram
┌─────────────────┐
│ GitHub Event │
│ (push, PR, │
│ schedule...) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Trigger Check │
│ (`on:` key) │
│ │
│ • Branch match? │
│ • Path match? │
│ • Type match? │
└────────┬────────┘
│
┌────┴────┐
│ Match? │
└────┬────┘
│
┌────▼────┐
│ Yes │ ┌──────┐
└────┬────┘ │ No │──> Workflow skipped
│ └──────┘
▼
┌─────────────────┐
│ Workflow Starts │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Job 1 runs │
│ (if condition │
│ evaluates) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Steps execute │
│ sequentially │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Workflow Result │
│ (success/fail) │
└─────────────────┘
Internal Linking Opportunities
Throughout your DevOps journey on thedevopstooling.com, these related articles will help deepen your GitHub Actions expertise:
- Getting started? Read GitHub Actions Basics: Your First Workflow to understand fundamental concepts before diving into advanced triggers
- Need context on workflow structure? Check out GitHub Actions Workflow Anatomy Explained for a complete breakdown of workflow components
- Using GitHub Actions for infrastructure? See Terraform CI/CD with GitHub Actions for infrastructure-as-code automation patterns
- Deploying containerized apps? Our Kubernetes Deployments with GitHub Actions guide covers trigger strategies for K8s workloads
- Security concerns? Review GitHub Actions Security Best Practices for comprehensive security hardening beyond triggers
FAQs: GitHub Actions Workflow Triggers
1. What are workflow triggers in GitHub Actions?
Workflow triggers are events that cause a GitHub Actions workflow to execute. They’re defined in the on: section of a workflow YAML file and can include code events (push, pull request), time-based schedules (cron), manual triggers (workflow_dispatch), or external webhooks (repository_dispatch). Triggers determine when your CI/CD automation runs.
2. How do I trigger a workflow on push or pull request?
To trigger on push events, use:
on:
push:
branches: [main]
For pull requests:
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
You can combine both events in the same workflow and add filters for branches, paths, or tags to control exactly when workflows execute.
3. What is workflow_dispatch in GitHub Actions?
workflow_dispatch is a manual trigger that allows you to run workflows on-demand through the GitHub UI, API, or CLI. It’s useful for deployments, ad-hoc tasks, or testing. You can add input parameters to make the workflow interactive:
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options: [staging, production]
4. How can I run workflows across repositories?
Use repository_dispatch to trigger workflows in other repositories. The triggering repo sends a webhook event via the GitHub API:
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/owner/repo/dispatches \
-d '{"event_type":"deploy","client_payload":{"version":"1.0"}}'
The receiving repo listens with:
on:
repository_dispatch:
types: [deploy]
5. Why didn’t my workflow trigger in GitHub Actions?
Common reasons include: (1) Branch or path filters don’t match your changes, (2) The workflow file doesn’t exist in the target branch, (3) YAML syntax errors prevent workflow registration, (4) For pull requests, event types: don’t include the activity that occurred, or (5) The workflow is disabled in repository settings. Check the Actions tab for errors and validate your YAML syntax with actionlint.
6. What are best practices for GitHub Actions workflow triggers?
Best practices include: (1) Use specific branch and path filters to avoid unnecessary runs, (2) Implement concurrency groups with cancel-in-progress to prevent resource waste, (3) Separate CI and CD workflows for better security, (4) Use workflow_dispatch for sensitive operations requiring manual approval, (5) Add job-level if: conditions for additional safety, (6) Monitor usage and optimize expensive workflows, and (7) Be cautious with pull_request_target to prevent secret exposure from forks.
Downloadable Resources
Workflow Trigger Checklist (PDF-Ready Content)
GitHub Actions Workflow Trigger Checklist
Before deploying your workflow, verify:
Event Configuration
- ☐ Event type matches use case (push/PR/schedule/dispatch)
- ☐ Event activity types configured (
types:array) - ☐ Multiple events don’t create unintended overlaps
Filters & Optimization
- ☐ Branch filters target correct branches
- ☐ Path filters prevent irrelevant triggers
- ☐ Glob patterns tested and validated
- ☐ Documentation changes excluded (**.md)
- ☐ Test files excluded when appropriate
Performance
- ☐ Concurrency groups configured
- ☐ cancel-in-progress set for PR workflows
- ☐ Workflow timeout specified (timeout-minutes)
- ☐ Caching strategy implemented
- ☐ Matrix strategy optimized (exclude unused combinations)
Security
- ☐ No secrets exposed to fork PRs
- ☐ pull_request_target used safely (if at all)
- ☐ Deployment workflows restricted to protected branches
- ☐ Environment protection rules configured
- ☐ Third-party actions pinned to SHA
- ☐ GITHUB_TOKEN permissions minimized
Testing & Validation
- ☐ Workflow tested on feature branch
- ☐ YAML validated with actionlint
- ☐ Event payload inspection added for debugging
- ☐ Manual trigger available (workflow_dispatch)
- ☐ Job-level if conditions guard expensive operations
Documentation
- ☐ Workflow name is descriptive
- ☐ Complex logic documented with comments
- ☐ Team trained on manual trigger process
- ☐ Trigger design reviewed by team
Visual: GitHub Actions Trigger Flow Diagram
════════════════════════════════════════════════════════════════════
GITHUB ACTIONS TRIGGER FLOW
════════════════════════════════════════════════════════════════════
┌──────────────────────────────────────────────────────────┐
│ GITHUB EVENT SOURCES │
└──────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Push │ │ PR │ │Schedule│ │ Manual │
│ Event │ │ Event │ │ (Cron) │ │Trigger │
└────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘
│ │ │ │
└────────────┴────────────┴────────────┘
│
▼
┌─────────────────────────┐
│ WORKFLOW YAML FILE │
│ .github/workflows/*.yml │
└────────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ `on:` SECTION │
│ Trigger Definition │
└────────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ FILTER EVALUATION │
│ │
│ • Branch filter │
│ • Path filter │
│ • Event type filter │
│ • Tag filter │
└────────────┬─────────────┘
│
┌──────┴──────┐
│ All Match? │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
YES NO
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ START WORKFLOW │ │ SKIP EXECUTION │
└────────┬─────────┘ └─────────────────┘
│
▼
┌──────────────────┐
│ JOB EVALUATION │
│ (if: condition) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ EXECUTE STEPS │
│ │
│ 1. Checkout │
│ 2. Setup │
│ 3. Build │
│ 4. Test │
│ 5. Deploy │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ WORKFLOW RESULT │
│ ✓ Success / ✗ Fail │
└──────────────────┘
════════════════════════════════════════════════════════════════════
Common Trigger Patterns Cheat Sheet
Pattern 1: Basic CI on Pull Requests
on:
pull_request:
branches: [main, develop]
types: [opened, synchronize, reopened]
Pattern 2: Deploy on Tag Creation
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
Pattern 3: Monorepo with Path Filters
on:
push:
branches: [main]
paths:
- 'services/api/**'
- 'shared/**'
paths-ignore:
- '**.md'
- 'docs/**'
Pattern 4: Manual Deployment with Parameters
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
version:
type: string
required: true
Pattern 5: Scheduled Maintenance
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM UTC
workflow_dispatch: # Also allow manual runs
Pattern 6: Chain Workflows (CI → CD)
on:
workflow_run:
workflows: ["CI Build"]
types: [completed]
branches: [main]
jobs:
deploy:
if: github.event.workflow_run.conclusion == 'success'
Pattern 7: Cross-Repo Trigger
on:
repository_dispatch:
types: [deploy-service]
jobs:
deploy:
steps:
- run: echo "Version: ${{ github.event.client_payload.version }}"
Pattern 8: Prevent Duplicate Runs
on:
pull_request:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Pattern 9: Only on Specific File Types
on:
pull_request:
paths:
- '**.js'
- '**.ts'
- '**.jsx'
- '**.tsx'
- 'package.json'
Pattern 10: Multiple Events with Different Configs
on:
push:
branches: [main]
paths: ['src/**']
pull_request:
branches: [main, develop]
paths: ['src/**', 'tests/**']
workflow_dispatch:
Advanced Debugging Techniques
Inspecting Trigger Context
When workflows behave unexpectedly, inspect the full event context:
name: Debug Triggers
on: [push, pull_request, workflow_dispatch]
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: Dump all contexts
run: |
echo "Event name: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Base ref: ${{ github.base_ref }}"
echo "Head ref: ${{ github.head_ref }}"
- name: Dump full GitHub context
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"
- name: Dump event payload
env:
EVENT_CONTEXT: ${{ toJSON(github.event) }}
run: echo "$EVENT_CONTEXT"
- name: Show environment variables
run: env | sort
Using GitHub CLI for Trigger Analysis
# List recent workflow runs
gh run list --workflow="CI Build" --limit 20
# View specific run details
gh run view 1234567890
# View run with event details
gh run view 1234567890 --json event,conclusion,headBranch
# Download logs for analysis
gh run download 1234567890
# Re-run failed workflows
gh run rerun 1234567890
# Watch workflow run in real-time
gh run watch 1234567890
Creating a Test Workflow for Trigger Validation
# .github/workflows/trigger-test.yml
name: Trigger Test
on:
push:
branches: ['test-triggers']
pull_request:
branches: ['test-triggers']
jobs:
test-filters:
runs-on: ubuntu-latest
steps:
- name: Test branch filter
run: |
echo "Branch: ${{ github.ref }}"
echo "Expected: refs/heads/test-triggers"
if [[ "${{ github.ref }}" == "refs/heads/test-triggers" ]]; then
echo "✓ Branch filter working"
else
echo "✗ Branch filter failed"
exit 1
fi
- name: Test event type
run: |
echo "Event: ${{ github.event_name }}"
if [[ "${{ github.event_name }}" == "push" || "${{ github.event_name }}" == "pull_request" ]]; then
echo "✓ Event filter working"
else
echo "✗ Unexpected event type"
exit 1
fi
Performance Optimization Case Study
Before Optimization: Inefficient Trigger Design
# ❌ BEFORE: Runs on every push to any branch
name: Inefficient CI
on: push
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
Problems:
- Runs on ALL branches (including personal feature branches)
- Runs even when only docs change
- No concurrency control (multiple overlapping runs)
- Separate workflows waste setup time
Monthly cost for 50 developers:
- ~3,000 workflow runs/month
- ~15,000 minutes (5 minutes per run)
- Cost: $120/month (private repos)
After Optimization: Efficient Trigger Design
# ✅ AFTER: Optimized with filters and concurrency
name: Efficient CI
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- 'tests/**'
- 'package.json'
- 'package-lock.json'
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [main, develop]
paths:
- 'src/**'
- 'tests/**'
- 'package.json'
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
quality-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- run: npm ci
# Run all checks in one job (saves setup time)
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
Improvements:
- Only runs on main/develop branches (50% fewer runs)
- Skips runs for documentation changes (20% fewer runs)
- Concurrency control cancels outdated runs (15% fewer runs)
- Combined job reduces setup overhead (40% faster)
Monthly cost after optimization:
- ~1,000 workflow runs/month (67% reduction)
- ~3,000 minutes (80% reduction)
- Cost: $24/month (80% savings = $96/month saved)
Trigger Security Deep Dive
Anatomy of a Secure Workflow Trigger
name: Secure Production Deploy
on:
push:
branches:
- main # ✓ Only protected branch
paths:
- 'src/**' # ✓ Only relevant changes
tags:
- 'v*' # ✓ Only version tags
workflow_dispatch: # ✓ Manual trigger option
inputs:
confirm:
description: 'Type "DEPLOY" to confirm'
required: true
# Limit permissions to minimum necessary
permissions:
contents: read # ✓ Read-only by default
deployments: write # ✓ Only what's needed
concurrency:
group: production-deploy
cancel-in-progress: false # ✓ Never cancel prod deploys
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Validate manual confirmation
if: github.event_name == 'workflow_dispatch'
run: |
if [[ "${{ inputs.confirm }}" != "DEPLOY" ]]; then
echo "Deployment not confirmed"
exit 1
fi
- name: Verify production branch
run: |
if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then
echo "Can only deploy from main branch"
exit 1
fi
deploy:
needs: validate
runs-on: ubuntu-latest
environment: production # ✓ Requires approval
steps:
- name: Deploy to production
env:
DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}
run: |
echo "Deploying to production..."
# Deployment logic
Unsafe Pattern: Fork PR with Secrets
# ❌ DANGEROUS: Exposes secrets to fork PRs
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# DANGER: Running potentially malicious code with secrets
- run: npm install
- run: npm test
env:
API_KEY: ${{ secrets.API_KEY }} # ❌ Secret exposed!
Attack scenario:
- Attacker forks your repo
- Modifies
package.jsonto include malicious postinstall script - Opens PR to trigger workflow
- Script exfiltrates
API_KEYto attacker’s server
Safe Pattern: Separate Validation and Privileged Operations
# ✅ SAFE: Validate first, then act with privileges
name: Safe PR Workflow
on:
pull_request: # Runs from fork, no secrets
types: [opened, synchronize]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test # ✓ No secrets, safe to run fork code
---
# Separate workflow for privileged operations
name: Add PR Label
on:
pull_request_target: # Has write access
types: [opened]
jobs:
label:
runs-on: ubuntu-latest
steps:
# ✓ NOT checking out PR code
- uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['needs-review']
})
Trigger Troubleshooting Decision Tree
Workflow didn't trigger?
│
├─> Check 1: Does workflow file exist in target branch?
│ ├─> NO → Add workflow file to branch and push
│ └─> YES → Continue
│
├─> Check 2: Is YAML syntax valid?
│ ├─> NO → Run `actionlint` to find syntax errors
│ └─> YES → Continue
│
├─> Check 3: Do branch filters match?
│ ├─> NO → Adjust `branches:` filter or push to correct branch
│ └─> YES → Continue
│
├─> Check 4: Do path filters match?
│ ├─> NO → Check if changed files match `paths:` patterns
│ └─> YES → Continue
│
├─> Check 5: Is event type included in workflow?
│ ├─> NO → Add event to `on:` section
│ └─> YES → Continue
│
├─> Check 6: For PRs, are activity types correct?
│ ├─> NO → Add required types (e.g., `[opened, synchronize]`)
│ └─> YES → Continue
│
├─> Check 7: Is workflow enabled in repo settings?
│ ├─> NO → Go to Settings → Actions → Enable workflow
│ └─> YES → Continue
│
└─> Check 8: Check Actions tab for error messages
└─> Review logs for specific error details
Trigger Patterns by Development Stage
Development Stage
name: Development CI
on:
push:
branches:
- 'feature/**'
- 'bugfix/**'
- 'hotfix/**'
pull_request:
types: [opened, synchronize]
jobs:
fast-checks:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Quick validation
run: |
npm run lint
npm run test:unit # Only unit tests, skip integration
Staging Stage
name: Staging Deploy
on:
push:
branches: [develop, staging]
workflow_dispatch:
inputs:
environment:
default: 'staging'
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging
run: echo "Deploying to staging"
- name: Run smoke tests
run: npm run test:smoke
Production Stage
name: Production Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
inputs:
version:
required: true
description: 'Version to deploy'
concurrency:
group: production
cancel-in-progress: false # Never cancel production deploys
jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- name: Deploy to production
run: echo "Deploying version ${{ github.ref_name }}"
Final Thoughts & Action Items
GitHub Actions workflow triggers are the gatekeepers of your CI/CD pipeline. Master them, and you’ll build automation that’s fast, secure, and cost-effective.
Immediate Action Items
- Audit existing workflows this week
- Identify workflows running unnecessarily
- Add path filters to high-frequency workflows
- Implement concurrency groups where appropriate
- Establish trigger standards
- Document team conventions for trigger design
- Create workflow templates with sensible defaults
- Set up pre-commit hooks to validate workflow YAML
- Monitor and optimize monthly
- Review Actions usage in billing dashboard
- Identify expensive workflows
- Continuously refine filters based on usage patterns
- Security review quarterly
- Audit workflows using
pull_request_target - Review secret exposure in fork PRs
- Validate environment protection rules
- Audit workflows using
Key Metrics to Track
- Workflow runs per day/week/month
- Average workflow duration
- Cost per workflow run
- Percentage of skipped runs (thanks to filters)
- Concurrency-cancelled runs
Continue Learning
Stay updated with GitHub Actions evolution:
- Subscribe to GitHub Changelog
- Follow @github for announcements
- Join GitHub Community Discussions
Did this guide help you master GitHub Actions workflow triggers? Share it with your team and let us know what trigger patterns work best for your infrastructure at thedevopstooling.com!
