| name | github-actions |
| description | Comprehensive guide for building robust, secure, and efficient CI/CD pipelines using GitHub Actions. Use when the task involves `creating or editing GitHub Actions workflows`, `.github/workflows/*.yml`, `CI/CD pipelines`, `GitHub Actions best practices`, or `workflow optimization`. |
| license | MIT |
| metadata | {"version":"1.0.0"} |
When to Use
- Creating or editing GitHub Actions workflow files (
.github/workflows/*.yml)
- Reviewing CI/CD pipelines for security, performance, or correctness
- Setting up automated testing, building, or deployment pipelines
- Troubleshooting failing or flaky GitHub Actions workflows
- Designing deployment strategies (staging, production, blue/green, canary)
Critical Patterns
- Pin Actions to Commit SHA: Always use full commit SHA, never mutable tags (
@v4, @main).
Add version as comment:
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1. Tags can be silently
moved to compromised commits (supply chain attack).
- Least Privilege
GITHUB_TOKEN: Set permissions: contents: read at workflow level. Grant
write only where explicitly needed, per-job.
- Secrets via
secrets Context Only: Never hardcode sensitive data. Use ${{ secrets.NAME }}.
Use environment secrets for deployment targets with manual approvals.
- OIDC Over Static Credentials: Use OpenID Connect for cloud auth (AWS, Azure, GCP) instead of
long-lived access keys.
- Cache Dependencies: Use
actions/cache with hashFiles() keys for node_modules, pip,
Maven, etc. to dramatically speed up builds.
- Shallow Clone: Use
fetch-depth: 1 in actions/checkout unless full history is needed.
- Fail Fast on Security: Integrate dependency review and SAST (CodeQL) as blocking checks.
- Environment Protection: Use GitHub Environments with required reviewers and branch
restrictions for staging/production deploys.
Workflow Structure
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
environment:
description: 'Deploy target'
required: false
default: 'staging'
type: choice
options: [ staging, production ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 1
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
if: always()
with:
name: test-results
path: coverage/
retention-days: 14
build:
runs-on: ubuntu-latest
needs: test
outputs:
artifact_name: ${{ steps.package.outputs.name }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Package application
id: package
run: |
zip -r dist.zip dist
echo "name=dist.zip" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
with:
name: app-build
path: dist.zip
retention-days: 30
deploy-staging:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: app-build
- name: Deploy to staging
env:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
run: |
unzip dist.zip
echo "Deploying to staging..."
# ./deploy.sh --env staging
Code Examples
Action Pinning (Security)
- uses: actions/checkout@v4
- uses: some-org/some-action@main
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: some-org/some-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Permissions (Least Privilege)
permissions:
contents: read
jobs:
lint:
steps: [ ... ]
deploy:
permissions:
contents: read
deployments: write
steps: [ ... ]
comment-on-pr:
permissions:
contents: read
pull-requests: write
steps: [ ... ]
Caching
- name: Cache node modules
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Matrix Strategy
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest ]
node-version: [ 18, 20, 22 ]
exclude:
- os: windows-latest
node-version: 18
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610
with:
node-version: ${{ matrix.node-version }}
- run: npm ci && npm test
OIDC Cloud Authentication
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722
with:
role-to-assume: arn:aws:iam::123456789012:role/my-deploy-role
aws-region: us-east-1
- run: aws s3 sync dist/ s3://my-bucket/
Integration Tests with Services
jobs:
integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
Environment Protection (Production Deploy)
jobs:
deploy-prod:
runs-on: ubuntu-latest
needs: [ test, build, deploy-staging ]
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://example.com
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: app-build
- name: Deploy to production
env:
PROD_API_KEY: ${{ secrets.PROD_API_KEY }}
run: ./deploy.sh --env production
- name: Post-deploy smoke test
run: |
curl -sf https://example.com/health || exit 1
echo "Smoke test passed"
Reusable Workflow
name: Reusable Build
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '20'
outputs:
artifact-name:
description: 'Name of the uploaded artifact'
value: ${{ jobs.build.outputs.artifact-name }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: app-build-${{ github.sha }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci && npm run build
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
with:
name: app-build-${{ github.sha }}
path: dist/
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '20'
Anti-Patterns to Avoid
| Anti-Pattern | Risk | Do Instead |
|---|
uses: actions/checkout@v4 (mutable tag) | Supply chain attack — tag can be moved to malicious commit | Pin to full SHA: @34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 |
No permissions block | GITHUB_TOKEN gets default broad access | Set permissions: contents: read at workflow level |
secrets.MY_KEY in echo or logs | Secret leakage even with masking | Never print secrets; use them only in env vars for commands |
| Long-lived cloud credentials as secrets | Large blast radius if compromised | Use OIDC for short-lived tokens |
fetch-depth: 0 by default | Slow checkout, wastes bandwidth | fetch-depth: 1 unless full history needed |
No concurrency on deploy workflows | Race conditions, double deploys | Add concurrency with cancel-in-progress |
| Hardcoded versions in multiple places | Drift, maintenance burden | Use reusable workflows or composite actions |
No timeout-minutes on jobs | Hung workflows burn runner minutes | Set reasonable timeout-minutes per job |
No retention-days on artifacts | Storage bloat, cost increase | Set retention-days based on need (7-30 days) |
| Running E2E tests on every push | Slow CI, wasted resources | Run on PR and main only, or use path filters |
Deployment Strategies Reference
| Strategy | How It Works | Best For | Rollback |
|---|
| Rolling | Gradually replaces old instances | Stateless apps, most cases | Redeploy previous version |
| Blue/Green | Full parallel env, switch traffic | Zero-downtime critical apps | Switch traffic back to blue |
| Canary | Route small % to new version | Risk-sensitive changes | Route 100% back to stable |
| Dark Launch | Deploy hidden behind feature flags | Decoupling deploy from release | Toggle flag off |
Commands
actionlint .github/workflows/*.yml
act push --job build
gh run list --workflow=ci.yml
gh run view <run-id> --log
gh run rerun <run-id> --failed
gh workflow run deploy.yml -f environment=staging
gh secret list
Resources