| name | github-workflows |
| description | Write and optimize GitHub Actions workflows. Use when creating CI/CD pipelines, configuring workflow triggers, managing artifacts, or debugging workflow runs. |
GitHub Workflows
Activate when creating, modifying, debugging, or optimizing GitHub Actions workflow files. This skill covers workflow syntax, structure, best practices, and common CI/CD patterns.
When to Use This Skill
Activate when:
- Writing .github/workflows/*.yml files
- Configuring workflow triggers and events
- Defining jobs, steps, and dependencies
- Using expressions and contexts
- Managing secrets and environment variables
- Implementing CI/CD pipelines
- Optimizing workflow performance
- Debugging workflow failures
Workflow File Structure
Basic Anatomy
name: CI
on:
push:
branches: [main, develop]
pull_request:
env:
NODE_VERSION: '20'
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
run: npm test
File Location
Workflows must be in .github/workflows/ directory:
.github/
└── workflows/
├── ci.yml
├── deploy.yml
└── release.yml
Trigger Events (on:)
Push Events
on:
push:
branches:
- main
- 'release/**'
tags:
- 'v*'
paths:
- 'src/**'
- '!docs/**'
Pull Request Events
on:
pull_request:
types:
- opened
- synchronize
- reopened
branches:
- main
paths-ignore:
- '**.md'
Schedule (Cron)
on:
schedule:
- cron: '0 2 * * *'
- cron: '0 9 * * 1'
Manual Trigger (workflow_dispatch)
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
type: choice
options:
- development
- staging
- production
debug:
description: 'Enable debug logging'
required: false
type: boolean
default: false
Multiple Events
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
Jobs
Basic Job Configuration
jobs:
build:
name: Build Application
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
Runner Selection
jobs:
test:
runs-on: ubuntu-latest
test-macos:
runs-on: macos-latest
test-windows:
runs-on: windows-latest
test-specific:
runs-on: ubuntu-22.04
Matrix Strategy
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 21]
exclude:
- os: macos-latest
node: 18
fail-fast: false
max-parallel: 4
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
Job Dependencies
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps:
- run: npm run deploy
Conditional Execution
jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- run: npm run deploy
notify:
if: failure()
needs: [build, test]
runs-on: ubuntu-latest
steps:
- run: echo "Build failed"
Steps
Using Actions
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
Running Commands
steps:
- name: Single command
run: npm install
- name: Multi-line script
run: |
echo "Installing dependencies"
npm ci
npm run build
- name: Shell selection
shell: bash
run: echo "Using bash"
Conditional Steps
steps:
- name: Run on main branch only
if: github.ref == 'refs/heads/main'
run: npm run deploy
- name: Run on PR only
if: github.event_name == 'pull_request'
run: npm run test:pr
Continue on Error
steps:
- name: Lint (optional)
continue-on-error: true
run: npm run lint
- name: Test (required)
run: npm test
Environment Variables and Secrets
Global Variables
env:
NODE_ENV: production
API_URL: https://api.example.com
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo $NODE_ENV
Job-Level Variables
jobs:
build:
env:
BUILD_TYPE: release
steps:
- run: echo $BUILD_TYPE
Step-Level Variables
steps:
- name: Configure
env:
CONFIG_PATH: ./config.json
run: cat $CONFIG_PATH
Using Secrets
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: ./deploy.sh
Setting Variables Between Steps
steps:
- name: Set version
id: version
run: echo "VERSION=$(cat version.txt)" >> $GITHUB_OUTPUT
- name: Use version
run: echo "Version is ${{ steps.version.outputs.VERSION }}"
Contexts
github Context
steps:
- name: Context information
run: |
echo "Repository: ${{ github.repository }}"
echo "Branch: ${{ github.ref_name }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Event: ${{ github.event_name }}"
echo "Run ID: ${{ github.run_id }}"
env Context
env:
MY_VAR: value
steps:
- run: echo "${{ env.MY_VAR }}"
job Context
steps:
- name: Job status
if: job.status == 'success'
run: echo "Job succeeded"
steps Context
steps:
- id: first-step
run: echo "output=hello" >> $GITHUB_OUTPUT
- run: echo "${{ steps.first-step.outputs.output }}"
runner Context
steps:
- run: |
echo "OS: ${{ runner.os }}"
echo "Arch: ${{ runner.arch }}"
echo "Temp: ${{ runner.temp }}"
matrix Context
strategy:
matrix:
version: [18, 20]
steps:
- run: echo "Node ${{ matrix.version }}"
Expressions
Operators
steps:
- if: github.ref == 'refs/heads/main'
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- if: github.event_name == 'pull_request' || github.event_name == 'push'
- if: "!cancelled()"
- if: contains(github.event.head_commit.message, '[skip ci]')
- if: startsWith(github.ref, 'refs/tags/v')
- if: endsWith(github.ref, '-beta')
Functions
steps:
- if: success()
- if: failure()
- if: always()
- if: cancelled()
- run: echo "${{ format('Hello {0}', github.actor) }}"
- if: contains(github.event.pull_request.labels.*.name, 'deploy')
- run: echo '${{ toJSON(github.event) }}'
- run: echo '${{ fromJSON(env.CONFIG).database.host }}'
- run: echo "${{ hashFiles('**/package-lock.json') }}"
Artifacts
Upload Artifacts
steps:
- name: Build
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-files
path: |
dist/
build/
retention-days: 7
if-no-files-found: error
Download Artifacts
jobs:
build:
steps:
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
test:
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: npm test
Caching
npm Cache
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
Manual Cache
steps:
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Permissions
Repository Token Permissions
permissions:
contents: read
pull-requests: write
issues: write
checks: write
statuses: write
deployments: write
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Job-Level Permissions
jobs:
build:
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
Concurrency
Prevent Concurrent Runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
Job-Level Concurrency
jobs:
deploy:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
steps:
- run: ./deploy.sh
Reusable Workflows
Define Reusable Workflow
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
required: true
type: string
coverage:
required: false
type: boolean
default: false
outputs:
test-result:
description: "Test execution result"
value: ${{ jobs.test.outputs.result }}
secrets:
token:
required: true
jobs:
test:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.test.outputs.result }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm test
id: test
Call Reusable Workflow
jobs:
test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: '20'
coverage: true
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
Common CI/CD Patterns
Node.js CI
name: Node.js CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 21]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
Docker Build and Push
name: Docker
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Deploy on Release
name: Deploy
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to production
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: ./deploy.sh
Monorepo with Path Filtering
name: Monorepo CI
on:
pull_request:
paths:
- 'packages/**'
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
backend: ${{ steps.filter.outputs.backend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
frontend:
- 'packages/frontend/**'
backend:
- 'packages/backend/**'
test-frontend:
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- run: npm test --workspace=frontend
test-backend:
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- run: npm test --workspace=backend
Debugging Workflows
Replay a workflow locally with act before pushing — see /github:act for local run configuration and troubleshooting.
Enable Debug Logging
Set repository secrets:
ACTIONS_RUNNER_DEBUG: true
ACTIONS_STEP_DEBUG: true
Debug Steps
steps:
- name: Debug context
run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
- name: Dump GitHub context
run: echo '${{ toJSON(github) }}'
- name: Dump runner context
run: echo '${{ toJSON(runner) }}'
Tmate Debugging
steps:
- name: Setup tmate session
if: failure()
uses: mxschmitt/action-tmate@v3
timeout-minutes: 30
Performance Optimization
Use Caching
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
Optimize Checkout
- uses: actions/checkout@v4
with:
fetch-depth: 1
sparse-checkout: |
src/
tests/
Concurrent Jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- run: npm run build
Anti-Fabrication Requirements
- Execute Read tool to verify workflow files exist before claiming structure
- Use Bash with
gh workflow list to confirm actual workflow names before referencing them
- Execute
gh workflow view <workflow> to verify trigger configuration before documenting it
- Use Glob to find actual workflow files before claiming their presence
- Execute
gh run list to verify actual workflow runs before discussing execution patterns
- Never claim workflow success rates without actual run history analysis
- Validate YAML syntax using yamllint or similar tools via Bash before claiming correctness
- Report actual permission errors from workflow runs, not fabricated authorization issues
- Execute actual cache operations before claiming cache hit/miss percentages
- Use Read tool on action.yml files to verify action inputs/outputs before documenting usage