CI/CD Pipeline Best Practices
Pipeline Stages
Code Push
|
v
[1] Build & Lint (< 2 min)
- Compile TypeScript
- Run ESLint
- Check formatting
|
v
[2] Unit Tests (< 5 min)
- Run jest unit tests
- Check coverage threshold
|
v
[3] Integration Tests (< 10 min)
- Spin up test DB/Redis
- Run integration tests
|
v
[4] Build Docker Image (< 5 min)
- Multi-stage build
- Push to registry
|
v
[5] Deploy to Staging (< 3 min)
|
v
[6] E2E Tests (< 15 min)
- Playwright tests against staging
|
v
[7] Deploy to Production (manual approval)
GitHub Actions Pipeline
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: myorg/api
jobs:
quality:
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 lint
- run: npm run type-check
- run: npm run test:unit -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
integration-test:
needs: quality
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/test
REDIS_URL: redis://localhost:6379
build:
needs: integration-test
runs-on: ubuntu-latest
outputs:
image: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging
run: |
kubectl set image deployment/api api=${{ needs.build.outputs.image }}
kubectl rollout status deployment/api --timeout=120s
e2e:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
env:
BASE_URL: https://staging.myapp.com
deploy-production:
needs: e2e
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
steps:
- name: Deploy to production
run: |
kubectl set image deployment/api api=${{ needs.build.outputs.image }} -n production
kubectl rollout status deployment/api -n production --timeout=300s
Deployment Strategies
# Rolling update (default in Kubernetes)
kubectl set image deployment/api api=myimage:v2
kubectl rollout status deployment/api
# Rollback if issues
kubectl rollout undo deployment/api
# Canary deployment
kubectl apply -f canary-deployment.yaml # 10% of pods run new version
# Monitor metrics...
kubectl scale deployment api-canary --replicas=0 # Rollback
# OR
kubectl scale deployment api-canary --replicas=10 # Full rollout
# Blue-Green deployment
kubectl patch service api -p '{"spec":{"selector":{"version":"green"}}}'
Measuring Pipeline Effectiveness
DORA Metrics:
Deployment Frequency: How often you deploy to production
Elite: Multiple times/day
Lead Time for Changes: Code commit -> production
Elite: < 1 hour
Change Failure Rate: % of deployments causing incidents
Elite: 0-15%
Mean Time to Restore (MTTR): Time to recover from failures
Elite: < 1 hour
A fast, reliable CI/CD pipeline is the foundation of high-performing engineering teams.