.do

CI/CD

Automate testing and deployment pipelines

CI/CD

Automate your testing and deployment pipelines for fast, reliable releases of Business-as-Code and Services-as-Software.

Overview

Modern CI/CD pipelines automate the entire software delivery process from code commit to production deployment. For Cloudflare Workers and edge deployments, automation is critical for maintaining velocity while ensuring reliability.

GitHub Actions Workflows

Complete CI/CD Pipeline

Full production-ready workflow with Cloudflare Workers:

name: CI/CD Pipeline

on:
  push:
    branches: [main, develop, 'release/**']
  pull_request:
    branches: [main, develop]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to deploy'
        required: true
        default: 'staging'
        type: choice
        options:
          - development
          - staging
          - production

env:
  NODE_VERSION: '20'
  PNPM_VERSION: '8'

jobs:
  # Quality checks
  quality:
    name: Code Quality
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint code
        run: pnpm lint

      - name: Type check
        run: pnpm typecheck

      - name: Check formatting
        run: pnpm format:check

      - name: Security audit
        run: pnpm audit --audit-level moderate
        continue-on-error: true

  # Unit and integration tests
  test:
    name: Test Suite
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run unit tests
        run: pnpm test:unit --coverage

      - name: Run integration tests
        run: pnpm test:integration

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json
          flags: unittests

  # Build validation
  build:
    name: Build Workers
    runs-on: ubuntu-latest
    needs: [quality, test]
    strategy:
      matrix:
        worker: [api, auth, webhooks, mcp, pipeline]
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build ${{ matrix.worker }} worker
        run: pnpm --filter @workers/${{ matrix.worker }} build

      - name: Check bundle size
        run: |
          SIZE=$(stat -f%z "workers/${{ matrix.worker }}/dist/index.js" 2>/dev/null || stat -c%s "workers/${{ matrix.worker }}/dist/index.js")
          echo "Bundle size: ${SIZE} bytes"
          if [ $SIZE -gt 1048576 ]; then
            echo "::warning::Bundle size exceeds 1MB limit"
          fi

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: worker-${{ matrix.worker }}
          path: workers/${{ matrix.worker }}/dist
          retention-days: 7

  # Deploy preview for PRs
  deploy-preview:
    name: Deploy Preview
    runs-on: ubuntu-latest
    needs: [build]
    if: github.event_name == 'pull_request'
    environment:
      name: preview-pr-${{ github.event.pull_request.number }}
      url: https://pr-${{ github.event.pull_request.number }}.example.do
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Deploy to Cloudflare Workers
        run: |
          pnpm wrangler deploy --env preview \
            --var PR_NUMBER:${{ github.event.pull_request.number }} \
            --var COMMIT_SHA:${{ github.sha }}
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      - name: Run smoke tests
        run: pnpm test:smoke --url https://pr-${{ github.event.pull_request.number }}.example.do

      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## 🚀 Preview Deployed

              Your changes have been deployed to a preview environment:

              - **URL**: https://pr-${{ github.event.pull_request.number }}.example.do
              - **Commit**: \`${{ github.sha }}\`
              - **Environment**: \`preview\`

              The preview will be automatically deleted when this PR is closed.`
            })

  # Deploy to staging
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: [build]
    if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release/')
    environment:
      name: staging
      url: https://staging.example.do
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Deploy all workers to staging
        run: |
          pnpm wrangler deploy --env staging
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      - name: Run E2E tests against staging
        run: pnpm test:e2e --url https://staging.example.do
        env:
          TEST_API_TOKEN: ${{ secrets.STAGING_API_TOKEN }}

      - name: Notify Slack
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "✅ Staging deployment complete",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Staging Deployment Complete*\n\n• Commit: `${{ github.sha }}`\n• Branch: `${{ github.ref_name }}`\n• URL: https://staging.example.do"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

  # Deploy to production
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: [build]
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://example.do
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Create deployment record
        id: deployment
        run: |
          DEPLOYMENT_ID=$(date +%s)
          echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
          echo "Deployment ID: $DEPLOYMENT_ID"

      - name: Deploy with gradual rollout
        run: |
          # Deploy canary (10% traffic)
          pnpm wrangler versions upload --tag canary
          pnpm wrangler versions deploy --percentage 10 canary

          # Wait and monitor
          sleep 60

          # Check error rate
          ERROR_RATE=$(curl -s "https://api.cloudflare.com/client/v4/accounts/${{ secrets.CLOUDFLARE_ACCOUNT_ID }}/workers/scripts/api/metrics" \
            -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" | jq '.result.error_rate')

          if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
            echo "::error::Error rate too high, rolling back"
            pnpm wrangler versions deploy --percentage 100 stable
            exit 1
          fi

          # Increase to 50%
          pnpm wrangler versions deploy --percentage 50 canary

          sleep 60

          # Full rollout
          pnpm wrangler versions deploy --percentage 100 canary
          pnpm wrangler versions tag canary stable
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      - name: Run production smoke tests
        run: pnpm test:smoke --url https://example.do
        env:
          TEST_API_TOKEN: ${{ secrets.PRODUCTION_API_TOKEN }}

      - name: Create GitHub release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: v${{ steps.deployment.outputs.deployment_id }}
          release_name: Production Release v${{ steps.deployment.outputs.deployment_id }}
          body: |
            Automated production deployment

            Commit: ${{ github.sha }}
            Deployed at: ${{ github.event.head_commit.timestamp }}
          draft: false
          prerelease: false

      - name: Notify Slack
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "🚀 Production deployment complete",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Production Deployment Complete* 🎉\n\n• Deployment: `${{ steps.deployment.outputs.deployment_id }}`\n• Commit: `${{ github.sha }}`\n• URL: https://example.do"
                  }
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plain_text",
                        "text": "View Metrics"
                      },
                      "url": "https://dash.cloudflare.com"
                    }
                  ]
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

  # Rollback if needed
  rollback:
    name: Emergency Rollback
    runs-on: ubuntu-latest
    if: failure() && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: ${{ env.PNPM_VERSION }}

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Rollback to stable version
        run: |
          pnpm wrangler versions deploy --percentage 100 stable
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

      - name: Notify team of rollback
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "⚠️ Emergency rollback executed for commit ${{ github.sha }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Wrangler Configuration

Multi-Environment Setup

Configure environments in wrangler.toml:

name = "api-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]

# Development environment
[env.development]
name = "api-worker-dev"
workers_dev = true
vars = { ENVIRONMENT = "development" }

[[env.development.kv_namespaces]]
binding = "KV"
id = "dev-kv-namespace-id"

[[env.development.d1_databases]]
binding = "DB"
database_name = "dev-database"
database_id = "dev-db-id"

# Staging environment
[env.staging]
name = "api-worker-staging"
route = { pattern = "staging.example.do/*", zone_name = "example.do" }
vars = { ENVIRONMENT = "staging" }

[[env.staging.kv_namespaces]]
binding = "KV"
id = "staging-kv-namespace-id"

[[env.staging.d1_databases]]
binding = "DB"
database_name = "staging-database"
database_id = "staging-db-id"

# Production environment
[env.production]
name = "api-worker"
routes = [
  { pattern = "api.example.do/*", zone_name = "example.do" },
  { pattern = "example.do/api/*", zone_name = "example.do" }
]
vars = { ENVIRONMENT = "production" }

[[env.production.kv_namespaces]]
binding = "KV"
id = "production-kv-namespace-id"

[[env.production.d1_databases]]
binding = "DB"
database_name = "production-database"
database_id = "production-db-id"

[[env.production.r2_buckets]]
binding = "STORAGE"
bucket_name = "production-storage"

# Observability
[observability]
enabled = true
head_sampling_rate = 1

Advanced Pipeline Stages

1. Security Scanning

Scan for vulnerabilities:

security-scan:
  name: Security Scan
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        scan-type: 'fs'
        scan-ref: '.'
        format: 'sarif'
        output: 'trivy-results.sarif'

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

    - name: Run Snyk security scan
      uses: snyk/actions/node@master
      env:
        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      with:
        args: --severity-threshold=high

    - name: Check for secrets
      uses: trufflesecurity/trufflehog@main
      with:
        path: ./
        base: ${{ github.event.repository.default_branch }}
        head: HEAD

2. Performance Testing

Load test before production:

performance-test:
  name: Performance Test
  runs-on: ubuntu-latest
  needs: [deploy-staging]
  steps:
    - uses: actions/checkout@v4

    - name: Install k6
      run: |
        curl https://github.com/grafana/k6/releases/download/v0.47.0/k6-v0.47.0-linux-amd64.tar.gz -L | tar xvz
        sudo mv k6-v0.47.0-linux-amd64/k6 /usr/local/bin/

    - name: Run load test
      run: k6 run tests/load-test.js

    - name: Upload results
      uses: actions/upload-artifact@v4
      with:
        name: k6-results
        path: results/

Load test script (tests/load-test.js):

import http from 'k6/http'
import { check, sleep } from 'k6'

export const options = {
  stages: [
    { duration: '2m', target: 100 }, // Ramp up
    { duration: '5m', target: 100 }, // Stay at 100
    { duration: '2m', target: 200 }, // Ramp to 200
    { duration: '5m', target: 200 }, // Stay at 200
    { duration: '2m', target: 0 }, // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'], // Less than 1% errors
  },
}

export default function () {
  const res = http.get('https://staging.example.do/api/health')

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  })

  sleep(1)
}

3. Database Migrations

Run migrations before deployment:

migrate:
  name: Run Migrations
  runs-on: ubuntu-latest
  needs: [test]
  steps:
    - uses: actions/checkout@v4

    - uses: pnpm/action-setup@v2

    - uses: actions/setup-node@v4

    - name: Install dependencies
      run: pnpm install --frozen-lockfile

    - name: Run D1 migrations
      run: |
        # Apply migrations to staging
        pnpm wrangler d1 migrations apply staging-database --env staging

        # Verify migrations
        pnpm wrangler d1 execute staging-database --env staging \
          --command "SELECT name FROM sqlite_master WHERE type='table';"
      env:
        CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

4. Visual Regression Testing

Catch UI regressions:

visual-test:
  name: Visual Regression Test
  runs-on: ubuntu-latest
  needs: [deploy-preview]
  steps:
    - uses: actions/checkout@v4

    - uses: pnpm/action-setup@v2

    - uses: actions/setup-node@v4

    - name: Install dependencies
      run: pnpm install --frozen-lockfile

    - name: Install Playwright
      run: pnpm playwright install --with-deps

    - name: Run visual tests
      run: pnpm playwright test --grep @visual
      env:
        BASE_URL: https://pr-${{ github.event.pull_request.number }}.example.do

    - name: Upload screenshots
      if: failure()
      uses: actions/upload-artifact@v4
      with:
        name: visual-regression-diffs
        path: test-results/

Deployment Strategies in CI/CD

Canary Deployment

Gradual rollout with automated verification:

deploy-canary:
  name: Canary Deploy
  runs-on: ubuntu-latest
  steps:
    - name: Deploy canary (10%)
      run: |
        pnpm wrangler versions upload --tag canary
        pnpm wrangler versions deploy --percentage 10 canary

    - name: Monitor for 5 minutes
      run: |
        for i in {1..10}; do
          ERROR_RATE=$(curl -s "$METRICS_API" | jq '.error_rate')
          echo "Error rate: $ERROR_RATE"

          if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
            echo "::error::Error rate exceeded threshold"
            pnpm wrangler versions deploy --percentage 0 canary
            exit 1
          fi

          sleep 30
        done

    - name: Increase to 50%
      run: pnpm wrangler versions deploy --percentage 50 canary

    - name: Monitor again
      run: ./scripts/monitor-deployment.sh

    - name: Full rollout
      run: pnpm wrangler versions deploy --percentage 100 canary

Blue-Green Deployment

Switch between environments:

deploy-blue-green:
  name: Blue-Green Deploy
  runs-on: ubuntu-latest
  steps:
    - name: Determine current environment
      id: current
      run: |
        ACTIVE=$(curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/active_worker" \
          -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
          | jq -r '.result.value')
        echo "active=$ACTIVE" >> $GITHUB_OUTPUT
        echo "inactive=$([[ $ACTIVE == 'blue' ]] && echo 'green' || echo 'blue')" >> $GITHUB_OUTPUT

    - name: Deploy to inactive environment
      run: |
        pnpm wrangler deploy --env ${{ steps.current.outputs.inactive }}

    - name: Run smoke tests on inactive
      run: |
        pnpm test:smoke --url https://${{ steps.current.outputs.inactive }}.example.do

    - name: Switch traffic to new environment
      run: |
        curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/active_worker" \
          -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
          -H "Content-Type: application/json" \
          -d '{"value": "${{ steps.current.outputs.inactive }}"}'

    - name: Monitor new environment
      run: ./scripts/monitor-deployment.sh --duration 300

    - name: Rollback on failure
      if: failure()
      run: |
        curl -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/active_worker" \
          -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
          -H "Content-Type: application/json" \
          -d '{"value": "${{ steps.current.outputs.active }}"}'

Secrets Management

GitHub Secrets Configuration

Required secrets for CI/CD:

# Cloudflare credentials
CLOUDFLARE_API_TOKEN=your-api-token
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_ZONE_ID=your-zone-id

# API tokens for testing
STAGING_API_TOKEN=staging-token
PRODUCTION_API_TOKEN=production-token

# Third-party integrations
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
SNYK_TOKEN=snyk-api-token
DATADOG_API_KEY=datadog-key

# Database credentials (if needed)
DATABASE_URL=postgresql://...

Using Secrets in Workers

Store secrets in Cloudflare:

# Set secret via wrangler
pnpm wrangler secret put API_KEY --env production

# Or via GitHub Actions
echo "${{ secrets.API_KEY }}" | pnpm wrangler secret put API_KEY --env production

Access in worker code:

export default {
  async fetch(request: Request, env: Env) {
    // Access secret from env
    const apiKey = env.API_KEY

    // Use in request
    const response = await fetch('https://api.example.com', {
      headers: {
        Authorization: `Bearer ${apiKey}`,
      },
    })

    return response
  },
}

Monitoring Deployments

Deployment Tracking

Track deployment metrics:

// scripts/monitor-deployment.sh
#!/bin/bash

DEPLOYMENT_ID=$1
START_TIME=$(date +%s)
DURATION=${2:-300} # Default 5 minutes

while [ $(($(date +%s) - START_TIME)) -lt $DURATION ]; do
  # Fetch metrics from Cloudflare
  METRICS=$(curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/api/metrics" \
    -H "Authorization: Bearer $API_TOKEN")

  ERROR_RATE=$(echo $METRICS | jq '.result.error_rate')
  P95_LATENCY=$(echo $METRICS | jq '.result.p95_latency')
  REQUESTS=$(echo $METRICS | jq '.result.requests')

  echo "Time: $(date) | Errors: $ERROR_RATE | P95: $P95_LATENCY | RPS: $REQUESTS"

  # Check thresholds
  if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
    echo "::error::Error rate exceeded 1%"
    exit 1
  fi

  if (( $(echo "$P95_LATENCY > 1000" | bc -l) )); then
    echo "::warning::P95 latency exceeded 1000ms"
  fi

  sleep 30
done

echo "✅ Deployment monitoring completed successfully"

Best Practices

1. Automated Testing

  • Unit tests: Test individual functions
  • Integration tests: Test service interactions
  • E2E tests: Test complete user flows
  • Performance tests: Validate under load
  • Security tests: Scan for vulnerabilities

2. Deployment Safety

  • Gradual rollouts: Start with small percentage
  • Automated rollbacks: Rollback on failures
  • Smoke tests: Verify critical paths
  • Monitoring: Watch metrics during deployment
  • Feature flags: Control feature availability

3. Environment Parity

  • Configuration as code: Manage in version control
  • Consistent environments: Same setup across envs
  • Secret management: Use secure secret storage
  • Database migrations: Automated and reversible

4. Documentation

  • Deployment runbooks: Document procedures
  • Incident response: Have rollback plans
  • Change logs: Track what was deployed
  • Architecture docs: Keep up to date

Troubleshooting

Common Issues

Build failures:

# Clear cache and rebuild
pnpm store prune
rm -rf node_modules
pnpm install
pnpm build

Deployment timeouts:

# Increase timeout in wrangler.toml
[build]
command = "pnpm build"
cwd = "./"
watch_dirs = ["src"]

[deploy]
workers_dev = true
compatibility_date = "2024-01-01"
keep_vars = true

Secret not found:

# List secrets
pnpm wrangler secret list --env production

# Re-create secret
echo "new-value" | pnpm wrangler secret put SECRET_NAME --env production

Next Steps


CI/CD Tip: Automate everything. The best deployment is one that requires zero manual intervention.