GitHub Actions has become the default CI/CD tool for most engineering teams. Here's how to set up a production-grade pipeline from scratch.
Why GitHub Actions?
- Free for public repos, 2,000 minutes/month for private
- Native GitHub integration — no third-party OAuth, no webhook setup
- Massive marketplace — 20,000+ pre-built actions
- Matrix builds — test across multiple OS/language versions simultaneously
Basic Pipeline Structure
Every GitHub Actions workflow lives in .github/workflows/. Here's a production-ready starting point:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test
- run: npm run lint
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build
path: dist/
- name: Deploy to production
run: |
# Your deploy command here
echo "Deploying to production..."
Key Concepts
Job Dependencies
Use needs: to create a pipeline flow:
test → build → deploy
If tests fail, build and deploy never run. This prevents broken code from reaching production.
Caching
Always cache dependencies. Without caching, npm ci runs fresh every time (30-60 seconds wasted).
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # This caches node_modules
Secrets Management
Never hardcode credentials. Use GitHub Secrets:
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: aws s3 sync dist/ s3://my-bucket
Matrix Builds
Test across multiple versions simultaneously:
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
Production Best Practices
1. Branch Protection Rules
- Require PR reviews before merging to main
- Require status checks (CI must pass)
- No direct pushes to main
2. Environment Protection
deploy:
environment: production # Requires manual approval
runs-on: ubuntu-latest
3. Concurrency Control
Prevent multiple deploys running simultaneously:
concurrency:
group: deploy-production
cancel-in-progress: false
4. Timeout Limits
jobs:
test:
timeout-minutes: 10 # Kill stuck jobs
5. Artifact Retention
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
retention-days: 7 # Don't store forever
Docker Build and Push
Most production apps deploy containers:
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
AWS Deployment Example
Deploy to ECS, Lambda, or S3:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/deploy
aws-region: us-east-1
- name: Deploy to S3 + CloudFront
run: |
aws s3 sync dist/ s3://my-bucket --delete
aws cloudfront create-invalidation \
--distribution-id E1234 --paths "/*"
Monitoring Your Pipeline
Track these metrics:
- Build time — should be under 5 minutes
- Success rate — aim for 95%+
- Queue time — if jobs wait, add self-hosted runners
- Flaky tests — identify and fix tests that fail intermittently
Common Mistakes
- Not caching dependencies — adds 30-60s per run
-
Running everything sequentially — use
needs:for parallelism - No timeout — stuck jobs burn through minutes quota
- Deploying from PRs — always gate deploys to main branch only
- Hardcoded secrets — use GitHub Secrets, never commit credentials
Cost Optimization
-
Use
ubuntu-latest— Linux runners are cheapest -
Cancel redundant runs — use
concurrencyto stop outdated builds - Self-hosted runners — free minutes, you pay for compute
- Cache aggressively — saves minutes on every run
What does your CI/CD pipeline look like? Drop a comment with your setup.
Top comments (0)