Common Repository Events
GitHub Actions responds to events — things that happen in your repository. Every workflow starts with an on: key that specifies which events trigger execution. Understanding the full event catalog is essential for building precise, efficient CI/CD pipelines.
flowchart TD
A[Workflow Triggers] --> B[Repository Events]
A --> C[Scheduled Events]
A --> D[Manual Events]
A --> E[Cross-Workflow Events]
B --> B1[push]
B --> B2[pull_request]
B --> B3[issues]
B --> B4[release]
B --> B5[create / delete]
B --> B6[fork / watch / star]
C --> C1["schedule (cron)"]
D --> D1[workflow_dispatch]
D --> D2[repository_dispatch]
E --> E1[workflow_run]
E --> E2[workflow_call]
style A fill:#132440,color:#fff
style B fill:#3B9797,color:#fff
style C fill:#16476A,color:#fff
style D fill:#BF092F,color:#fff
style E fill:#3B9797,color:#fff
Push Events
The push event fires whenever commits are pushed to a branch or tag. This is the most common trigger for CI pipelines — run tests every time code changes.
# .github/workflows/ci-on-push.yml
name: CI on Push
on:
push:
branches:
- main
- develop
- 'release/**'
tags:
- 'v*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
The push event provides rich context through github.event: the list of commits pushed, the before/after SHAs, the pusher's identity, and whether it was a force push.
Pull Request Events
The pull_request event fires for PR lifecycle actions. By default it triggers on opened, synchronize (new commits pushed), and reopened.
# .github/workflows/pr-checks.yml
name: PR Checks
on:
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
Issues and Comments
Issue-related events enable powerful automation — auto-labeling, triage bots, stale issue management, and slash command handlers.
# .github/workflows/issue-triage.yml
name: Issue Triage
on:
issues:
types: [opened, labeled]
issue_comment:
types: [created]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Auto-label based on title
if: github.event_name == 'issues' && github.event.action == 'opened'
uses: actions/github-script@v7
with:
script: |
const title = context.payload.issue.title.toLowerCase();
const labels = [];
if (title.includes('bug')) labels.push('bug');
if (title.includes('feature')) labels.push('enhancement');
if (title.includes('docs')) labels.push('documentation');
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: labels
});
}
Release, Create, Delete, and Other Events
| Event | Fires When | Common Use Case |
|---|---|---|
push | Commits pushed to branch/tag | CI testing, deployments |
pull_request | PR opened/updated/merged | PR checks, previews |
issues | Issue created/edited/labeled | Triage automation |
issue_comment | Comment on issue or PR | Slash commands, bot replies |
release | Release published/created | Package publishing, deployment |
create | Branch or tag created | Branch setup automation |
delete | Branch or tag deleted | Cleanup resources |
fork | Repository forked | Tracking/notifications |
watch | Repository starred | Community metrics |
deployment | Deployment requested via API | Custom deploy orchestration |
discussion | Discussion created/edited | Community management |
label | Label created/edited/deleted | Repository maintenance |
# .github/workflows/publish-on-release.yml
name: Publish Package on Release
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://npm.pkg.github.com'
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Event Activity Types and Filters
Most events have multiple activity types — sub-events that describe what specifically happened. By default, workflows trigger on a subset of activity types. The types: keyword lets you be precise about which activities matter.
types:, pull_request triggers on opened, synchronize, and reopened only. If you need to react to PR reviews, labels, or assignments, you must explicitly list those activity types.
Pull Request Activity Types
# .github/workflows/pr-complete-lifecycle.yml
name: PR Lifecycle
on:
pull_request:
types:
- opened # PR created
- synchronize # New commits pushed to PR branch
- reopened # Previously closed PR reopened
- ready_for_review # Draft PR marked as ready
- labeled # Label added to PR
- unlabeled # Label removed from PR
- assigned # Reviewer assigned
- review_requested # Review requested from team/person
- closed # PR closed (merged or not)
jobs:
on-open-or-sync:
if: github.event.action == 'opened' || github.event.action == 'synchronize'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
on-ready-for-review:
if: github.event.action == 'ready_for_review'
runs-on: ubuntu-latest
steps:
- name: Notify team
run: echo "PR is ready for review - ${{ github.event.pull_request.title }}"
on-labeled-deploy:
if: github.event.action == 'labeled' && github.event.label.name == 'deploy-preview'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploying preview for PR #${{ github.event.pull_request.number }}"
Issues Activity Types
# .github/workflows/issue-automation.yml
name: Issue Automation
on:
issues:
types:
- opened # New issue created
- edited # Issue title/body changed
- deleted # Issue deleted
- transferred # Issue moved to another repo
- pinned # Issue pinned
- unpinned # Issue unpinned
- closed # Issue closed
- reopened # Issue reopened
- assigned # Assignee added
- unassigned # Assignee removed
- labeled # Label added
- unlabeled # Label removed
- milestoned # Added to milestone
jobs:
welcome-new-contributor:
if: github.event.action == 'opened'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const creator = context.payload.issue.user.login;
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
creator: creator,
state: 'all'
});
if (issues.length === 1) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Welcome @${creator}! Thanks for your first issue. A maintainer will review it soon.`
});
}
Using Event Filters (Branches, Tags, Paths)
Event filters let you narrow when a workflow runs. Instead of triggering on every push to any branch, you can target specific branches, tags, or file paths. This eliminates wasted CI minutes and reduces notification noise.
Branch Filters
# .github/workflows/branch-filtering.yml
name: Branch-Filtered CI
on:
push:
# Include patterns — workflow runs ONLY for matching branches
branches:
- main # Exact match
- develop # Exact match
- 'release/**' # Glob: release/1.0, release/2.0/rc1, etc.
- 'feature/*' # Glob: feature/login (but NOT feature/auth/oauth)
- '!feature/experimental' # Negation: exclude this specific branch
pull_request:
# branches-ignore — workflow runs for ALL branches EXCEPT these
branches-ignore:
- 'dependabot/**' # Skip Dependabot PRs
- 'docs/**' # Skip documentation branches
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Running on branch ${{ github.ref_name }}"
branches: and branches-ignore: for the same event. Choose one approach. Use branches: (allowlist) when you want to target specific branches. Use branches-ignore: (denylist) when you want to exclude a few branches from an otherwise open trigger.
Tag Filters
# .github/workflows/release-on-tag.yml
name: Release on Tag
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+' # Matches v1.0.0, v2.13.7, etc.
- 'v[0-9]+.[0-9]+.[0-9]+-rc*' # Matches v1.0.0-rc1, v2.0.0-rc.3
# tags-ignore works the same way
# tags-ignore:
# - 'v*-alpha*' # Skip alpha pre-releases
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Build release
run: |
echo "Building version ${{ steps.version.outputs.version }}"
npm ci
npm run build
Path Filters
Path filters are one of the most impactful optimizations. In a monorepo with frontend, backend, and infrastructure code, you can run frontend CI only when frontend files change.
# .github/workflows/monorepo-ci.yml
name: Monorepo CI
on:
push:
branches: [main]
paths:
- 'src/frontend/**' # Only frontend source files
- 'package.json' # Dependency changes
- 'package-lock.json'
- '.github/workflows/monorepo-ci.yml' # This workflow file itself
pull_request:
branches: [main]
paths-ignore:
- '**.md' # Skip documentation-only changes
- 'docs/**' # Skip docs folder
- '.github/ISSUE_TEMPLATE/**'
- 'LICENSE'
- '.gitignore'
jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd src/frontend && npm ci && npm test
ci-frontend.yml, ci-backend.yml, ci-infra.yml — each with targeted path filters. This gives maximum parallelism while ensuring each service only rebuilds when its files change.
Combining Filters
When you combine branch and path filters, both must match for the workflow to trigger. This is an AND relationship.
# Only triggers when:
# 1. Push is to main or develop AND
# 2. Files in src/ or tests/ are changed
on:
push:
branches: [main, develop]
paths: ['src/**', 'tests/**']
Pull Request Events and Security Considerations
Pull requests from forks present a fundamental security challenge. A fork contributor can modify the workflow file in their PR, potentially exfiltrating secrets or running malicious code. GitHub's security model handles this through careful permission boundaries.
flowchart TD
A[PR from Fork] --> B{Which event?}
B -->|pull_request| C[Runs in fork context]
B -->|pull_request_target| D[Runs in base repo context]
C --> C1[Read-only GITHUB_TOKEN]
C --> C2[NO access to secrets]
C --> C3[Cannot write to base repo]
C --> C4[Safe by default]
D --> D1[Full GITHUB_TOKEN permissions]
D --> D2[Access to ALL secrets]
D --> D3[Can write to base repo]
D --> D4[DANGEROUS if checkout PR code]
style A fill:#132440,color:#fff
style C fill:#3B9797,color:#fff
style D fill:#BF092F,color:#fff
style C4 fill:#3B9797,color:#fff
style D4 fill:#BF092F,color:#fff
pull_request_target event runs in the context of the base repository with full access to secrets. If you checkout the PR's head code (ref: ${{ github.event.pull_request.head.sha }}) and then execute it, an attacker can exfiltrate all your repository secrets. This is one of the most common GitHub Actions security vulnerabilities.
Safe Pattern: pull_request (Default)
# .github/workflows/pr-safe.yml — Safe for fork PRs
name: PR Checks (Safe)
# pull_request runs in fork context — no secrets available
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Checks out the merge commit (safe)
- run: npm ci
- run: npm test
# This works fine — no secrets needed for basic CI
Dangerous Pattern: pull_request_target
# .github/workflows/pr-label.yml — Safe use of pull_request_target
name: Label PR (Safe)
# pull_request_target runs in BASE repo context — has secrets
on:
pull_request_target:
types: [opened]
jobs:
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
# SAFE: We do NOT checkout PR code — only use event metadata
- name: Add size label
uses: actions/github-script@v7
with:
script: |
const { additions, deletions } = context.payload.pull_request;
const size = additions + deletions;
let label = 'size/S';
if (size > 100) label = 'size/M';
if (size > 500) label = 'size/L';
if (size > 1000) label = 'size/XL';
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [label]
});
# .github/workflows/pr-dangerous.yml — DANGEROUS anti-pattern
name: PR Build (DANGEROUS - DO NOT COPY)
on:
pull_request_target:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
# DANGEROUS: Checking out PR code in base context
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # Attacker's code!
# DANGEROUS: Running attacker's code with access to secrets
- run: npm ci # package.json could have malicious postinstall scripts
- run: npm test # Tests could exfiltrate ${{ secrets.DEPLOY_KEY }}
env:
API_KEY: ${{ secrets.API_KEY }} # Directly exposed to attacker
Safe Pattern When You Need Both
# .github/workflows/pr-comment-results.yml
# Pattern: Run tests in safe context, then report in privileged context
name: PR Test and Report
on:
# Safe: tests run without secrets
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/lcov.info
---
# Separate workflow triggered after the above completes
# .github/workflows/pr-report.yml
name: Report Coverage
on:
workflow_run:
workflows: ["PR Test and Report"]
types: [completed]
jobs:
report:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/download-artifact@v4
with:
name: coverage-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Comment coverage on PR
uses: actions/github-script@v7
with:
script: |
// Post coverage results as PR comment (has write access)
const fs = require('fs');
const coverage = fs.readFileSync('coverage/lcov.info', 'utf8');
// Parse and comment...
Manual Triggers (workflow_dispatch)
The workflow_dispatch event lets you trigger workflows manually from the GitHub UI, the REST API, or the gh CLI. This is essential for deployment workflows, ad-hoc tasks, and operations that shouldn't run automatically.
# .github/workflows/deploy.yml
name: Deploy Application
on:
workflow_dispatch:
inputs:
environment:
description: 'Target deployment environment'
required: true
type: choice
options:
- development
- staging
- production
default: staging
version:
description: 'Version to deploy (e.g., 1.2.3 or latest)'
required: true
type: string
default: 'latest'
dry_run:
description: 'Perform a dry run without actual deployment'
required: false
type: boolean
default: false
log_level:
description: 'Logging verbosity'
required: false
type: choice
options:
- info
- debug
- trace
default: info
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.version == 'latest' && 'main' || format('v{0}', inputs.version) }}
- name: Validate inputs
run: |
echo "Environment: ${{ inputs.environment }}"
echo "Version: ${{ inputs.version }}"
echo "Dry Run: ${{ inputs.dry_run }}"
echo "Log Level: ${{ inputs.log_level }}"
- name: Deploy
if: inputs.dry_run == false
run: |
./scripts/deploy.sh \
--env "${{ inputs.environment }}" \
--version "${{ inputs.version }}" \
--log-level "${{ inputs.log_level }}"
- name: Dry run
if: inputs.dry_run == true
run: |
echo "DRY RUN — would deploy ${{ inputs.version }} to ${{ inputs.environment }}"
./scripts/deploy.sh --env "${{ inputs.environment }}" --version "${{ inputs.version }}" --dry-run
string— Free-text input fieldchoice— Dropdown with predefined optionsboolean— Checkbox (true/false)environment— Dropdown of configured GitHub Environments
${{ inputs.name }} in the workflow. They're always strings at runtime (even booleans become "true"/"false").
Triggering via CLI and API
# Trigger via GitHub CLI
gh workflow run deploy.yml \
-f environment=staging \
-f version=1.2.3 \
-f dry_run=false \
-f log_level=debug
# Trigger via REST API
curl -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/OWNER/REPO/actions/workflows/deploy.yml/dispatches" \
-d '{
"ref": "main",
"inputs": {
"environment": "production",
"version": "2.0.0",
"dry_run": "false",
"log_level": "info"
}
}'
Scheduled Workflows (cron)
The schedule event uses POSIX cron syntax to run workflows at specified times. This is ideal for nightly builds, periodic dependency updates, health checks, and report generation.
# .github/workflows/nightly.yml
name: Nightly Tasks
on:
schedule:
# ┌───────────── minute (0-59)
# │ ┌───────────── hour (0-23)
# │ │ ┌───────────── day of month (1-31)
# │ │ │ ┌───────────── month (1-12)
# │ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
# │ │ │ │ │
- cron: '0 6 * * 1-5' # 6:00 AM UTC, Monday through Friday
- cron: '0 0 * * 0' # Midnight UTC every Sunday (weekly)
jobs:
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for outdated dependencies
run: |
npm outdated --json > outdated.json || true
if [ -s outdated.json ]; then
echo "::warning::Outdated dependencies found"
cat outdated.json
fi
stale-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
days-before-stale: 30
days-before-close: 7
stale-issue-label: 'stale'
stale-pr-label: 'stale'
stale-issue-message: 'This issue has been inactive for 30 days and will be closed in 7 days unless there is new activity.'
Cron Syntax Quick Reference
| Expression | Schedule | Use Case |
|---|---|---|
0 * * * * | Every hour | Health checks |
0 6 * * * | Daily at 6 AM UTC | Nightly builds |
0 6 * * 1-5 | Weekdays at 6 AM UTC | Business-hours CI |
0 0 * * 0 | Weekly (Sunday midnight) | Dependency audits |
0 0 1 * * | Monthly (1st at midnight) | License checks |
*/15 * * * * | Every 15 minutes | Monitoring (use sparingly) |
0 9 * * 1 | Monday at 9 AM UTC | Weekly reports |
- All times are UTC — There's no timezone setting. Convert your local time to UTC.
- Not guaranteed exact — During high-load periods, scheduled workflows may be delayed by up to 15-30 minutes.
- Minimum interval: 5 minutes — GitHub ignores cron expressions that run more frequently.
- Disabled after 60 days of inactivity — If no repo activity occurs for 60 days, scheduled workflows are automatically disabled. A push or manual trigger re-enables them.
- Runs on default branch only — Scheduled workflows always use the version from the default branch (usually
main).
Repository Dispatch and External Triggers
The repository_dispatch event enables external systems to trigger workflows via the GitHub REST API. This bridges GitHub Actions with external tools — monitoring systems, CMS webhooks, custom dashboards, or other CI/CD platforms.
# .github/workflows/external-trigger.yml
name: External Deployment Trigger
on:
repository_dispatch:
types:
- deploy-request
- rollback-request
- cache-invalidation
jobs:
handle-deploy:
if: github.event.action == 'deploy-request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy with payload data
run: |
echo "Deploying version: ${{ github.event.client_payload.version }}"
echo "Requested by: ${{ github.event.client_payload.requested_by }}"
echo "Target: ${{ github.event.client_payload.environment }}"
./scripts/deploy.sh \
--version "${{ github.event.client_payload.version }}" \
--env "${{ github.event.client_payload.environment }}"
handle-rollback:
if: github.event.action == 'rollback-request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Rollback
run: |
echo "Rolling back to: ${{ github.event.client_payload.target_version }}"
echo "Reason: ${{ github.event.client_payload.reason }}"
Triggering Repository Dispatch from External Systems
# Trigger deployment via API
curl -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/OWNER/REPO/dispatches" \
-d '{
"event_type": "deploy-request",
"client_payload": {
"version": "2.1.0",
"environment": "production",
"requested_by": "monitoring-bot",
"timestamp": "2026-06-02T10:30:00Z",
"commit_sha": "abc123def456"
}
}'
// Trigger from a Node.js application (e.g., Slack bot, monitoring service)
const response = await fetch(
'https://api.github.com/repos/OWNER/REPO/dispatches',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_type: 'deploy-request',
client_payload: {
version: '2.1.0',
environment: 'staging',
requested_by: 'slack-bot',
channel: '#deployments'
}
})
}
);
if (response.status === 204) {
console.log('Dispatch event sent successfully');
}
- ChatOps — Trigger deployments from Slack/Teams commands
- External monitoring — Auto-rollback when an alert fires
- Cross-repo orchestration — One repo's release triggers another repo's deploy
- CMS webhooks — Rebuild static site when content changes in headless CMS
- Scheduled from external scheduler — More control than built-in cron
Workflow Run Approval for Forked Pull Requests
For public repositories, GitHub requires manual approval before running workflows on PRs from first-time contributors. This prevents abuse of your CI minutes and protects against crypto-mining attacks.
Approval Settings
Navigate to Settings → Actions → General → Fork pull request workflows to configure:
| Setting | Who Needs Approval | Risk Level |
|---|---|---|
| Require approval for first-time contributors | Only new contributors who haven't had PRs merged | Low (recommended for active projects) |
| Require approval for first-time contributors who are new to GitHub | Only accounts created recently | Lowest (most permissive) |
| Require approval for all outside collaborators | Everyone without write access | Highest (most secure, most friction) |
When approval is required, maintainers see a yellow "Approve and run" button on the PR's checks section. The workflow remains in a "Waiting for approval" state until a maintainer approves it.
Triggering Workflows from Other Workflows
GitHub Actions provides two mechanisms for workflow chaining: workflow_run (event-based, loosely coupled) and workflow_call (direct invocation, tightly coupled like a function call).
workflow_run — Event-Based Chaining
The workflow_run event triggers when another workflow completes, succeeds, or is requested. This enables sequential workflows without tight coupling.
# .github/workflows/deploy-after-ci.yml
name: Deploy After CI
on:
workflow_run:
workflows: ["CI Pipeline"] # Name of the triggering workflow
types: [completed] # completed, requested
branches: [main] # Optional: filter by branch
jobs:
deploy:
# Only deploy if CI succeeded
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "CI passed on main — deploying"
echo "Triggered by run: ${{ github.event.workflow_run.id }}"
echo "Head SHA: ${{ github.event.workflow_run.head_sha }}"
./scripts/deploy.sh --env production
notify-failure:
if: github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
steps:
- name: Notify team of CI failure
run: |
echo "::error::CI Pipeline failed on main"
# Send Slack notification, create issue, etc.
workflow_call — Reusable Workflows
The workflow_call event turns a workflow into a reusable component that other workflows can invoke directly — like calling a function. The caller passes inputs and receives outputs.
# .github/workflows/reusable-deploy.yml — The reusable workflow
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
version:
required: true
type: string
notify:
required: false
type: boolean
default: true
secrets:
DEPLOY_KEY:
required: true
SLACK_WEBHOOK:
required: false
outputs:
deploy_url:
description: "The URL of the deployed application"
value: ${{ jobs.deploy.outputs.url }}
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
outputs:
url: ${{ steps.deploy.outputs.url }}
steps:
- uses: actions/checkout@v4
- name: Deploy
id: deploy
run: |
echo "Deploying ${{ inputs.version }} to ${{ inputs.environment }}"
url="https://${{ inputs.environment }}.example.com"
echo "url=$url" >> "$GITHUB_OUTPUT"
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
- name: Notify
if: inputs.notify
run: |
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-d '{"text":"Deployed ${{ inputs.version }} to ${{ inputs.environment }}"}'
# .github/workflows/release-pipeline.yml — The caller workflow
name: Release Pipeline
on:
push:
tags: ['v*']
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
deploy-staging:
needs: test
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
version: ${{ github.ref_name }}
notify: true
secrets:
DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
version: ${{ github.ref_name }}
notify: true
secrets:
DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
flowchart LR
subgraph "workflow_run (Loose Coupling)"
A1[CI Workflow] -->|completes| B1[Deploy Workflow]
A1 -->|completes| C1[Notify Workflow]
end
subgraph "workflow_call (Tight Coupling)"
A2[Release Pipeline] -->|calls| B2[Reusable: Test]
A2 -->|calls| C2[Reusable: Deploy]
A2 -->|calls| D2[Reusable: Notify]
end
style A1 fill:#3B9797,color:#fff
style A2 fill:#3B9797,color:#fff
style B1 fill:#16476A,color:#fff
style C1 fill:#16476A,color:#fff
style B2 fill:#16476A,color:#fff
style C2 fill:#16476A,color:#fff
style D2 fill:#16476A,color:#fff
- workflow_run — When workflows are independent and you want to chain on completion (e.g., deploy after CI passes, comment on PR after tests finish)
- workflow_call — When you want to reuse workflow logic like a function (e.g., standardized deploy process used by 10 repos, shared test matrix)
Exercises
Create a repository with frontend/ and backend/ directories. Write two separate workflow files — one for each service — using paths: filters so that changing frontend/src/app.js only triggers the frontend CI, and changing backend/server.py only triggers the backend CI. Verify by making isolated commits to each directory.
Build a scheduled workflow that runs every Monday at 9 AM UTC. It should: (1) run npm audit and save the output, (2) check for outdated dependencies with npm outdated, and (3) create a GitHub Issue if any high/critical vulnerabilities are found. Use actions/github-script to create the issue programmatically.
Create a workflow_dispatch workflow with inputs for environment (choice: staging/production) and version (string). Configure a GitHub Environment called "production" with required reviewers. The workflow should deploy to staging immediately but require manual approval before deploying to production. Trigger it from the gh CLI.
Set up two repositories. In Repo A, create a workflow that on successful completion of tests, sends a repository_dispatch event to Repo B with the commit SHA and version in the client_payload. In Repo B, create a workflow that listens for that dispatch event and uses the payload data to deploy. Test the full chain by pushing to Repo A and verifying Repo B's workflow triggers.
Next in the Bootcamp
In Module 3: Jobs, Steps, and Workflow Structure, we'll master job dependencies, conditional execution, matrix strategies, reusable steps, and how to structure complex multi-job workflows for maximum parallelism and clarity.