GitHub Actions has become the dominant CI/CD platform. This guide covers production patterns beyond the basic "run tests" workflow.
1. Workflow Structure
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ env.NODE_VERSION }} }
- run: npm ci
- run: npm run lint
test:
needs: lint
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node-version }} }
- run: npm ci
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
2. Matrix Builds
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
fail-fast: false
max-parallel: 6
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node-version }} }
- run: npm ci
- run: npm test
3. Caching
steps:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- run: npm ci
Cache tips:
- Use
hashFiles()with the lock file - Use
restore-keysfor fallback - Cache Docker layers with
docker/build-push-action@v5andcache-from: type=gha
4. Docker Build and Push
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.ref == 'refs/heads/main' }}
tags: ghcr.io/${{ github.repository }}:latest,${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
5. Deployment Environments
jobs:
staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- run: ./deploy-staging.sh
production:
needs: staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
# Environment requires manual approval
steps:
- uses: actions/checkout@v4
- run: ./deploy-production.sh
6. Reusable Workflows
# .github/workflows/node-ci.yml
on:
workflow_call:
inputs:
node-version: { required: false, type: string, default: '20' }
secrets:
NPM_TOKEN: { required: false }
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ inputs.node-version }} }
- run: npm ci
- run: npm test
# consumer repo
jobs:
ci:
uses: org/shared-workflows/.github/workflows/node-ci.yml@v1
with:
node-version: '22'
7. Common Pitfalls
| Issue | Fix |
|---|---|
| 5min+ installs | Add actions/cache@v4 |
| Pipeline too long | Use matrix builds, parallel jobs |
| Hardcoded secrets | Use GitHub Environments |
| Duplicate deployments | Use concurrency group |
| Workflow hard to maintain | Split into reusable workflows |
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
A production-grade pipeline has: matrix builds, caching, conditional deployment with environment gates, and reusable workflows. Treat your workflow file like code.