Comprehensive toolkit for generating best practice GitHub Actions workflows, custom local actions, and configurations following current standards and conventions. Use this skill when creating new GitHub Actions resources, implementing CI/CD workflows, or building reusable actions.
Overall
score
100%
Does it follow best practices?
Validation for skill structure
Last Updated: November 2025
This guide covers advanced trigger patterns for GitHub Actions workflows beyond the basic push, pull_request, and schedule triggers. These patterns enable workflow orchestration, external integrations, ChatOps, and complex automation scenarios.
The workflow_run trigger allows you to chain workflows together, running one workflow after another completes. This is the recommended pattern for handling external pull requests securely.
name: Deploy Application
on:
workflow_run:
workflows: ["CI Pipeline"]
types: [completed]
branches: [main, staging]requested - Workflow run was requestedin_progress - Workflow run is currently runningcompleted - Workflow run has finished (success, failure, or cancelled)# deploy.yml - Separate deployment workflow
name: Deploy to Production
on:
workflow_run:
workflows: ["CI Pipeline"]
types: [completed]
branches: [main]
jobs:
deploy:
# Only deploy if CI passed
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download build artifacts from CI
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: build-artifacts
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy application
run: |
echo "Deploying commit ${{ github.event.workflow_run.head_sha }}"
# Deployment commands here# security-scan.yml - Runs after CI for external PRs
name: Security Scan
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
security-events: write
contents: read
jobs:
scan:
# Only scan if CI passed and it was a PR
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout PR code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Run security scan
run: |
# Security scanning without exposing secrets to PR
npm audit --audit-level=highsteps:
- name: Get workflow run details
run: |
echo "Workflow: ${{ github.event.workflow_run.name }}"
echo "Conclusion: ${{ github.event.workflow_run.conclusion }}"
echo "Head SHA: ${{ github.event.workflow_run.head_sha }}"
echo "Head Branch: ${{ github.event.workflow_run.head_branch }}"
echo "Run ID: ${{ github.event.workflow_run.id }}"
echo "Event: ${{ github.event.workflow_run.event }}"✅ Safer than pull_request_target for external PRs:
The repository_dispatch trigger allows external systems to trigger workflows via the GitHub API. This enables integration with webhooks, custom dashboards, monitoring systems, and other external tools.
name: Handle External Event
on:
repository_dispatch:
types: [deploy-prod, deploy-staging, run-migration, rebuild-cache]Event types are custom strings you define. Common patterns:
deploy-<environment> - Deployment triggersrun-<task> - Task executionnotify-<event> - Notification handlingUsing curl:
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/OWNER/REPO/dispatches \
-d '{
"event_type": "deploy-prod",
"client_payload": {
"version": "v1.2.3",
"requestor": "monitoring-system",
"environment": "production",
"rollback": false
}
}'Using Python:
import requests
def trigger_deployment(repo, token, version, environment):
url = f"https://api.github.com/repos/{repo}/dispatches"
headers = {
"Authorization": f"token {token}",
"Accept": "application/vnd.github.v3+json"
}
payload = {
"event_type": f"deploy-{environment}",
"client_payload": {
"version": version,
"requestor": "api",
"environment": environment
}
}
response = requests.post(url, json=payload, headers=headers)
return response.status_code == 204Using Node.js (Octokit):
const { Octokit } = require("@octokit/rest");
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
await octokit.repos.createDispatchEvent({
owner: "OWNER",
repo: "REPO",
event_type: "deploy-prod",
client_payload: {
version: "v1.2.3",
requestor: "api",
environment: "production"
}
});name: External Deployment Trigger
on:
repository_dispatch:
types: [deploy-prod, deploy-staging, deploy-dev]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Parse dispatch payload
id: payload
run: |
echo "Event Type: ${{ github.event.action }}"
echo "Version: ${{ github.event.client_payload.version }}"
echo "Environment: ${{ github.event.client_payload.environment }}"
echo "Requestor: ${{ github.event.client_payload.requestor }}"
# Set outputs for later steps
echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT
echo "environment=${{ github.event.client_payload.environment }}" >> $GITHUB_OUTPUT
- name: Checkout specific version
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ steps.payload.outputs.version }}
- name: Deploy to environment
env:
ENVIRONMENT: ${{ steps.payload.outputs.environment }}
VERSION: ${{ steps.payload.outputs.version }}
run: |
echo "Deploying $VERSION to $ENVIRONMENT"
# Deployment logic hereTrigger workflows from external monitoring/alerting systems:
on:
repository_dispatch:
types: [incident-detected, performance-degradation]
jobs:
handle-alert:
runs-on: ubuntu-latest
steps:
- name: Process alert
run: |
SEVERITY="${{ github.event.client_payload.severity }}"
MESSAGE="${{ github.event.client_payload.message }}"
echo "Alert received: $MESSAGE (Severity: $SEVERITY)"
if [[ "$SEVERITY" == "critical" ]]; then
# Trigger emergency procedures
echo "Initiating critical incident response"
fiCustom deployment dashboard that triggers GitHub Actions:
on:
repository_dispatch:
types: [dashboard-deploy]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: ${{ github.event.client_payload.environment }}
steps:
- name: Validate payload
run: |
# Validate required fields
if [[ -z "${{ github.event.client_payload.version }}" ]]; then
echo "Error: version is required"
exit 1
fi
if [[ -z "${{ github.event.client_payload.approver }}" ]]; then
echo "Error: approver is required"
exit 1
fi
- name: Deploy
run: |
echo "Deploying version ${{ github.event.client_payload.version }}"
echo "Approved by: ${{ github.event.client_payload.approver }}"Trigger workflow in repo A from repo B:
# In Repository A
on:
repository_dispatch:
types: [dependency-updated]
jobs:
rebuild:
runs-on: ubuntu-latest
steps:
- name: Rebuild with new dependency
run: |
echo "Dependency ${{ github.event.client_payload.dependency }} updated to ${{ github.event.client_payload.version }}"
# Rebuild logic🔒 Token Security:
repo (for private repos) or public_repo (for public repos)🔒 Payload Validation:
client_payload fields- name: Validate environment
run: |
ENV="${{ github.event.client_payload.environment }}"
# Only allow specific environments
if [[ ! "$ENV" =~ ^(dev|staging|production)$ ]]; then
echo "Error: Invalid environment: $ENV"
exit 1
fiThe issue_comment trigger allows you to implement ChatOps - executing workflows via commands in issue or PR comments.
name: ChatOps Commands
on:
issue_comment:
types: [created, edited]created - New comment postededited - Comment was editeddeleted - Comment was deleted (rarely used)Full ChatOps Example:
name: ChatOps - Deploy Command
on:
issue_comment:
types: [created]
jobs:
deploy:
# Security checks (CRITICAL!)
if: |
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/deploy') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
deployments: write
steps:
# Step 1: React to comment to show command received
- name: Add reaction to comment
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'rocket'
});
# Step 2: Parse command and arguments
- name: Parse deploy command
id: parse
run: |
COMMAND="${{ github.event.comment.body }}"
# Extract environment (default: staging)
ENV=$(echo "$COMMAND" | grep -oP '/deploy\s+\K\w+' || echo 'staging')
# Validate environment
if [[ ! "$ENV" =~ ^(dev|staging|production)$ ]]; then
echo "error=Invalid environment: $ENV" >> $GITHUB_OUTPUT
exit 1
fi
echo "environment=$ENV" >> $GITHUB_OUTPUT
echo "pr_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
# Step 3: Get PR details
- name: Get PR branch
id: pr
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ steps.parse.outputs.pr_number }}
});
core.setOutput('ref', pr.data.head.ref);
core.setOutput('sha', pr.data.head.sha);
core.setOutput('repo', pr.data.head.repo.full_name);
# Step 4: Checkout PR code
- name: Checkout PR code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ steps.pr.outputs.ref }}
repository: ${{ steps.pr.outputs.repo }}
# Step 5: Deploy
- name: Deploy to environment
id: deploy
env:
ENVIRONMENT: ${{ steps.parse.outputs.environment }}
PR_SHA: ${{ steps.pr.outputs.sha }}
run: |
echo "Deploying PR #${{ steps.parse.outputs.pr_number }} to $ENVIRONMENT"
echo "SHA: $PR_SHA"
# Deployment logic here
DEPLOY_URL="https://$ENVIRONMENT.example.com"
echo "url=$DEPLOY_URL" >> $GITHUB_OUTPUT
# Step 6: Comment with results
- name: Comment deployment result
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const environment = '${{ steps.parse.outputs.environment }}';
const url = '${{ steps.deploy.outputs.url }}';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ steps.parse.outputs.pr_number }},
body: `✅ Deployed to **${environment}**\n\n🔗 ${url}\n\nTriggered by: @${{ github.event.comment.user.login }}`
});
# Step 7: Handle failures
- name: Comment on failure
if: failure()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: ${{ github.event.issue.number }},
body: `❌ Deployment failed\n\nCheck the [workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.`
});1. /deploy [environment]
startsWith(github.event.comment.body, '/deploy')2. /run-tests [suite]
startsWith(github.event.comment.body, '/run-tests')contains(github.event.comment.body, '/benchmark')github.event.comment.body == '/approve'Author Association Levels:
OWNER - Repository ownerMEMBER - Organization memberCOLLABORATOR - Repository collaboratorCONTRIBUTOR - Has contributed to repoFIRST_TIME_CONTRIBUTOR - First contributionFIRST_TIMER - First time interactingNONE - No associationCheck permissions:
# Only owners and members
if: contains(fromJSON('["OWNER", "MEMBER"]'), github.event.comment.author_association)
# More permissive
if: contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR", "CONTRIBUTOR"]'), github.event.comment.author_association)Advanced permission check with team membership:
steps:
- name: Check team membership
uses: actions/github-script@v7
with:
script: |
const teams = ['deployment-team', 'admin-team'];
const user = context.payload.comment.user.login;
let authorized = false;
for (const team of teams) {
try {
await github.rest.teams.getMembershipForUserInOrg({
org: context.repo.owner,
team_slug: team,
username: user
});
authorized = true;
break;
} catch (error) {
// User not in this team
}
}
if (!authorized) {
core.setFailed(`User ${user} not authorized`);
}🔒 Always validate:
github.event.issue.pull_requestgithub.event.comment.author_association🔒 Never:
🔒 Use environment variables:
# BAD - Command injection risk
- run: echo ${{ github.event.comment.body }}
# GOOD - Safe
- env:
COMMENT: ${{ github.event.comment.body }}
run: echo "$COMMENT"These triggers integrate with GitHub's deployment API.
name: Handle Deployment
on:
deployment:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Get deployment info
run: |
echo "Environment: ${{ github.event.deployment.environment }}"
echo "Ref: ${{ github.event.deployment.ref }}"
echo "Task: ${{ github.event.deployment.task }}"
echo "Payload: ${{ toJSON(github.event.deployment.payload) }}"name: Post-Deployment Actions
on:
deployment_status:
jobs:
notify:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- name: Send notification
run: |
echo "Deployment to ${{ github.event.deployment.environment }} succeeded"
# Send Slack/email notificationon:
push:
paths:
# Include specific paths
- 'src/**'
- 'lib/**/*.js'
# Exclude paths (ignore)
- '!src/**/*.md'
- '!src/**/*.test.js'
- '!**/__tests__/**'
# Only specific file types
- '**.py'
- '**.yaml'
- '**.yml'on:
pull_request:
paths:
- 'backend/**'
push:
branches: [main]
paths:
- 'backend/**'on:
pull_request:
paths:
- 'packages/frontend/**'
- 'packages/shared/**'
- '!packages/**/README.md'
- '!packages/**/*.test.*'| Trigger | Context | Secrets | Use Case | Risk Level |
|---|---|---|---|---|
pull_request | PR branch | ❌ No access | Standard PR validation | ✅ Safe |
pull_request_target | Target branch | ✅ Full access | Write to PR from fork | ⚠️ High risk |
workflow_run | Target branch | ✅ Full access | Post-CI for external PRs | ✅ Safe (if used correctly) |
on:
pull_request:
branches: [main]
# Safe: No secrets exposed, runs PR code in isolationon:
workflow_run:
workflows: ["CI"]
types: [completed]
# Safe: Runs after CI, has secrets, but uses target branch codeon:
pull_request_target:
branches: [main]
# DANGEROUS: External PRs can access secrets!
# Only use if you explicitly checkout target branch codeIf you must use pull_request_target:
on:
pull_request_target:
jobs:
comment:
runs-on: ubuntu-latest
steps:
# SAFE: Don't checkout PR code
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Thanks for your contribution!'
});
# UNSAFE: Never do this!
# - uses: actions/checkout@v4
# with:
# ref: ${{ github.event.pull_request.head.sha }}on:
check_run:
types: [created, rerequested, completed]
on:
check_suite:
types: [completed, requested]on:
status:
jobs:
handle-status:
runs-on: ubuntu-latest
steps:
- name: Check status
run: |
echo "State: ${{ github.event.state }}"
echo "Context: ${{ github.event.context }}"on:
package:
types: [published, updated]| Scenario | Recommended Trigger |
|---|---|
| Standard PR validation | pull_request |
| External PR with secrets | workflow_run after pull_request |
| Deploy after CI | workflow_run |
| Manual dashboard trigger | repository_dispatch |
| ChatOps commands | issue_comment |
| Scheduled cleanup | schedule |
| External webhook | repository_dispatch |
workflow_run instead of pull_request_target when possibleworkflow_run to separate slow jobs from fast CIpaths to avoid unnecessary runsconcurrency to cancel outdated runsCheck trigger details:
- name: Debug trigger info
run: |
echo "Event name: ${{ github.event_name }}"
echo "Event: ${{ toJSON(github.event) }}"Test repository_dispatch locally:
# Set token
export GITHUB_TOKEN="your_token"
# Trigger workflow
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/OWNER/REPO/dispatches \
-d '{"event_type":"test","client_payload":{"debug":true}}'See the assets/examples/triggers/ directory for complete working examples:
workflow-orchestration.yml - CI → Deploy workflow chainingrepository-dispatch.yml - External API triggerschatops-commands.yml - Full ChatOps implementation