Skip to content
intermediate Phase 16 · DevOps & Deployment

CI/CD Pipelines

Set up automated testing and deployment with GitHub Actions, matrix builds, and deployment strategies.

1h 15m
0 problems
Topic Progress 0%

GitHub Actions Pipeline Architecture

GitHub Actions Pipeline Architecture

Trigger Configuration

GitHub Actions workflows are defined as YAML files in .github/workflows/. The on key controls when the pipeline runs. Push events trigger on branch commits, while pull_request events run on PR creation and updates. The schedule trigger uses cron syntax for periodic runs like nightly builds or dependency updates.

# .github/workflows/ci.yml
name: CI Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Weekly Monday at 2am UTC
  workflow_dispatch:       # Manual trigger
    inputs:
      skip_tests:
        description: 'Skip test suite'
        required: false
        default: 'false'

Job Dependency Graph

Jobs run in parallel by default. Use needs to create dependency chains. The outputs keyword passes data between jobs, and if conditions control conditional execution based on previous job results or context variables.

jobs:
  lint:
    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 typecheck

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run test:unit
      - run: npm run test:integration
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/testdb
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/
          retention-days: 14

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: |
          docker build -t myapp:${{ github.sha }} .
          docker tag myapp:${{ github.sha }} myapp:latest
      - name: Push to registry
        run: |
          echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin
          docker push myapp:${{ github.sha }}
          docker push myapp:latest

Matrix Builds

Matrix strategy runs the same job across multiple OS or language versions in parallel. This catches compatibility issues before merge. Combine with exclude and include to fine-tune the matrix.

  test-matrix:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20, 22]
        exclude:
          - os: windows-latest
            node-version: 18
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

Automated Quality Gates

Automated Quality Gates

Linting and Type Checking

Quality gates enforce code standards before merge. Run Prettier for formatting, ESLint for code quality, and TypeScript compiler for type safety. The --max-warnings 0 flag treats warnings as failures.

  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci

      - name: Prettier check
        run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css}"

      - name: ESLint
        run: npx eslint . --max-warnings 0 --format stylish

      - name: TypeScript
        run: npx tsc --noEmit

      - name: Test coverage threshold
        run: npm run test:coverage -- --coverageThresholds=80
        if: github.event_name == 'pull_request'

PR Coverage Comment

Post coverage summaries as PR comments so reviewers see impact without leaving the pull request. The sticky-pull-request-comment action recreates the comment on each push.

      - name: Generate coverage report
        run: npm run test:coverage
        if: github.event_name == 'pull_request'

      - name: Comment coverage on PR
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          recreate: true
          path: coverage/coverage-summary.json
          header: 'coverage-report'

Security Scanning

Scan container images for known vulnerabilities using Trivy. Upload SARIF results to GitHub Security tab for centralized vulnerability tracking across the repository.

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image for scanning
        run: docker build -t myapp:scan .

      - name: Scan with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:scan'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

      - name: Upload to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Dependency audit
        run: npm audit --audit-level=high
        continue-on-error: true

Branch Protection Rules

Configure branch protection in repository settings or via the GitHub API. Required status checks must pass before merge. Require at least one approval and restrict force pushes to the main branch.

# Branch protection for main:
# Required status checks:
#   - quality
#   - test
#   - security
#   - build
# Require pull request reviews: 1 approval minimum
# Require branches to be up to date before merging
# Restrict who can push to matching branches
# Require signed commits
# Require linear history (no merge commits)

Deployment Strategies and Rollbacks

Deployment Strategies and Rollbacks

Blue-Green Deployment

Blue-green deployment maintains two identical environments. The blue environment serves production traffic while green is updated. After smoke testing green, traffic is switched over. If issues arise, traffic routes back to blue instantly.

# .github/workflows/deploy-production.yml
deploy-production:
  runs-on: ubuntu-latest
  environment:
    name: production
    url: https://myapp.com
  steps:
    - uses: actions/checkout@v4

    - name: Deploy to green environment
      run: |
        docker-compose -f docker-compose.green.yml up -d --build
        docker-compose -f docker-compose.green.yml exec -T app node scripts/migrate.js

    - name: Health check green
      run: |
        for i in {1..15}; do
          STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://green.myapp.com/health)
          if [ "$STATUS" = "200" ]; then
            echo "Green environment healthy after $i attempts"
            exit 0
          fi
          echo "Attempt $i: status $STATUS"
          sleep 10
        done
        echo 'Green environment failed health checks'
        exit 1

    - name: Run smoke tests against green
      run: |
        npm run test:smoke -- --baseUrl=http://green.myapp.com

    - name: Switch traffic to green
      run: |
        # Update load balancer or DNS to point to green
        aws elbv2 modify-rule --rule-arn $RULE_ARN \
          --actions Type=forward,TargetGroupArn=$GREEN_TG_ARN
        echo 'Traffic switched to green'

    - name: Verify production
      run: |
        sleep 30
        STATUS=$(curl -s -o /dev/null -w '%{http_code}' https://myapp.com/health)
        if [ "$STATUS" != "200" ]; then
          echo 'Rolling back to blue...'
          aws elbv2 modify-rule --rule-arn $RULE_ARN \
            --actions Type=forward,TargetGroupArn=$BLUE_TG_ARN
          exit 1
        fi
        echo 'Deployment verified successfully'

    - name: Cleanup old environment
      if: success()
      run: docker-compose -f docker-compose.blue.yml down

Canary Deployment

Canary deployments gradually shift traffic to the new version. Start with 5% of traffic, monitor error rates and latency, then incrementally increase. Automated rollback triggers if metrics exceed thresholds.

  canary-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy canary (5% traffic)
        run: |
        kubectl apply -f k8s/canary-deployment.yaml
        kubectl set image deployment/app-canary app=myapp:${{ github.sha }}
        kubectl scale deployment/app-canary --replicas=1

      - name: Monitor canary metrics
        run: |
          sleep 120
          ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~'5..',version='canary'}[5m])/rate(http_requests_total{version='canary'}[5m])*100" | jq -r '.data.result[0].value[1]')
          echo "Canary error rate: ${ERROR_RATE}%"
          if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
            echo 'Error rate exceeded threshold, rolling back'
            kubectl delete deployment app-canary
            exit 1
          fi

      - name: Promote canary to full deployment
        run: |
          kubectl set image deployment/app-production app=myapp:${{ github.sha }}
          kubectl rollout status deployment/app-production --timeout=300s
          kubectl delete deployment app-canary

Automated Rollback on Failure

  deploy-with-rollback:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Capture current version
        id: current
        run: |
          CURRENT=$(kubectl get deployment app -o jsonpath='{.spec.template.spec.containers[0].image}')
          echo "version=$CURRENT" >> $GITHUB_OUTPUT

      - name: Deploy new version
        id: deploy
        continue-on-error: true
        run: |
          kubectl set image deployment/app app=myapp:${{ github.sha }}
          kubectl rollout status deployment/app --timeout=180s

      - name: Rollback on failure
        if: steps.deploy.outcome == 'failure'
        run: |
          echo "Deploy failed, rolling back to ${{ steps.current.outputs.version }}"
          kubectl rollout undo deployment/app
          kubectl rollout status deployment/app --timeout=180s

Database Migrations in CI/CD

Run database migrations as a separate step before application deployment. Use lock files or advisory locks to prevent concurrent migration runs across replicas.

  migrate-and-deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Run migrations
        run: |
          npx prisma migrate deploy
          echo 'Migrations applied successfully'
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL_PRODUCTION }}

      - name: Verify schema
        run: npx prisma db pull --print | head -20

      - name: Deploy application
        run: |
          kubectl set image deployment/app app=myapp:${{ github.sha }}
          kubectl rollout status deployment/app --timeout=300s

Secrets Management

GitHub Actions secrets are encrypted and masked in logs. Environment-specific secrets are scoped to deployment environments. Use GITHUB_TOKEN for repository operations without storing additional credentials.

  deploy-with-secrets:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1

      - name: Deploy to ECS
        run: |
          aws ecs update-service --cluster production --service myapp \
            --force-new-deployment

      - name: Notify Slack
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {"text": "Deployed ${{ github.sha }} to production"}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Quiz

1. What does the `needs` keyword do in a GitHub Actions workflow?

Question 1 options

2. In a blue-green deployment, what is the primary advantage over a simple rolling update?

Question 2 options

3. Why should you use `npm ci` instead of `npm install` in CI/CD pipelines?

Question 3 options

Flashcards

Question

What is the difference between `on: push` and `on: pull_request` triggers in GitHub Actions?

Answer

`on: push` triggers when code is pushed to a branch (merges, direct pushes). `on: pull_request` triggers when a PR is opened, synchronized, or reopened. Use push for main branch deployments and pull_request for CI validation before merge.

Question

How does a canary deployment differ from a blue-green deployment?

Answer

Blue-green maintains two full environments and switches all traffic at once. Canary gradually shifts a small percentage of traffic (e.g., 5%) to the new version, monitors metrics, then increases. Canary reduces risk by catching issues with minimal blast radius before full rollout.

Question

What are GitHub Actions environments and why use them?

Answer

Environments are deployment targets (staging, production) with their own secrets, variables, and protection rules. They enable environment-specific secrets, require manual approvals before deployment, provide deployment history, and allow conditional step execution based on the target environment.

Revision Notes

Key Takeaways

  • 1. GitHub Actions workflows trigger on push, pull_request, schedule, and workflow_dispatch events with configurable branch filters
  • 2. Use `needs` to create job dependency chains and pass data between jobs via `outputs`
  • 3. Matrix strategy runs jobs across multiple OS/language versions to catch compatibility issues
  • 4. Quality gates should enforce linting, type checking, test coverage thresholds, and security scans before merge
  • 5. Blue-green deployment maintains two environments for instant rollback; canary gradually shifts traffic with metric monitoring
  • 6. Use `npm ci` in CI for deterministic builds from package-lock.json instead of `npm install`

Interview Tips

  • Explain how you would structure a CI/CD pipeline for a microservices architecture with independent deployments
  • Describe the tradeoffs between blue-green, canary, and rolling deployment strategies
  • Discuss how to handle database migrations safely in a CI/CD pipeline with zero downtime
  • Explain the purpose of GitHub Actions environments and how they differ from repository secrets
  • Describe strategies for reducing CI pipeline execution time (caching, parallel jobs, matrix builds)
  • Discuss how to implement automated rollback when deployment health checks fail

Cheat Sheet

GitHub Actions: on: push/PR/schedule/workflow_dispatch triggers. needs: creates job dependencies. services: starts Docker containers for tests. matrix: parallel builds across OS/versions. Deploy: blue-green switches all traffic instantly, canary shifts percentage gradually. Secrets: environment-scoped, masked in logs. npm ci: deterministic installs from lockfile. Health checks: curl loop with retry logic. Rollback: kubectl rollout undo or traffic switch back to previous environment.