GitHub Actions handles 70% of CI/CD workloads for open-source projects and an increasing share of enterprise pipelines. Moving beyond basic build-test-deploy workflows requires matrix strategies for multi-platform testing, reusable workflows for DRY pipelines, OIDC for secure cloud authentication, and caching to cut build times by 60-80%. This guide covers each pattern with production-tested configurations.
---
Matrix Builds
Matrix strategies run the same job across multiple configurations simultaneously. This is essential for testing across Node.js versions, operating systems, or database versions.
<h1>.github/workflows/test.yml</h1>
name: Test Matrix
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # Continue other jobs if one fails
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18 # Skip older Node on Windows
include:
- os: ubuntu-latest
node: 22
coverage: true # Add coverage only for one combo
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
- if: matrix.coverage
run: npm run coverage
| Matrix Feature | Purpose | Example |
| exclude | Skip specific combinations | Skip Node 18 on Windows |
| include | Add extra properties | Add coverage flag to one combo |
| fail-fast: false | Run all even if one fails | Useful for compatibility testing |
| max-parallel | Limit concurrent jobs | Reduce runner costs |
---
Reusable Workflows
Reusable workflows define a workflow once and call it from multiple repositories or workflows. This eliminates duplication across projects.
<h1>.github/workflows/reusable-deploy.yml (in shared repo)</h1>
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
image_tag:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Deploy to ${{ inputs.environment }}
run: |
echo "Deploying ${{ inputs.image_tag }} to ${{ inputs.environment }}"
kubectl set image deployment/app \
app=${{ inputs.image_tag }}
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
<h1>.github/workflows/production.yml (in consuming repo)</h1>
name: Production Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.build.outputs.tag }}
steps:
- uses: actions/checkout@v4
- id: build
run: echo "tag=myapp:${{ github.sha }}" >> $GITHUB_OUTPUT
deploy-staging:
needs: build
uses: org/shared-workflows/.github/workflows/reusable-deploy.yml@main
with:
environment: staging
image_tag: ${{ needs.build.outputs.image_tag }}
secrets:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
deploy-production:
needs: [build, deploy-staging]
uses: org/shared-workflows/.github/workflows/reusable-deploy.yml@main
with:
environment: production
image_tag: ${{ needs.build.outputs.image_tag }}
secrets:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
---
Composite Actions
Composite actions bundle multiple steps into a single reusable action. Unlike reusable workflows, composite actions run within the calling job.
<h1>.github/actions/setup-project/action.yml</h1>
name: Setup Project
description: Install dependencies and build
inputs:
node-version:
description: Node.js version
default: "20"
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
shell: bash
- run: npm run build
shell: bash
---
OIDC Authentication
OIDC eliminates long-lived credentials for cloud provider access. GitHub Actions generates a short-lived JWT token that your cloud provider exchanges for temporary credentials.
<h1>OIDC with AWS</h1>
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: us-east-1
- run: aws s3 ls # Uses temporary credentials
| Auth Method | Security | Setup Complexity | Credential Rotation |
| Repository secrets | Medium | Low | Manual |
| Environment secrets | Higher | Medium | Manual |
| OIDC | Highest | Medium | Automatic (per-run) |
| Self-hosted runner IAM | Highest | High | Automatic |
---
Dependency Caching
<h1>Advanced caching strategies</h1>
steps:
- uses: actions/cache@v4
id: npm-cache
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci
# Docker layer caching
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
| Cache Strategy | Time Saved | Cache Size | Best For |
| npm/yarn cache | 30-60s | 50-200 MB | Node.js projects |
| pip cache | 20-40s | 20-100 MB | Python projects |
| Docker layer cache | 2-10 min | 500 MB-5 GB | Container builds |
| Gradle/Maven cache | 1-5 min | 100-500 MB | Java projects |
| Turbo/nx cache | 1-10 min | 50-500 MB | Monorepos |
---
Self-Hosted Runners
<h1>Set up self-hosted runner on VPS</h1>
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64.tar.gz -L \
https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-2.321.0.tar.gz
tar xzf actions-runner-linux-x64.tar.gz
<h1>Configure with repository token</h1>
./config.sh --url https://github.com/org/repo \
--token YOUR_TOKEN \
--labels self-hosted,linux,x64,gpu
<h1>Install as service</h1>
sudo ./svc.sh install
sudo ./svc.sh start
For CI/CD pipeline fundamentals, see our CI/CD pipeline guide for startups. Teams implementing feature flags alongside CI/CD should read our feature flags deployment strategy. For infrastructure testing within your CI pipeline, check our infrastructure testing strategies guide.
---
Frequently Asked Questions
How do I reduce GitHub Actions costs?
Three strategies: (1) Use caching aggressively to reduce build times. (2) Use self-hosted runners for heavy workloads (Docker builds, GPU tasks). (3) Cancel redundant workflow runs with concurrency groups. The concurrency key cancels in-progress runs when a new commit is pushed to the same branch, saving minutes on superseded builds.
When should I use reusable workflows vs composite actions?
Use reusable workflows when you need a complete job with its own runner, environment protections, and secret access. Use composite actions when you want to bundle steps that run within an existing job. Reusable workflows are better for deployment pipelines. Composite actions are better for setup and build steps.
How secure is OIDC compared to stored secrets?
OIDC is significantly more secure. Stored secrets can be exfiltrated by anyone with write access to the repository (via a malicious workflow). OIDC tokens are scoped to a specific repository, branch, and environment, expire after the job completes, and cannot be reused. The cloud provider IAM role can restrict access based on the repository, branch, and workflow.
Can I run GitHub Actions locally for testing?
Use the act tool (github.com/nektos/act) to run workflows locally in Docker containers. It supports most workflow features but has limitations with services, OIDC, and some GitHub context variables. For simple build-test workflows, act provides 90% accuracy compared to running on GitHub.
What is the maximum workflow run time?
Each job can run for up to 6 hours on GitHub-hosted runners. Each workflow run can last up to 35 days (including wait times for approvals). Self-hosted runners have no time limit. If your builds take longer than 6 hours, split them into multiple jobs or use self-hosted runners.