CI/CD Patterns with GitHub Actions
GitHub Actions is now the default CI for most open-source projects. Build matrices save config lines, caching node_modules and Go modules saves minutes per run. Reusable workflows keep things DRY across repos.
Build Matrices
Don’t copy-paste job definitions for different Node versions or OS targets. Use a matrix:
jobs:
test:
strategy:
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm testThis single definition expands to 6 parallel jobs. Add fail-fast: false if you want all runs to complete even when one fails — helpful for catching platform-specific bugs in one CI run.
Smart Caching
Caching is the easiest way to cut CI minutes:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: npm-${{ runner.os }}-The restore-keys fallback is important: even a partial cache hit saves time. For Go modules, cache ~/go/pkg/mod. For Docker builds, use BuildKit’s registry cache (--cache-from type=registry) for layer caching across CI runs.
Environment-Specific Deployments
Use environments to gate production deploys:
deploy-prod:
needs: [test, build]
environment: production
steps:
- run: ./deploy.sh productionThe environment: production lets you set required reviewers, wait timers, and environment-specific secrets in the GitHub UI. No more accidentally deploying to prod from a feature branch.
Reusable Workflows
When you maintain multiple repos, reusable workflows prevent drift:
# .github/workflows/deploy.yml in a shared repo
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
AWS_ROLE:
required: trueCall it from any repo:
jobs:
deploy:
uses: org/shared-workflows/.github/workflows/deploy.yml@main
with:
environment: staging
secrets:
AWS_ROLE: ${{ secrets.AWS_ROLE }}Update the shared workflow once, and every repo benefits. This pattern scales well for orgs with 10+ services.
Practical Tips
- Set
timeout-minuteson every job — the default is 360 minutes, and you don’t want a hung test burning your quota. - Use
concurrencyto cancel redundant runs when pushing to the same PR multiple times. - Pin action versions to SHA hashes for supply-chain security, not just tags.