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 = 1Advanced 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: HEAD2. 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 canaryBlue-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 productionAccess 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 buildDeployment 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 = trueSecret not found:
# List secrets
pnpm wrangler secret list --env production
# Re-create secret
echo "new-value" | pnpm wrangler secret put SECRET_NAME --env productionNext Steps
- Infrastructure → - Infrastructure as Code
- Strategies → - Deployment patterns
- Configuration → - Manage configs
- Observe → - Monitor deployments
CI/CD Tip: Automate everything. The best deployment is one that requires zero manual intervention.