GitHub Actions has evolved from a simple automation tool to a full-featured CI/CD platform. Beyond basic build and test workflows, it supports complex deployment pipelines, matrix builds, reusable workflows, and sophisticated orchestration.
Here are advanced patterns that make GitHub Actions shine.
Workflow Optimization
Matrix Builds
Test across multiple configurations:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [14, 16, 18]
exclude:
- os: windows-latest
node: 14
include:
- os: ubuntu-latest
node: 18
coverage: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
- if: matrix.coverage
run: npm run coverage
Caching
Speed up workflows with caching:
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# For Go
- uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
# For Python
- uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
Conditional Execution
Run steps based on conditions:
steps:
# Only on main branch
- if: github.ref == 'refs/heads/main'
run: npm run deploy
# Only for pull requests
- if: github.event_name == 'pull_request'
run: npm run lint
# Only when specific files changed
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
src:
- 'src/**'
docs:
- 'docs/**'
- if: steps.changes.outputs.src == 'true'
run: npm test
# Only on success of previous steps
- if: success()
run: npm run notify-success
# Only on failure
- if: failure()
run: npm run notify-failure
Reusable Workflows
Creating Reusable Workflows
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
version:
required: true
type: string
secrets:
aws-access-key:
required: true
aws-secret-key:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v3
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.aws-access-key }}
aws-secret-access-key: ${{ secrets.aws-secret-key }}
aws-region: us-east-1
- name: Deploy
run: ./deploy.sh ${{ inputs.environment }} ${{ inputs.version }}
Using Reusable Workflows
# .github/workflows/deploy-production.yml
name: Deploy Production
on:
push:
tags:
- 'v*'
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
version: ${{ github.ref_name }}
secrets:
aws-access-key: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Complex Deployment Patterns
Environment Protection
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- run: ./deploy.sh production
Configure environment protection rules in GitHub:
- Required reviewers
- Wait timer
- Branch restrictions
Canary Deployments
jobs:
deploy-canary:
runs-on: ubuntu-latest
steps:
- name: Deploy to canary
run: kubectl set image deployment/app app=${{ env.IMAGE }}:${{ github.sha }} --namespace=canary
- name: Wait and verify
run: |
sleep 300 # 5 minutes
./scripts/verify-canary.sh
- name: Check metrics
id: metrics
run: |
ERROR_RATE=$(./scripts/get-error-rate.sh canary)
echo "error_rate=$ERROR_RATE" >> $GITHUB_OUTPUT
promote:
needs: deploy-canary
if: needs.deploy-canary.outputs.error_rate < 1
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: kubectl set image deployment/app app=${{ env.IMAGE }}:${{ github.sha }} --namespace=production
rollback:
needs: deploy-canary
if: failure() || needs.deploy-canary.outputs.error_rate >= 1
runs-on: ubuntu-latest
steps:
- name: Rollback canary
run: kubectl rollout undo deployment/app --namespace=canary
Multi-Region Deployments
jobs:
build:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.build.outputs.image }}
steps:
- uses: actions/checkout@v3
- id: build
run: |
IMAGE="registry.example.com/app:${{ github.sha }}"
docker build -t $IMAGE .
docker push $IMAGE
echo "image=$IMAGE" >> $GITHUB_OUTPUT
deploy:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
region: [us-east-1, eu-west-1, ap-southeast-1]
max-parallel: 1 # Sequential deployment
fail-fast: false
steps:
- name: Deploy to ${{ matrix.region }}
run: |
./deploy.sh ${{ matrix.region }} ${{ needs.build.outputs.image }}
- name: Verify deployment
run: |
./verify.sh ${{ matrix.region }}
Custom Actions
Composite Actions
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Setup Node.js, install dependencies, and configure environment'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '18'
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
- name: Cache dependencies
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
shell: bash
run: npm ci
- name: Setup environment
shell: bash
run: cp .env.example .env
Usage:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-project
with:
node-version: '18'
JavaScript Actions
// .github/actions/notify-slack/index.js
const core = require('@actions/core');
const github = require('@actions/github');
async function run() {
try {
const webhook = core.getInput('webhook-url');
const status = core.getInput('status');
const { context } = github;
const message = {
text: `Build ${status}: ${context.repo.owner}/${context.repo.repo}`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${context.workflow}* ${status}\n<${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}|View run>`
}
}
]
};
await fetch(webhook, {
method: 'POST',
body: JSON.stringify(message)
});
} catch (error) {
core.setFailed(error.message);
}
}
run();
Security Best Practices
Secrets Management
# Use environment secrets for sensitive environments
jobs:
deploy:
environment: production
steps:
- run: echo "${{ secrets.PROD_API_KEY }}" # Environment-scoped secret
# Mask sensitive output
steps:
- run: |
TOKEN=$(./get-token.sh)
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> $GITHUB_OUTPUT
Limit Permissions
# Workflow-level permissions
permissions:
contents: read
packages: write
# Job-level permissions
jobs:
build:
permissions:
contents: read
deploy:
permissions:
contents: read
deployments: write
Pin Actions to SHA
# Instead of
- uses: actions/checkout@v3
# Pin to specific SHA
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
Secure Pull Request Workflows
# For untrusted PR code
on:
pull_request_target: # Runs in context of base branch
jobs:
build:
permissions:
contents: read
steps:
# Checkout PR code safely
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
# Don't expose secrets to PR code
- run: npm test
Testing Workflows
Act for Local Testing
# Install act
brew install act
# Run workflow locally
act push -W .github/workflows/ci.yml
# With secrets
act -s GITHUB_TOKEN=xxx
# Specific job
act -j build
Workflow Debugging
steps:
- name: Debug context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- name: Enable debug logging
run: echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV
Cost Optimization
Self-Hosted Runners
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v3
- run: make build
When to use:
- High volume builds
- Specialized hardware needs
- On-premises requirements
- Cost savings at scale
Minimize Runner Time
# Cancel in-progress runs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Fail fast on matrix
strategy:
fail-fast: true
# Timeout
jobs:
build:
timeout-minutes: 15
Key Takeaways
- Matrix builds test multiple configurations efficiently
- Caching dramatically speeds up workflows; use it for dependencies
- Reusable workflows reduce duplication across repositories
- Environments with protection rules enable safe deployments
- Composite actions package common steps; JavaScript actions for complex logic
- Limit permissions to minimum required; pin actions to SHA
- Use
pull_request_targetcarefully for PRs from forks - Self-hosted runners reduce costs at scale
- Cancel in-progress runs and set timeouts to avoid waste
GitHub Actions is powerful enough for complex CI/CD pipelines. Invest in workflow design for maintainable, secure, and efficient automation.