11. CI/CD Pipeline

Chapter 11 of 18 · 15 min

CI/CD automates testing, building, and deployment. GitHub Actions provides the pipeline infrastructure. The pipeline runs on every push and pull request, catching issues before they reach production.

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          
      - name: Install dependencies
        run: |
          pip install -r backend/requirements.txt
          pip install -r requirements-dev.txt
          
      - name: Run linting
        run: |
          flake8 backend/
          mypy backend/
          
      - name: Run unit tests
        run: pytest backend/tests/unit -v
        
      - name: Run integration tests
        run: |
          docker compose -f docker-compose.test.yml up -d
          pytest backend/tests/integration -v
          docker compose -f docker-compose.test.yml down

  build:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        
      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=semver,pattern={{version}}
            
      - name: Build and push backend
        uses: docker/build-push-action@v5
        with:
          context: ./backend
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}-backend
          cache-from: type=gha
          cache-to: type=gha,mode=max
          
      - name: Build and push frontend
        uses: docker/build-push-action@v5
        with:
          context: ./frontend
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}-frontend
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    environment: staging
    steps:
      - name: Deploy to staging
        run: |
          echo "${{ secrets.SSH_KEY }}" > deploy_key
          chmod 600 deploy_key
          ssh -o StrictHostKeyChecking=no -i deploy_key user@staging-server \
            "docker compose -f /app/docker-compose.staging.yml pull && \
             docker compose -f /app/docker-compose.staging.yml up -d"

Pipeline failures often stem from flaky tests. Implement test retries with exponential backoff for integration tests that interact with external services. Cache dependencies between runs to speed up the pipeline. Use matrix builds to test across Python versions or Node versions in parallel.

EXERCISE

Create a GitHub Actions workflow that runs tests, builds Docker images, and deploys to a staging environment on merge to main.