Back to Blog
CI/CDGitHub ActionsDevOpsAutomation

CI/CD Pipeline Design for Modern Applications

Build effective CI/CD pipelines. From GitHub Actions to testing strategies to deployment automation.

B
Bootspring Team
Engineering
June 20, 2023
6 min read

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 build

Complete 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 test

Caching 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@v2

Release 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: false

Deployment 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 fi

Reusable 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.

Share this article

Help spread the word about Bootspring