CI/CD automates building, testing, and deploying code. A well-designed pipeline catches bugs early and enables frequent, reliable releases.
GitHub Actions Basics#
1# .github/workflows/ci.yml
2name: CI
3
4on:
5 push:
6 branches: [main]
7 pull_request:
8 branches: [main]
9
10jobs:
11 test:
12 runs-on: ubuntu-latest
13
14 steps:
15 - uses: actions/checkout@v4
16
17 - name: Setup Node.js
18 uses: actions/setup-node@v4
19 with:
20 node-version: '20'
21 cache: 'npm'
22
23 - name: Install dependencies
24 run: npm ci
25
26 - name: Run linter
27 run: npm run lint
28
29 - name: Run type check
30 run: npm run typecheck
31
32 - name: Run tests
33 run: npm test
34
35 - name: Build
36 run: npm run buildComplete Pipeline#
1# .github/workflows/ci-cd.yml
2name: CI/CD
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10env:
11 NODE_VERSION: '20'
12 REGISTRY: ghcr.io
13 IMAGE_NAME: ${{ github.repository }}
14
15jobs:
16 lint:
17 runs-on: ubuntu-latest
18 steps:
19 - uses: actions/checkout@v4
20 - uses: actions/setup-node@v4
21 with:
22 node-version: ${{ env.NODE_VERSION }}
23 cache: 'npm'
24 - run: npm ci
25 - run: npm run lint
26 - run: npm run typecheck
27
28 test:
29 runs-on: ubuntu-latest
30 needs: lint
31
32 services:
33 postgres:
34 image: postgres:15
35 env:
36 POSTGRES_USER: test
37 POSTGRES_PASSWORD: test
38 POSTGRES_DB: test
39 ports:
40 - 5432:5432
41 options: >-
42 --health-cmd pg_isready
43 --health-interval 10s
44 --health-timeout 5s
45 --health-retries 5
46
47 redis:
48 image: redis:7
49 ports:
50 - 6379:6379
51
52 steps:
53 - uses: actions/checkout@v4
54 - uses: actions/setup-node@v4
55 with:
56 node-version: ${{ env.NODE_VERSION }}
57 cache: 'npm'
58
59 - run: npm ci
60
61 - name: Run migrations
62 run: npx prisma migrate deploy
63 env:
64 DATABASE_URL: postgresql://test:test@localhost:5432/test
65
66 - name: Run tests
67 run: npm test -- --coverage
68 env:
69 DATABASE_URL: postgresql://test:test@localhost:5432/test
70 REDIS_URL: redis://localhost:6379
71
72 - name: Upload coverage
73 uses: codecov/codecov-action@v3
74 with:
75 files: ./coverage/lcov.info
76
77 build:
78 runs-on: ubuntu-latest
79 needs: test
80
81 steps:
82 - uses: actions/checkout@v4
83
84 - name: Set up Docker Buildx
85 uses: docker/setup-buildx-action@v3
86
87 - name: Login to Container Registry
88 uses: docker/login-action@v3
89 with:
90 registry: ${{ env.REGISTRY }}
91 username: ${{ github.actor }}
92 password: ${{ secrets.GITHUB_TOKEN }}
93
94 - name: Extract metadata
95 id: meta
96 uses: docker/metadata-action@v5
97 with:
98 images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
99 tags: |
100 type=sha
101 type=ref,event=branch
102 type=semver,pattern={{version}}
103
104 - name: Build and push
105 uses: docker/build-push-action@v5
106 with:
107 context: .
108 push: true
109 tags: ${{ steps.meta.outputs.tags }}
110 labels: ${{ steps.meta.outputs.labels }}
111 cache-from: type=gha
112 cache-to: type=gha,mode=max
113
114 deploy-staging:
115 runs-on: ubuntu-latest
116 needs: build
117 if: github.ref == 'refs/heads/develop'
118 environment: staging
119
120 steps:
121 - name: Deploy to staging
122 run: |
123 curl -X POST ${{ secrets.DEPLOY_WEBHOOK_STAGING }} \
124 -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
125 -d '{"image": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"}'
126
127 deploy-production:
128 runs-on: ubuntu-latest
129 needs: build
130 if: github.ref == 'refs/heads/main'
131 environment: production
132
133 steps:
134 - name: Deploy to production
135 run: |
136 curl -X POST ${{ secrets.DEPLOY_WEBHOOK_PRODUCTION }} \
137 -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
138 -d '{"image": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"}'Matrix Testing#
1jobs:
2 test:
3 runs-on: ubuntu-latest
4 strategy:
5 matrix:
6 node-version: [18, 20, 22]
7 database: [postgres, mysql]
8 include:
9 - database: postgres
10 db-image: postgres:15
11 db-port: 5432
12 - database: mysql
13 db-image: mysql:8
14 db-port: 3306
15
16 services:
17 database:
18 image: ${{ matrix.db-image }}
19 ports:
20 - ${{ matrix.db-port }}:${{ matrix.db-port }}
21
22 steps:
23 - uses: actions/checkout@v4
24 - uses: actions/setup-node@v4
25 with:
26 node-version: ${{ matrix.node-version }}
27 - run: npm ci
28 - run: npm testCaching Strategies#
1jobs:
2 build:
3 runs-on: ubuntu-latest
4 steps:
5 - uses: actions/checkout@v4
6
7 # Cache npm dependencies
8 - uses: actions/setup-node@v4
9 with:
10 node-version: '20'
11 cache: 'npm'
12
13 # Cache Prisma client
14 - name: Cache Prisma
15 uses: actions/cache@v3
16 with:
17 path: node_modules/.prisma
18 key: prisma-${{ hashFiles('prisma/schema.prisma') }}
19
20 # Cache build output
21 - name: Cache build
22 uses: actions/cache@v3
23 with:
24 path: .next/cache
25 key: nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
26 restore-keys: |
27 nextjs-${{ hashFiles('**/package-lock.json') }}-E2E Testing#
1jobs:
2 e2e:
3 runs-on: ubuntu-latest
4 steps:
5 - uses: actions/checkout@v4
6 - uses: actions/setup-node@v4
7 with:
8 node-version: '20'
9
10 - name: Install dependencies
11 run: npm ci
12
13 - name: Install Playwright browsers
14 run: npx playwright install --with-deps
15
16 - name: Build application
17 run: npm run build
18
19 - name: Run E2E tests
20 run: npx playwright test
21 env:
22 BASE_URL: http://localhost:3000
23
24 - name: Upload test results
25 uses: actions/upload-artifact@v3
26 if: failure()
27 with:
28 name: playwright-report
29 path: playwright-report/Security Scanning#
1jobs:
2 security:
3 runs-on: ubuntu-latest
4 steps:
5 - uses: actions/checkout@v4
6
7 - name: Run Trivy vulnerability scanner
8 uses: aquasecurity/trivy-action@master
9 with:
10 scan-type: 'fs'
11 scan-ref: '.'
12 severity: 'CRITICAL,HIGH'
13 exit-code: '1'
14
15 - name: Run npm audit
16 run: npm audit --audit-level=high
17
18 - name: Run CodeQL analysis
19 uses: github/codeql-action/analyze@v2Release Workflow#
1# .github/workflows/release.yml
2name: Release
3
4on:
5 push:
6 tags:
7 - 'v*'
8
9jobs:
10 release:
11 runs-on: ubuntu-latest
12 steps:
13 - uses: actions/checkout@v4
14 with:
15 fetch-depth: 0
16
17 - name: Generate changelog
18 id: changelog
19 uses: metcalfc/changelog-generator@v4
20 with:
21 myToken: ${{ secrets.GITHUB_TOKEN }}
22
23 - name: Create Release
24 uses: actions/create-release@v1
25 env:
26 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 with:
28 tag_name: ${{ github.ref_name }}
29 release_name: Release ${{ github.ref_name }}
30 body: ${{ steps.changelog.outputs.changelog }}
31 draft: false
32 prerelease: falseDeployment Strategies#
1# Blue-Green Deployment
2deploy:
3 steps:
4 - name: Deploy to blue environment
5 run: kubectl apply -f k8s/blue/
6
7 - name: Run smoke tests
8 run: ./scripts/smoke-test.sh $BLUE_URL
9
10 - name: Switch traffic to blue
11 run: kubectl patch service app -p '{"spec":{"selector":{"version":"blue"}}}'
12
13 - name: Cleanup green environment
14 run: kubectl delete -f k8s/green/
15
16# Canary Deployment
17deploy-canary:
18 steps:
19 - name: Deploy canary (10% traffic)
20 run: |
21 kubectl apply -f k8s/canary/
22 kubectl patch virtualservice app --type merge -p '
23 {"spec":{"http":[{"route":[
24 {"destination":{"host":"app","subset":"stable"},"weight":90},
25 {"destination":{"host":"app","subset":"canary"},"weight":10}
26 ]}]}}'
27
28 - name: Monitor canary metrics
29 run: ./scripts/monitor-canary.sh
30
31 - name: Promote or rollback
32 run: |
33 if [ "$CANARY_SUCCESS" = "true" ]; then
34 kubectl apply -f k8s/stable/
35 else
36 kubectl delete -f k8s/canary/
37 fiReusable Workflows#
1# .github/workflows/reusable-deploy.yml
2name: Reusable Deploy
3
4on:
5 workflow_call:
6 inputs:
7 environment:
8 required: true
9 type: string
10 image-tag:
11 required: true
12 type: string
13 secrets:
14 DEPLOY_TOKEN:
15 required: true
16
17jobs:
18 deploy:
19 runs-on: ubuntu-latest
20 environment: ${{ inputs.environment }}
21 steps:
22 - name: Deploy
23 run: ./deploy.sh ${{ inputs.environment }} ${{ inputs.image-tag }}
24 env:
25 DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
26
27# Usage in another workflow
28jobs:
29 deploy-staging:
30 uses: ./.github/workflows/reusable-deploy.yml
31 with:
32 environment: staging
33 image-tag: ${{ needs.build.outputs.image-tag }}
34 secrets:
35 DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Best Practices#
Pipeline Design:
✓ Fail fast - run quick checks first
✓ Parallelize independent jobs
✓ Cache dependencies aggressively
✓ Use matrix builds for compatibility
Testing:
✓ Run unit tests on every commit
✓ Run integration tests before merge
✓ Run E2E tests before deploy
✓ Include security scanning
Deployment:
✓ Use environment protection
✓ Require approvals for production
✓ Implement rollback capability
✓ Monitor after deployment
Conclusion#
Good CI/CD pipelines are fast, reliable, and provide clear feedback. Start simple with linting and testing, then add deployment automation. Invest in caching and parallelization to keep pipelines fast as your codebase grows.