Beyond the Basic Pipeline
Most teams get GitHub Actions working quickly and then stop improving it. The result is a CI pipeline that takes 20 minutes, burns through expensive minutes, and has secrets copied across a dozen workflow files. The advanced patterns in this guide cut build times by 60–80%, eliminate secret duplication, and make pipelines maintainable as codebases grow.
Caching Dependencies Correctly
Dependency installation is often the slowest step in a pipeline. Use actions/cache with a hash of your lockfile as the cache key — the cache is invalidated when dependencies change and reused otherwise.
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
For Python projects, cache the pip download directory rather than site-packages, since site-packages can vary by Python version and platform in ways that cause subtle cache poisoning bugs.
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
Matrix Builds for Cross-Platform Testing
Test across multiple versions, operating systems, or configurations in parallel using a matrix strategy. All combinations run concurrently — no sequential overhead.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: ['18', '20', '22']
exclude:
- os: windows-latest
node: '18' # skip this specific combination
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
Set fail-fast: false so a failure in one matrix combination does not cancel all others — you want to see the full failure picture, not just the first failure.
Reusable Workflows
When the same pipeline logic appears in multiple repositories, extract it into a reusable workflow. The calling workflow passes inputs; the reusable workflow does the work. This is the right pattern for shared deployment logic across a monorepo or across a fleet of microservices.
# .github/workflows/deploy-reusable.yml
on:
workflow_call:
inputs:
environment:
required: true
type: string
service:
required: true
type: string
secrets:
AWS_ROLE_ARN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- run: ./scripts/deploy.sh ${{ inputs.service }} ${{ inputs.environment }}
# Calling workflow
jobs:
deploy:
uses: ./.github/workflows/deploy-reusable.yml
with:
environment: production
service: api
secrets:
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}
OIDC for Keyless Cloud Authentication
Storing AWS or GCP credentials as GitHub secrets is a security risk — long-lived credentials that can be leaked. Use OIDC (OpenID Connect) instead: GitHub generates a short-lived token for each workflow run that your cloud provider can validate without any stored secret.
# Configure AWS IAM trust policy to accept GitHub OIDC tokens
# (one-time setup in AWS IAM)
# In your workflow — no long-lived credentials needed
jobs:
deploy:
permissions:
id-token: write # required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
aws-region: us-east-1
# No access key or secret key — OIDC handles it
OIDC tokens are valid for the duration of the workflow run only, have no expiry to rotate, and can be scoped to specific branches or environments in the IAM trust policy.
Concurrency Controls
By default, pushing multiple commits quickly queues up multiple deployments. Use concurrency groups to cancel superseded runs automatically.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # cancel older runs in the same branch
For deployment workflows, you may want cancel-in-progress: false so a deployment is never interrupted mid-flight — only queued-but-not-started runs are cancelled.
Speed Optimisation Checklist
- Use
actions/upload-artifactto pass build outputs between jobs instead of rebuilding in each job - Move slow checks (e2e tests, security scans) to a separate workflow triggered on PRs but not blocking merge for non-critical paths
- Use
pathsfilters to skip workflows when only unrelated files change (e.g. skip backend tests when only docs change) - Split a large test suite across parallel jobs using test sharding
- Use larger runners (
ubuntu-latest-4-cores) for compute-heavy jobs — the cost per minute is higher but total time and cost often drops