
Cloudflare Workers Ci Cd
- 220 installs
- 202 repo stars
- Updated August 4, 2026
- secondsky/claude-skills
Use cloudflare-workers-ci-cd for development tasks
About
cloudflare-workers-ci-cd: A skill for development. This provides functionality for development workflows.
- cloudflare-workers-ci-cd
Cloudflare Workers Ci Cd by the numbers
- 220 all-time installs (skills.sh)
- +12 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,809 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/secondsky/claude-skills --skill cloudflare-workers-ci-cdAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 220 |
|---|---|
| repo stars | ★ 202 |
| Last updated | August 4, 2026 |
| Repository | secondsky/claude-skills ↗ |
What it does
Use cloudflare-workers-ci-cd for development tasks
Files
Cloudflare Workers CI/CD
Status: ✅ Production Ready | Last Verified: 2025-01-27 GitHub Actions: v4 | GitLab CI: Latest | Wrangler: 4.50.0
Table of Contents
- What Is Workers CI/CD?
- New in 2025
- Quick Start (10 Minutes)
- Critical Rules
- Core Concepts
- Top 5 Use Cases
- Best Practices
- Top 7 Errors Prevented
- When to Load References
---
What Is Workers CI/CD?
Automated testing and deployment of Cloudflare Workers using GitHub Actions or GitLab CI. Enables running tests on every commit, deploying to preview/staging/production environments automatically, managing secrets securely, and implementing deployment gates for safe releases.
Key capabilities: Automated testing, multi-environment deployments, preview URLs per PR, secrets management, deployment verification, automatic rollbacks.
---
New in 2025
GitHub Actions Updates (January 2025):
- NEW:
cloudflare/wrangler-action@v4(improved caching, faster deployments) - IMPROVED: Secrets support with
varsandsecretsparameters - ADDED: Built-in preview environment cleanup
- BREAKING:
apiTokenrenamed toapi-token(kebab-case)
Migration from v3:
# ❌ OLD (v3)
- uses: cloudflare/wrangler-action@3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# ✅ NEW (v4)
- uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}Wrangler 4.50.0 (January 2025):
- NEW:
--dry-runflag for deployment validation - IMPROVED: Faster deployments with parallel uploads
- ADDED:
--keep-varsto preserve environment variables
---
Quick Start (10 Minutes)
GitHub Actions Setup
1. Create Cloudflare API Token
Go to: https://dash.cloudflare.com/profile/api-tokens
Create token with permissions:
- Account.Cloudflare Workers Scripts - Edit
- Account.Cloudflare Pages - Edit (if using Pages)
2. Add Secret to GitHub
Repository → Settings → Secrets → Actions → New repository secret:
- Name:
CLOUDFLARE_API_TOKEN - Value: [paste token]
3. Create `.github/workflows/deploy.yml`
name: Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy to Cloudflare Workers
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install
- run: bun test
- name: Deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy4. Push and Verify
git add .github/workflows/deploy.yml
git commit -m "Add CI/CD pipeline"
git pushCheck Actions tab on GitHub to see deployment progress.
---
Critical Rules
1. Never Commit Secrets to Git
✅ CORRECT:
# Use GitHub Secrets
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}❌ WRONG:
# ❌ NEVER hardcode tokens
api-token: "abc123def456..."Why: Exposed tokens allow anyone to deploy to your account.
2. Always Run Tests Before Deploy
✅ CORRECT:
- run: bun test # ✅ Tests run first
- name: Deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}❌ WRONG:
# ❌ Skipping tests
- name: Deploy
uses: cloudflare/wrangler-action@v4
# No tests!Why: Broken code shouldn't reach production.
3. Use Different Environments
✅ CORRECT:
# Production (main branch)
- name: Deploy to Production
if: github.ref == 'refs/heads/main'
run: bunx wrangler deploy --env production
# Staging (other branches)
- name: Deploy to Staging
if: github.ref != 'refs/heads/main'
run: bunx wrangler deploy --env staging❌ WRONG:
# ❌ Always deploying to production
- run: bunx wrangler deployWhy: Test changes in staging before production.
4. Verify Deployment Success
✅ CORRECT:
- name: Deploy
id: deploy
uses: cloudflare/wrangler-action@v4
- name: Verify Deployment
run: |
curl -f https://your-worker.workers.dev/health || exit 1❌ WRONG:
# ❌ No verification
- name: Deploy
uses: cloudflare/wrangler-action@v4
# Assuming it worked...Why: Deployments can fail silently (DNS issues, binding errors).
5. Use Deployment Gates for Production
✅ CORRECT:
deploy-production:
environment:
name: production
url: https://your-worker.workers.dev
# Requires manual approval❌ WRONG:
# ❌ Auto-deploy to production without review
deploy-production:
runs-on: ubuntu-latestWhy: Human review catches issues automation misses.
---
Core Concepts
Multi-Environment Strategy
Recommended setup:
- Production:
mainbranch → production environment - Staging: Pull requests → staging environment
- Preview: Each PR → unique preview URL
wrangler.jsonc:
{
"name": "my-worker",
"main": "src/index.ts",
"env": {
"production": {
"name": "my-worker-production",
"vars": {
"ENVIRONMENT": "production"
}
},
"staging": {
"name": "my-worker-staging",
"vars": {
"ENVIRONMENT": "staging"
}
}
}
}Secrets Management
Types of configuration: 1. Public variables (wrangler.jsonc) - Non-sensitive config 2. Secrets (wrangler secret) - API keys, tokens 3. CI variables (GitHub Secrets) - Deployment credentials
Setting secrets:
# Local development
wrangler secret put DATABASE_URL
# CI/CD (via GitHub Actions)
bunx wrangler secret put DATABASE_URL --env production <<< "${{ secrets.DATABASE_URL }}"Preview Deployments
Automatically deploy each PR to a unique URL for testing:
- name: Deploy Preview
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env preview-${{ github.event.number }}Each PR gets URL like: my-worker-preview-42.workers.dev
---
Top 5 Use Cases
1. Deploy on Push to Main
name: Deploy Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- run: bun run build
- name: Deploy to Production
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production2. Preview Deployments for PRs
name: Preview
on:
pull_request:
branches: [main]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- name: Deploy Preview
id: deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env preview-${{ github.event.number }}
- name: Comment PR
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 to: https://my-worker-preview-${{ github.event.number }}.workers.dev'
})3. Run Tests on Every Commit
name: Test
on:
push:
branches: ['**']
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info4. Deploy with Approval Gate
name: Deploy Production (Manual Approval)
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://my-worker.workers.dev
# Requires manual approval in GitHub Settings
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- name: Deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production5. Staged Rollout (Canary)
name: Canary Deployment
on:
workflow_dispatch:
inputs:
percentage:
description: 'Traffic percentage to new version'
required: true
default: '10'
jobs:
canary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
# Deploy to canary environment
- name: Deploy Canary
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env canary
# Configure traffic split via Cloudflare API
# (See references/deployment-strategies.md for full example)---
Best Practices
✅ DO
1. Use semantic commit messages:
feat: add user authentication
fix: resolve rate limiting issue
chore: update dependencies2. Run linting and type checking:
- run: bun run lint
- run: bun run type-check
- run: bun test3. Cache dependencies:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Bun automatically caches dependencies4. Deploy different branches to different environments:
- name: Deploy
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
bunx wrangler deploy --env production
else
bunx wrangler deploy --env staging
fi5. Monitor deployments:
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{"text": "Deployment failed: ${{ github.sha }}"}❌ DON'T
1. Don't skip tests 2. Don't deploy without verification 3. Don't hardcode secrets 4. Don't deploy to production from feature branches 5. Don't ignore deployment failures
---
Top 7 Errors Prevented
1. ❌ Error: A valid Cloudflare API token is required
Cause: Missing or invalid CLOUDFLARE_API_TOKEN secret.
Fix: 1. Create API token: https://dash.cloudflare.com/profile/api-tokens 2. Add to GitHub Secrets: Settings → Secrets → Actions 3. Use in workflow: api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
---
2. ❌ Error: Not enough permissions to deploy
Cause: API token lacks required permissions.
Fix: Recreate token with:
- Account.Cloudflare Workers Scripts - Edit
- Account settings - Read
---
3. ❌ Error: wrangler.toml not found
Cause: Missing wrangler configuration.
Fix: Ensure wrangler.jsonc exists in repository root.
---
4. ❌ Deployment succeeds but worker doesn't work
Cause: Missing secrets or environment variables.
Fix: Set secrets in CI:
- name: Set Secrets
run: |
echo "${{ secrets.DATABASE_URL }}" | bunx wrangler secret put DATABASE_URL --env production---
5. ❌ Tests pass locally but fail in CI
Cause: Environment differences (Node version, missing dependencies).
Fix:
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest # Lock version
- run: bun install --frozen-lockfile # Use exact versions---
6. ❌ Preview deployments conflict
Cause: Multiple PRs deploying to same preview environment.
Fix: Use PR number in environment name:
command: deploy --env preview-${{ github.event.number }}---
7. ❌ Secrets exposed in logs
Cause: Echoing secrets in workflow.
Fix:
# ❌ WRONG
- run: echo "Token: ${{ secrets.API_TOKEN }}"
# ✅ CORRECT
- run: echo "Deploying..." # No secrets in output---
When to Load References
Load reference files for detailed, specialized content:
Load `references/github-actions.md` when:
- Setting up GitHub Actions from scratch
- Configuring matrix builds (multiple Node versions)
- Using GitHub environments and deployment protection
- Implementing deployment gates and approvals
Load `references/gitlab-ci.md` when:
- Setting up GitLab CI pipelines
- Configuring GitLab environments
- Using GitLab secret variables
- Implementing review apps
Load `references/deployment-strategies.md` when:
- Implementing blue-green deployments
- Setting up canary releases
- Configuring traffic splitting
- Planning rollback procedures
Load `references/secrets-management.md` when:
- Managing secrets across environments
- Rotating API tokens
- Using external secret providers (Vault, 1Password)
- Implementing least-privilege access
Load `templates/github-actions-full.yml` for:
- Complete production-ready GitHub Actions workflow
- Multi-environment deployment example
- All deployment gates configured
Load `templates/gitlab-ci-full.yml` for:
- Complete GitLab CI pipeline
- Multi-stage deployment
- Review app configuration
Load `templates/preview-deployment.yml` for:
- PR preview deployment setup
- Automatic cleanup on PR close
- Comment with preview URL
Load `templates/rollback-workflow.yml` for:
- Manual rollback workflow
- Deployment history tracking
- Automated rollback on health check failure
Load `scripts/verify-deployment.sh` for:
- Automated deployment verification
- Health check implementation
- Smoke tests after deployment
---
Secure Installation
When installing CI/CD dependencies, follow supply chain security best practices:
- Block post-install scripts —
npm config set ignore-scripts true(or Bun: disabled by default) - Frozen lockfiles in CI — Always use
npm ciorbun install --frozen-lockfile - Security gate — Add
socket cito your CI pipeline to block PRs that violate your security policy
Load the dependency-upgrade skill for full security configuration including Socket CLI integration, cooldown setup, lockfile validation, and CI enforcement.
Related Cloudflare Plugins
For deployment testing, load:
- cloudflare-workers-testing - Test Workers before deployment
- cloudflare-manager - Manage deployments via Cloudflare API
This skill focuses on CI/CD automation for ALL Workers deployments regardless of bindings used.
---
Questions? Load references/secrets-management.md or use /workers-deploy command for guided deployment.
Deployment Strategies for Cloudflare Workers
Advanced deployment patterns for safe, reliable Workers releases.
Blue-Green Deployment
Deploy new version alongside old, then switch traffic atomically.
Strategy: 1. Deploy new version to "green" environment 2. Test green environment 3. Switch traffic from "blue" to "green" 4. Keep blue as instant rollback option
Implementation:
# .github/workflows/blue-green.yml
name: Blue-Green Deployment
on:
workflow_dispatch:
inputs:
version:
description: 'Version to deploy'
required: true
jobs:
deploy-green:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.version }}
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
# Deploy to green environment
- name: Deploy Green
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env green
# Test green environment
- name: Smoke Test Green
run: |
sleep 10
curl -f https://my-worker-green.workers.dev/health || exit 1
switch-traffic:
needs: deploy-green
runs-on: ubuntu-latest
environment:
name: production # Manual approval required
steps:
# Update DNS/Route to point to green
- name: Switch to Green
run: |
# Update Cloudflare route to green worker
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/${{ secrets.ZONE_ID }}/workers/routes/${{ secrets.ROUTE_ID }}" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{"script":"my-worker-green"}'
- name: Verify Production
run: |
sleep 5
curl -f https://my-worker.workers.dev/health || exit 1Canary Deployment
Gradually roll out new version to percentage of traffic.
Strategy: 1. Deploy canary version 2. Route 10% traffic to canary 3. Monitor metrics (errors, latency) 4. Incrementally increase to 25%, 50%, 100% 5. Rollback if issues detected
Implementation:
# .github/workflows/canary.yml
name: Canary Deployment
on:
workflow_dispatch:
inputs:
percentage:
description: 'Traffic percentage (10, 25, 50, 100)'
required: true
default: '10'
jobs:
canary-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
# Deploy canary version
- name: Deploy Canary
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env canary
# Configure traffic split via Cloudflare Load Balancer
- name: Set Traffic Split
run: |
PERCENTAGE=${{ github.event.inputs.percentage }}
STABLE=$((100 - PERCENTAGE))
curl -X PUT "https://api.cloudflare.com/client/v4/zones/${{ secrets.ZONE_ID }}/load_balancers/${{ secrets.LB_ID }}" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{
"default_pools": [
{"id": "${{ secrets.CANARY_POOL_ID }}", "weight": '$PERCENTAGE'},
{"id": "${{ secrets.STABLE_POOL_ID }}", "weight": '$STABLE'}
]
}'
- name: Monitor Canary
run: |
echo "Monitoring canary deployment at ${{ github.event.inputs.percentage }}%"
echo "Check metrics: https://dash.cloudflare.com/analytics"Rolling Deployment
Deploy to workers one at a time (useful for multi-region setups).
Strategy: 1. Deploy to first worker 2. Verify health 3. Deploy to next worker 4. Repeat until all updated
Implementation:
name: Rolling Deployment
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
max-parallel: 1 # Deploy one at a time
matrix:
worker: [worker-1, worker-2, worker-3]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Deploy ${{ matrix.worker }}
working-directory: workers/${{ matrix.worker }}
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy
- name: Health Check
run: |
sleep 5
curl -f https://${{ matrix.worker }}.workers.dev/health || exit 1
- name: Wait Before Next
run: sleep 30Feature Flag Deployment
Deploy new code behind feature flags, enable gradually.
Implementation:
Worker Code:
export default {
async fetch(request: Request, env: Env) {
// Check feature flag in KV
const newFeatureEnabled = await env.KV.get('feature:new-feature');
if (newFeatureEnabled === 'true') {
return handleNewFeature(request);
}
return handleOldFeature(request);
}
}Enable Feature:
name: Enable Feature
on:
workflow_dispatch:
inputs:
feature:
description: 'Feature flag name'
required: true
enabled:
description: 'Enable (true/false)'
required: true
jobs:
toggle-feature:
runs-on: ubuntu-latest
steps:
- name: Set Feature Flag
run: |
bunx wrangler kv:key put \
--namespace-id=${{ secrets.KV_NAMESPACE_ID }} \
"feature:${{ github.event.inputs.feature }}" \
"${{ github.event.inputs.enabled }}"Rollback Strategies
Instant Rollback
Keep previous version deployed, switch back immediately:
name: Rollback
on:
workflow_dispatch:
inputs:
version:
description: 'Version to rollback to'
required: true
jobs:
rollback:
runs-on: ubuntu-latest
environment:
name: production-rollback
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.version }}
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Deploy Previous Version
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production
- name: Verify Rollback
run: curl -f https://my-worker.workers.dev/health || exit 1Automated Rollback on Failure
- name: Deploy
id: deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy
- name: Verify Deployment
id: verify
run: |
sleep 5
curl -f https://my-worker.workers.dev/health || exit 1
- name: Rollback on Failure
if: failure() && steps.deploy.outcome == 'success'
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: rollbackDeployment Verification
Health Checks
#!/bin/bash
# health-check.sh
WORKER_URL="https://my-worker.workers.dev"
MAX_RETRIES=5
RETRY_DELAY=5
for i in $(seq 1 $MAX_RETRIES); do
echo "Health check attempt $i/$MAX_RETRIES..."
if curl -f "$WORKER_URL/health"; then
echo "✅ Health check passed"
exit 0
fi
if [ $i -lt $MAX_RETRIES ]; then
echo "❌ Health check failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
fi
done
echo "❌ Health check failed after $MAX_RETRIES attempts"
exit 1Smoke Tests
// smoke-test.ts
import { expect } from 'vitest';
const WORKER_URL = process.env.WORKER_URL || 'https://my-worker.workers.dev';
async function smokeTest() {
// Test main endpoint
const response = await fetch(WORKER_URL);
expect(response.status).toBe(200);
// Test API endpoint
const apiResponse = await fetch(`${WORKER_URL}/api/health`);
expect(apiResponse.status).toBe(200);
// Test authentication
const authResponse = await fetch(`${WORKER_URL}/api/protected`, {
headers: { Authorization: 'Bearer test-token' }
});
expect(authResponse.status).toBe(401); // Should reject invalid token
console.log('✅ All smoke tests passed');
}
smokeTest().catch(err => {
console.error('❌ Smoke tests failed:', err);
process.exit(1);
});Best Practices
1. Always test before switching traffic 2. Keep rollback option ready (blue-green, previous version) 3. Monitor metrics during rollout (error rate, latency) 4. Use gradual rollout for risky changes (canary, feature flags) 5. Automate health checks after deployment 6. Document rollback procedure for on-call engineers
Resources
- Cloudflare Load Balancer: https://developers.cloudflare.com/load-balancing/
- Workers Versioning: https://developers.cloudflare.com/workers/configuration/versions-and-deployments/
GitHub Actions for Cloudflare Workers
Complete guide for setting up GitHub Actions CI/CD pipelines for Workers.
Basic Workflow Structure
name: Deploy Workers
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Dependencies
run: bun install
- name: Run Tests
run: bun test
- name: Deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deployMulti-Environment Deployments
Separate Workflows Approach
`.github/workflows/deploy-staging.yml`:
name: Deploy Staging
on:
push:
branches: [develop, 'feature/**']
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: staging
url: https://my-worker-staging.workers.dev
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- name: Deploy to Staging
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env staging`.github/workflows/deploy-production.yml`:
name: Deploy Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://my-worker.workers.dev
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- run: bun run build
- name: Deploy to Production
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production
- name: Verify Deployment
run: |
sleep 5
curl -f https://my-worker.workers.dev/health || exit 1Single Workflow with Conditions
name: Deploy
on:
push:
branches: [main, develop]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- name: Deploy to Production
if: github.ref == 'refs/heads/main'
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production
- name: Deploy to Staging
if: github.ref == 'refs/heads/develop'
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env stagingPreview Deployments for Pull Requests
name: Preview Deployment
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
preview:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
deployments: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
- name: Deploy Preview
id: deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env preview-${{ github.event.number }}
- name: Comment Preview URL
uses: actions/github-script@v7
with:
script: |
const prNumber = context.issue.number;
const previewUrl = `https://my-worker-preview-${prNumber}.workers.dev`;
github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `✅ **Preview Deployment**\n\n🔗 ${previewUrl}\n\nDeployed commit: ${context.sha.substring(0, 7)}`
});Cleanup Preview on PR Close
name: Cleanup Preview
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Delete Preview Deployment
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: delete --name my-worker-preview-${{ github.event.number }}Deployment Approvals
GitHub Environments
Configure in: Settings → Environments → New environment
Create "production" environment with:
- Required reviewers: Team leads, DevOps
- Wait timer: 5 minutes (optional)
- Deployment branches: Only
main
Workflow:
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production # Requires approval
url: https://my-worker.workers.dev
steps:
- uses: actions/checkout@v4
- uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env productionMatrix Builds
Test across multiple Node versions:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
bun-version: [latest, '1.1.0']
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ matrix.bun-version }}
- run: bun install
- run: bun testSecrets Management
Setting Secrets
1. Go to: Repository → Settings → Secrets → Actions 2. Click "New repository secret" 3. Add:
CLOUDFLARE_API_TOKENDATABASE_URLSTRIPE_SECRET_KEY- etc.
Using Secrets in Workflows
- name: Deploy with Secrets
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Set Environment Variables
run: |
echo "${{ secrets.DATABASE_URL }}" | bunx wrangler secret put DATABASE_URL --env production
echo "${{ secrets.STRIPE_SECRET_KEY }}" | bunx wrangler secret put STRIPE_SECRET_KEY --env productionEnvironment-Specific Secrets
Organization/Repository Secrets (Settings → Secrets):
CLOUDFLARE_API_TOKEN(used in all workflows)
Environment Secrets (Settings → Environments → production → Secrets):
DATABASE_URL_PRODUCTIONSTRIPE_SECRET_KEY_PRODUCTION
Usage:
jobs:
deploy:
environment: production # Automatically loads environment secrets
steps:
- run: |
echo "${{ secrets.DATABASE_URL_PRODUCTION }}" | \
bunx wrangler secret put DATABASE_URL --env productionCaching Strategies
Bun Cache (Automatic)
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Bun automatically caches dependenciesManual Caching
- name: Cache Dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-Notifications
Slack Notifications
- name: Notify Slack on Success
if: success()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "✅ Deployment successful: ${{ github.sha }}"
}
- name: Notify Slack on Failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
payload: |
{
"text": "❌ Deployment failed: ${{ github.sha }}"
}Discord Notifications
- name: Discord Notification
uses: Ilshidur/action-discord@master
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
args: 'Deployed {{ EVENT_PAYLOAD.repository.full_name }} to production'Advanced Patterns
Conditional Deployment Based on Changed Files
- name: Check for Worker Changes
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
worker:
- 'src/**'
- 'wrangler.jsonc'
- name: Deploy if Worker Changed
if: steps.changes.outputs.worker == 'true'
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deployDeployment with Retries
- name: Deploy with Retry
uses: nick-fields/retry-action@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: bunx wrangler deploy --env productionParallel Deployments
jobs:
deploy-multiple-workers:
runs-on: ubuntu-latest
strategy:
matrix:
worker: [api, frontend, worker3]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Deploy ${{ matrix.worker }}
working-directory: workers/${{ matrix.worker }}
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deployTroubleshooting
Debug Mode
- name: Enable Wrangler Debug
run: WRANGLER_LOG=debug bunx wrangler deployView Full Logs
Enable debug logging for GitHub Actions:
1. Repository → Settings → Secrets → New secret 2. Name: ACTIONS_STEP_DEBUG, Value: true
Common Issues
Error: "Repository not found"
- Fix: Add
contents: readpermission to job
Error: "Resource not accessible by integration"
- Fix: Add
pull-requests: writepermission
Deployment hangs
- Fix: Add timeout:
timeout-minutes: 10
Resources
- GitHub Actions Docs: https://docs.github.com/actions
- Wrangler Action: https://github.com/cloudflare/wrangler-action
- Cloudflare Workers Docs: https://developers.cloudflare.com/workers/
GitLab CI for Cloudflare Workers
Complete guide for GitLab CI/CD pipelines for Workers.
Basic Pipeline Structure
.gitlab-ci.yml:
image: oven/bun:latest
stages:
- test
- deploy
variables:
FF_USE_FASTZIP: "true" # Faster caching
cache:
paths:
- node_modules/
- .bun/
test:
stage: test
script:
- bun install
- bun test --coverage
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
deploy:
stage: deploy
script:
- bun install
- bunx wrangler deploy
only:
- main
environment:
name: production
url: https://my-worker.workers.devMulti-Environment Deployments
deploy-staging:
stage: deploy
script:
- bunx wrangler deploy --env staging
only:
- develop
environment:
name: staging
url: https://my-worker-staging.workers.dev
deploy-production:
stage: deploy
script:
- bunx wrangler deploy --env production
only:
- main
environment:
name: production
url: https://my-worker.workers.dev
when: manual # Requires manual triggerReview Apps (Preview Deployments)
review:
stage: deploy
script:
- bunx wrangler deploy --env review-$CI_MERGE_REQUEST_IID
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://my-worker-review-$CI_MERGE_REQUEST_IID.workers.dev
on_stop: stop_review
only:
- merge_requests
stop_review:
stage: deploy
script:
- bunx wrangler delete --name my-worker-review-$CI_MERGE_REQUEST_IID
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
when: manual
only:
- merge_requestsSecrets Management
Setting Variables
Project Settings → CI/CD → Variables → Add variable:
CLOUDFLARE_API_TOKEN(masked, protected)DATABASE_URL(masked, protected)STRIPE_SECRET_KEY(masked, protected)
Using Variables
deploy:
script:
- echo "$DATABASE_URL" | bunx wrangler secret put DATABASE_URL --env production
- echo "$STRIPE_SECRET_KEY" | bunx wrangler secret put STRIPE_SECRET_KEY --env production
- bunx wrangler deploy --env production
environment:
name: productionEnvironment-Specific Variables
Create variables scoped to environments:
deploy-production:
variables:
ENVIRONMENT: "production"
script:
- echo "$DATABASE_URL_PRODUCTION" | bunx wrangler secret put DATABASE_URL
environment:
name: productionAdvanced Patterns
Conditional Deployment
deploy:
script:
- bunx wrangler deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
- if: '$CI_COMMIT_TAG'
when: never
- changes:
- src/**/*
- wrangler.jsoncParallel Jobs
test:
parallel:
matrix:
- BUN_VERSION: ['latest', '1.1.0']
image: oven/bun:${BUN_VERSION}
script:
- bun install
- bun testDeploy to Multiple Workers
deploy-api:
script:
- cd workers/api
- bunx wrangler deploy
deploy-frontend:
script:
- cd workers/frontend
- bunx wrangler deployCaching
cache:
key:
files:
- bun.lockb
paths:
- node_modules/
- .bun/Resources
- GitLab CI Docs: https://docs.gitlab.com/ee/ci/
- Workers Deployment: https://developers.cloudflare.com/workers/
Secrets Management for Cloudflare Workers CI/CD
Complete guide for securely managing secrets, API keys, and environment variables in Workers deployments.
Types of Configuration
1. Public Variables (wrangler.jsonc)
Non-sensitive configuration committed to git:
{
"name": "my-worker",
"vars": {
"ENVIRONMENT": "production",
"LOG_LEVEL": "info",
"API_VERSION": "v1"
}
}2. Secrets (wrangler secret)
Sensitive data encrypted at rest:
# Set secret locally
wrangler secret put DATABASE_URL
# Set secret in CI
echo "${{ secrets.DATABASE_URL }}" | bunx wrangler secret put DATABASE_URL --env production3. CI/CD Secrets (GitHub/GitLab)
Credentials for deployment:
CLOUDFLARE_API_TOKEN- Deploy accessDATABASE_URL- Database connectionSTRIPE_SECRET_KEY- Payment processing
GitHub Secrets
Setting Secrets
Repository Secrets (all environments): 1. Repository → Settings → Secrets → Actions 2. New repository secret 3. Add: CLOUDFLARE_API_TOKEN, DATABASE_URL, etc.
Environment Secrets (specific environments): 1. Repository → Settings → Environments → production 2. Add environment secret 3. Add: DATABASE_URL_PRODUCTION, STRIPE_KEY_PRODUCTION
Using Secrets
jobs:
deploy:
environment: production # Loads production-specific secrets
steps:
- name: Deploy with Secrets
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Set Worker Secrets
run: |
echo "${{ secrets.DATABASE_URL }}" | \
bunx wrangler secret put DATABASE_URL --env production
echo "${{ secrets.STRIPE_SECRET_KEY }}" | \
bunx wrangler secret put STRIPE_SECRET_KEY --env productionBest Practices
✅ DO:
- Use masked secrets for sensitive data
- Scope secrets to environments
- Rotate secrets regularly
- Use descriptive secret names
❌ DON'T:
- Echo secrets in logs
- Commit secrets to git
- Share secrets across projects
- Use secrets in pull requests from forks
GitLab Variables
Setting Variables
Project Variables: 1. Project → Settings → CI/CD → Variables 2. Add variable 3. Options:
- Protected: Only available in protected branches
- Masked: Hidden in job logs
- Environment scope: Limit to specific environment
Using Variables
deploy-production:
script:
- echo "$DATABASE_URL" | bunx wrangler secret put DATABASE_URL
environment:
name: production
variables:
DEPLOY_ENV: "production"Wrangler Secrets Management
Setting Secrets
Interactive (local):
wrangler secret put API_KEY
# Prompts for valueNon-interactive (CI):
echo "secret-value" | wrangler secret put API_KEYEnvironment-specific:
wrangler secret put API_KEY --env production
wrangler secret put API_KEY --env stagingListing Secrets
wrangler secret list
wrangler secret list --env productionDeleting Secrets
wrangler secret delete API_KEY
wrangler secret delete API_KEY --env productionEnvironment-Specific Configuration
Multiple Environments
wrangler.jsonc:
{
"name": "my-worker",
"env": {
"production": {
"name": "my-worker-prod",
"vars": {
"ENVIRONMENT": "production",
"API_URL": "https://api.example.com"
}
},
"staging": {
"name": "my-worker-staging",
"vars": {
"ENVIRONMENT": "staging",
"API_URL": "https://staging-api.example.com"
}
}
}
}CI Workflow
deploy-production:
steps:
- run: |
# Production secrets
echo "${{ secrets.DATABASE_URL_PROD }}" | \
bunx wrangler secret put DATABASE_URL --env production
bunx wrangler deploy --env production
deploy-staging:
steps:
- run: |
# Staging secrets (different values)
echo "${{ secrets.DATABASE_URL_STAGING }}" | \
bunx wrangler secret put DATABASE_URL --env staging
bunx wrangler deploy --env stagingExternal Secret Providers
HashiCorp Vault
- name: Import Secrets from Vault
uses: hashicorp/vault-action@v2
with:
url: ${{ secrets.VAULT_ADDR }}
token: ${{ secrets.VAULT_TOKEN }}
secrets: |
secret/data/cloudflare DATABASE_URL | DATABASE_URL
- name: Deploy with Vault Secrets
run: |
echo "$DATABASE_URL" | bunx wrangler secret put DATABASE_URLAWS Secrets Manager
- name: Get Secrets from AWS
uses: aws-actions/aws-secretsmanager-get-secrets@v1
with:
secret-ids: |
CLOUDFLARE_*
parse-json-secrets: true
- name: Deploy
run: |
echo "$CLOUDFLARE_DATABASE_URL" | \
bunx wrangler secret put DATABASE_URL1Password
- name: Load 1Password Secrets
uses: 1password/load-secrets-action@v1
with:
export-env: true
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
DATABASE_URL: op://production/cloudflare/database-url
- name: Deploy
run: |
echo "$DATABASE_URL" | bunx wrangler secret put DATABASE_URLSecret Rotation
Automated Rotation
name: Rotate Secrets
on:
schedule:
- cron: '0 0 1 * *' # Monthly
jobs:
rotate:
runs-on: ubuntu-latest
steps:
- name: Generate New API Key
id: new-key
run: |
NEW_KEY=$(openssl rand -hex 32)
echo "::add-mask::$NEW_KEY"
echo "key=$NEW_KEY" >> $GITHUB_OUTPUT
- name: Update Worker Secret
run: |
echo "${{ steps.new-key.outputs.key }}" | \
bunx wrangler secret put API_KEY --env production
- name: Update GitHub Secret
uses: gliech/create-github-secret-action@v1
with:
name: API_KEY
value: ${{ steps.new-key.outputs.key }}
pa_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}Security Best Practices
1. Never log secrets:
# ❌ WRONG
- run: echo "API key: ${{ secrets.API_KEY }}"
# ✅ CORRECT
- run: echo "Deploying with API key" # No secret value2. Use masked secrets:
- GitHub: Automatically masked
- GitLab: Check "Mask variable" option
3. Scope secrets to environments:
- Production secrets only in production
- Separate staging/dev secrets
4. Rotate secrets regularly:
- API tokens: Every 90 days
- Database credentials: Every 180 days
5. Use least-privilege tokens:
- Cloudflare API token: Only Workers deploy permission
- Database: Read/write only, no admin
6. Audit secret access:
- Review who has access to secrets
- Remove unused secrets
- Monitor secret usage logs
Troubleshooting
Secret Not Found in Worker
Symptom: env.SECRET_KEY is undefined
Cause: Secret not set or wrong environment
Fix:
# List secrets to verify
wrangler secret list --env production
# Set if missing
wrangler secret put SECRET_KEY --env productionSecret Exposed in Logs
Symptom: Secret value visible in CI logs
Cause: Secret echoed or printed
Fix: Remove all echo or console.log of secret values
Deployment Fails with "Invalid API token"
Cause: Token expired or lacks permissions
Fix: 1. Generate new token: https://dash.cloudflare.com/profile/api-tokens 2. Update GitHub/GitLab secret 3. Redeploy
Resources
- Wrangler Secrets: https://developers.cloudflare.com/workers/wrangler/commands/#secret
- GitHub Secrets: https://docs.github.com/actions/security-guides/encrypted-secrets
- GitLab Variables: https://docs.gitlab.com/ee/ci/variables/
#!/bin/bash
# Deployment Verification Script for Cloudflare Workers
#
# Features:
# - Health check with configurable retries
# - Response time measurement
# - Expected response validation
# - Exit codes for CI/CD integration
#
# Usage:
# ./verify-deployment.sh https://my-worker.workers.dev
# ./verify-deployment.sh https://my-worker.workers.dev --retries 10 --delay 5
# ./verify-deployment.sh https://my-worker.workers.dev --expect-status 200 --expect-body "ok"
set -e
# Default configuration
WORKER_URL=""
MAX_RETRIES=5
RETRY_DELAY=5
EXPECTED_STATUS=200
EXPECTED_BODY=""
TIMEOUT=10
HEALTH_ENDPOINT="/health"
VERBOSE=false
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--retries)
MAX_RETRIES="$2"
shift 2
;;
--delay)
RETRY_DELAY="$2"
shift 2
;;
--expect-status)
EXPECTED_STATUS="$2"
shift 2
;;
--expect-body)
EXPECTED_BODY="$2"
shift 2
;;
--timeout)
TIMEOUT="$2"
shift 2
;;
--health-endpoint)
HEALTH_ENDPOINT="$2"
shift 2
;;
--verbose|-v)
VERBOSE=true
shift
;;
--help|-h)
echo "Usage: $0 <worker-url> [options]"
echo ""
echo "Options:"
echo " --retries N Number of retry attempts (default: 5)"
echo " --delay N Seconds between retries (default: 5)"
echo " --expect-status N Expected HTTP status code (default: 200)"
echo " --expect-body TEXT Expected text in response body"
echo " --timeout N Request timeout in seconds (default: 10)"
echo " --health-endpoint Health check endpoint (default: /health)"
echo " --verbose, -v Verbose output"
echo " --help, -h Show this help"
echo ""
echo "Examples:"
echo " $0 https://my-worker.workers.dev"
echo " $0 https://my-worker.workers.dev --retries 10 --delay 3"
echo " $0 https://my-worker.workers.dev --expect-body 'healthy'"
exit 0
;;
-*)
echo "Unknown option: $1"
exit 1
;;
*)
WORKER_URL="$1"
shift
;;
esac
done
# Validate URL
if [ -z "$WORKER_URL" ]; then
echo -e "${RED}Error: Worker URL is required${NC}"
echo "Usage: $0 <worker-url> [options]"
exit 1
fi
# Construct full URL
FULL_URL="${WORKER_URL}${HEALTH_ENDPOINT}"
echo "========================================"
echo "Deployment Verification"
echo "========================================"
echo "URL: $FULL_URL"
echo "Expected Status: $EXPECTED_STATUS"
[ -n "$EXPECTED_BODY" ] && echo "Expected Body: $EXPECTED_BODY"
echo "Max Retries: $MAX_RETRIES"
echo "Retry Delay: ${RETRY_DELAY}s"
echo "Timeout: ${TIMEOUT}s"
echo "========================================"
echo ""
# Verification function
verify() {
local attempt=$1
echo -e "${YELLOW}Attempt $attempt/$MAX_RETRIES...${NC}"
# Make request and capture response
local start_time=$(date +%s%N)
RESPONSE=$(curl -s -w "\n%{http_code}\n%{time_total}" \
--max-time "$TIMEOUT" \
"$FULL_URL" 2>/dev/null) || {
echo -e "${RED} ✗ Connection failed${NC}"
return 1
}
local end_time=$(date +%s%N)
# Parse response
local body=$(echo "$RESPONSE" | head -n -2)
local status=$(echo "$RESPONSE" | tail -n 2 | head -n 1)
local time=$(echo "$RESPONSE" | tail -n 1)
if $VERBOSE; then
echo " Response body: $body"
echo " Status code: $status"
echo " Response time: ${time}s"
fi
# Check status code
if [ "$status" != "$EXPECTED_STATUS" ]; then
echo -e "${RED} ✗ Status mismatch: got $status, expected $EXPECTED_STATUS${NC}"
return 1
fi
echo -e "${GREEN} ✓ Status: $status${NC}"
# Check body if expected
if [ -n "$EXPECTED_BODY" ]; then
if echo "$body" | grep -q "$EXPECTED_BODY"; then
echo -e "${GREEN} ✓ Body contains: $EXPECTED_BODY${NC}"
else
echo -e "${RED} ✗ Body does not contain: $EXPECTED_BODY${NC}"
return 1
fi
fi
echo -e "${GREEN} ✓ Response time: ${time}s${NC}"
return 0
}
# Run verification with retries
for i in $(seq 1 $MAX_RETRIES); do
if verify $i; then
echo ""
echo "========================================"
echo -e "${GREEN}✅ Deployment verification PASSED${NC}"
echo "========================================"
exit 0
fi
if [ $i -lt $MAX_RETRIES ]; then
echo " Waiting ${RETRY_DELAY}s before retry..."
sleep $RETRY_DELAY
fi
done
echo ""
echo "========================================"
echo -e "${RED}❌ Deployment verification FAILED${NC}"
echo " after $MAX_RETRIES attempts"
echo "========================================"
exit 1
# Complete Production-Ready GitHub Actions Workflow for Cloudflare Workers
#
# Features:
# - Multi-environment deployments (staging, production)
# - Preview deployments for PRs
# - Automated testing with coverage
# - Deployment verification
# - Manual approval gates for production
# - Slack notifications
#
# Usage:
# 1. Copy to .github/workflows/deploy.yml
# 2. Add secrets: CLOUDFLARE_API_TOKEN, DATABASE_URL, SLACK_WEBHOOK
# 3. Update worker name and URLs
# 4. Configure GitHub environments (staging, production)
name: Deploy Workers
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
WORKER_NAME: my-worker
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Dependencies
run: bun install
- name: Run Linter
run: bun run lint
- name: Type Check
run: bun run type-check
- name: Run Tests
run: bun test --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
deploy-staging:
name: Deploy to Staging
needs: test
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment:
name: staging
url: https://${{ env.WORKER_NAME }}-staging.workers.dev
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Deploy to Staging
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env staging
- name: Set Secrets
run: |
echo "${{ secrets.DATABASE_URL_STAGING }}" | \
bunx wrangler secret put DATABASE_URL --env staging
- name: Verify Deployment
run: |
sleep 5
curl -f https://${{ env.WORKER_NAME }}-staging.workers.dev/health || exit 1
deploy-production:
name: Deploy to Production
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production # Requires manual approval
url: https://${{ env.WORKER_NAME }}.workers.dev
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run build
- name: Deploy to Production
id: deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production
- name: Set Secrets
run: |
echo "${{ secrets.DATABASE_URL_PRODUCTION }}" | \
bunx wrangler secret put DATABASE_URL --env production
- name: Verify Deployment
id: verify
run: |
sleep 5
curl -f https://${{ env.WORKER_NAME }}.workers.dev/health || exit 1
- name: Notify Success
if: success()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "✅ Production deployment successful: ${{ github.sha }}"
}
- name: Notify Failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "❌ Production deployment failed: ${{ github.sha }}"
}
preview:
name: Preview Deployment
needs: test
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
deployments: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Deploy Preview
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env preview-${{ github.event.number }}
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
const previewUrl = `https://${{ env.WORKER_NAME }}-preview-${{ github.event.number }}.workers.dev`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `✅ **Preview Deployed**\\n\\n🔗 ${previewUrl}\\n\\nCommit: ${context.sha.substring(0, 7)}`
});
# Complete Production-Ready GitLab CI Pipeline for Cloudflare Workers
#
# Features:
# - Multi-environment deployments (staging, production)
# - Review apps (preview deployments for merge requests)
# - Automated testing with coverage
# - Deployment verification
# - Manual approval for production
#
# Usage:
# 1. Copy to .gitlab-ci.yml at repository root
# 2. Add CI/CD variables: CLOUDFLARE_API_TOKEN, DATABASE_URL
# 3. Update worker name and URLs
# 4. Configure GitLab environments (staging, production)
image: oven/bun:latest
stages:
- test
- deploy
- cleanup
variables:
FF_USE_FASTZIP: "true"
WORKER_NAME: my-worker
cache:
key:
files:
- bun.lockb
paths:
- node_modules/
- .bun/
# =====================
# Testing
# =====================
test:
stage: test
script:
- bun install
- bun run lint
- bun run type-check
- bun test --coverage
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
expire_in: 1 week
# =====================
# Staging Deployment
# =====================
deploy-staging:
stage: deploy
script:
- bun install
- bunx wrangler deploy --env staging
environment:
name: staging
url: https://$WORKER_NAME-staging.workers.dev
on_stop: stop-staging
only:
- develop
dependencies:
- test
.set-staging-secrets: &set-staging-secrets
- echo "$DATABASE_URL_STAGING" | bunx wrangler secret put DATABASE_URL --env staging
- echo "$STRIPE_SECRET_KEY_STAGING" | bunx wrangler secret put STRIPE_SECRET_KEY --env staging
deploy-staging-with-secrets:
stage: deploy
script:
- bun install
- *set-staging-secrets
- bunx wrangler deploy --env staging
environment:
name: staging
url: https://$WORKER_NAME-staging.workers.dev
only:
- develop
dependencies:
- test
after_script:
- sleep 5
- curl -f https://$WORKER_NAME-staging.workers.dev/health || exit 1
# =====================
# Production Deployment
# =====================
deploy-production:
stage: deploy
script:
- bun install
- bun run build
- bunx wrangler deploy --env production
environment:
name: production
url: https://$WORKER_NAME.workers.dev
only:
- main
when: manual # Requires manual trigger
dependencies:
- test
.set-production-secrets: &set-production-secrets
- echo "$DATABASE_URL_PRODUCTION" | bunx wrangler secret put DATABASE_URL --env production
- echo "$STRIPE_SECRET_KEY_PRODUCTION" | bunx wrangler secret put STRIPE_SECRET_KEY --env production
deploy-production-with-secrets:
stage: deploy
script:
- bun install
- bun run build
- *set-production-secrets
- bunx wrangler deploy --env production
environment:
name: production
url: https://$WORKER_NAME.workers.dev
only:
- main
when: manual
dependencies:
- test
after_script:
- sleep 5
- curl -f https://$WORKER_NAME.workers.dev/health || exit 1
# =====================
# Review Apps (Preview Deployments)
# =====================
review:
stage: deploy
script:
- bun install
- bunx wrangler deploy --env review-$CI_MERGE_REQUEST_IID
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$WORKER_NAME-review-$CI_MERGE_REQUEST_IID.workers.dev
on_stop: stop-review
only:
- merge_requests
dependencies:
- test
stop-review:
stage: cleanup
variables:
GIT_STRATEGY: none
script:
- bunx wrangler delete --name $WORKER_NAME-review-$CI_MERGE_REQUEST_IID
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
when: manual
only:
- merge_requests
# =====================
# Conditional Deployment
# =====================
deploy-on-changes:
stage: deploy
script:
- bun install
- bunx wrangler deploy
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
- if: '$CI_COMMIT_TAG'
when: never
- changes:
- src/**/*
- wrangler.jsonc
when: on_success
# =====================
# Multi-Worker Deployment
# =====================
deploy-api:
stage: deploy
script:
- cd workers/api
- bun install
- bunx wrangler deploy
only:
- main
deploy-frontend:
stage: deploy
script:
- cd workers/frontend
- bun install
- bunx wrangler deploy
only:
- main
# =====================
# Verification
# =====================
verify-staging:
stage: deploy
needs: ["deploy-staging"]
script:
- |
echo "Verifying staging deployment..."
MAX_RETRIES=5
RETRY_DELAY=5
for i in $(seq 1 $MAX_RETRIES); do
echo "Health check attempt $i/$MAX_RETRIES..."
if curl -f "https://$WORKER_NAME-staging.workers.dev/health"; then
echo "✅ Health check passed"
exit 0
fi
if [ $i -lt $MAX_RETRIES ]; then
echo "❌ Health check failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
fi
done
echo "❌ Health check failed after $MAX_RETRIES attempts"
exit 1
only:
- develop
verify-production:
stage: deploy
needs: ["deploy-production"]
script:
- |
echo "Verifying production deployment..."
sleep 10
curl -f "https://$WORKER_NAME.workers.dev/health" || exit 1
echo "✅ Production deployment verified"
only:
- main
# Preview Deployment Workflow for Cloudflare Workers
#
# Purpose: Deploy Workers preview environments for pull requests
# Features:
# - Automatic preview deployment on PR open/update
# - PR comment with preview URL
# - Automatic cleanup on PR close
# - Deployment status checks
#
# Usage:
# 1. Copy to .github/workflows/preview.yml
# 2. Add secret: CLOUDFLARE_API_TOKEN
# 3. Update WORKER_NAME
# 4. Ensure GitHub App has permissions: contents: read, pull-requests: write, deployments: write
name: Preview Deployment
on:
pull_request:
types: [opened, synchronize, reopened, closed]
env:
WORKER_NAME: my-worker
jobs:
# =====================
# Deploy Preview
# =====================
deploy-preview:
name: Deploy Preview
if: github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
deployments: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Dependencies
run: bun install
- name: Run Tests
run: bun test
- name: Deploy Preview
id: deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env preview-${{ github.event.number }}
- name: Get Preview URL
id: preview-url
run: |
PREVIEW_URL="https://${{ env.WORKER_NAME }}-preview-${{ github.event.number }}.workers.dev"
echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT
- name: Verify Preview Deployment
run: |
sleep 5
curl -f ${{ steps.preview-url.outputs.url }}/health || echo "Warning: Health check failed"
- name: Comment PR with Preview URL
uses: actions/github-script@v7
with:
script: |
const previewUrl = '${{ steps.preview-url.outputs.url }}';
const commitSha = context.sha.substring(0, 7);
const prNumber = context.issue.number;
// Find existing preview comment
const comments = await github.rest.issues.listComments({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.data.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Preview Deployment')
);
const body = `## ✅ Preview Deployment
🔗 **Preview URL**: ${previewUrl}
📦 **Commit**: \`${commitSha}\`
⏰ **Deployed**: ${new Date().toUTCString()}
---
<sub>This preview will be automatically cleaned up when the PR is closed.</sub>`;
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
}
- name: Create Deployment Status
uses: actions/github-script@v7
if: always()
with:
script: |
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha,
environment: 'preview-${{ github.event.number }}',
auto_merge: false,
required_contexts: [],
});
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: '${{ job.status }}' === 'success' ? 'success' : 'failure',
environment_url: '${{ steps.preview-url.outputs.url }}',
});
# =====================
# Cleanup Preview
# =====================
cleanup-preview:
name: Cleanup Preview
if: github.event.action == 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Delete Preview Worker
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: delete --name ${{ env.WORKER_NAME }}-preview-${{ github.event.number }}
continue-on-error: true
- name: Comment PR
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 deployment cleaned up.',
});
# =====================
# Advanced: Multi-Region Preview
# =====================
deploy-preview-multi-region:
name: Deploy Preview (Multi-Region)
if: github.event.action != 'closed' && false # Disabled by default
runs-on: ubuntu-latest
strategy:
matrix:
region: [us, eu, apac]
permissions:
contents: read
pull-requests: write
deployments: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install Dependencies
run: bun install
- name: Deploy Preview to ${{ matrix.region }}
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env preview-${{ github.event.number }}-${{ matrix.region }}
- name: Get Regional Preview URL
id: preview-url
run: |
echo "url=https://${{ env.WORKER_NAME }}-preview-${{ github.event.number }}-${{ matrix.region }}.workers.dev" >> $GITHUB_OUTPUT
- name: Comment Regional Preview URL
uses: actions/github-script@v7
if: matrix.region == 'us' # Only comment once
with:
script: |
const regions = ['us', 'eu', 'apac'];
const prNumber = context.issue.number;
const workerName = '${{ env.WORKER_NAME }}';
const urls = regions.map(region =>
`- **${region.toUpperCase()}**: https://${workerName}-preview-${prNumber}-${region}.workers.dev`
).join('\n');
github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ✅ Multi-Region Preview Deployment\n\n${urls}\n\n📦 **Commit**: \`${context.sha.substring(0, 7)}\``,
});
# Rollback Workflow for Cloudflare Workers
#
# Purpose: Safely rollback Workers deployments to previous versions
# Features:
# - Manual rollback trigger with version/commit selection
# - Automatic rollback on deployment verification failure
# - Support for environment-specific rollbacks
# - Health check verification after rollback
# - Slack/Discord notifications
#
# Usage:
# 1. Copy to .github/workflows/rollback.yml
# 2. Add secrets: CLOUDFLARE_API_TOKEN, SLACK_WEBHOOK (optional)
# 3. Update WORKER_NAME
# 4. Configure GitHub environments with protection rules
name: Rollback Deployment
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to rollback'
required: true
type: choice
options:
- production
- staging
version:
description: 'Git commit SHA or tag to rollback to'
required: true
type: string
reason:
description: 'Reason for rollback'
required: false
type: string
env:
WORKER_NAME: my-worker
jobs:
# =====================
# Validate Rollback Request
# =====================
validate:
name: Validate Rollback
runs-on: ubuntu-latest
outputs:
commit-exists: ${{ steps.check.outputs.exists }}
commit-sha: ${{ steps.check.outputs.sha }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history
- name: Validate Commit
id: check
run: |
VERSION="${{ github.event.inputs.version }}"
# Check if version is a valid commit SHA or tag
if git rev-parse --verify "$VERSION" >/dev/null 2>&1; then
COMMIT_SHA=$(git rev-parse "$VERSION")
echo "exists=true" >> $GITHUB_OUTPUT
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "✅ Valid commit/tag: $VERSION ($COMMIT_SHA)"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "❌ Invalid commit/tag: $VERSION"
exit 1
fi
- name: Get Commit Info
run: |
git log -1 --format="%h - %s (%an, %ar)" ${{ steps.check.outputs.sha }}
# =====================
# Rollback Deployment
# =====================
rollback:
name: Rollback ${{ github.event.inputs.environment }}
needs: validate
runs-on: ubuntu-latest
environment:
name: ${{ github.event.inputs.environment }}-rollback
url: https://${{ env.WORKER_NAME }}${{ github.event.inputs.environment == 'staging' && '-staging' || '' }}.workers.dev
steps:
- name: Checkout Target Version
uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.commit-sha }}
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Dependencies
run: bun install
- name: Build
run: bun run build
continue-on-error: true
- name: Deploy Rollback
id: deploy
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env ${{ github.event.inputs.environment }}
- name: Verify Rollback
id: verify
run: |
ENV="${{ github.event.inputs.environment }}"
if [ "$ENV" = "staging" ]; then
WORKER_URL="https://${{ env.WORKER_NAME }}-staging.workers.dev"
else
WORKER_URL="https://${{ env.WORKER_NAME }}.workers.dev"
fi
echo "Verifying rollback at $WORKER_URL..."
sleep 5
MAX_RETRIES=3
for i in $(seq 1 $MAX_RETRIES); do
if curl -f "$WORKER_URL/health"; then
echo "✅ Rollback verified"
exit 0
fi
echo "Attempt $i/$MAX_RETRIES failed, retrying..."
sleep 5
done
echo "❌ Rollback verification failed"
exit 1
- name: Create Rollback Annotation
if: success()
run: |
git tag -a "rollback-${{ github.event.inputs.environment }}-$(date +%Y%m%d-%H%M%S)" \
-m "Rollback to ${{ needs.validate.outputs.commit-sha }}" \
-m "Environment: ${{ github.event.inputs.environment }}" \
-m "Reason: ${{ github.event.inputs.reason }}" \
${{ needs.validate.outputs.commit-sha }}
- name: Notify Success
if: success()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "✅ Rollback successful",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Rollback Successful*\n\n*Environment:* ${{ github.event.inputs.environment }}\n*Version:* `${{ needs.validate.outputs.commit-sha }}`\n*Triggered by:* ${{ github.actor }}\n*Reason:* ${{ github.event.inputs.reason }}"
}
}
]
}
- name: Notify Failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "❌ Rollback failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Rollback Failed*\n\n*Environment:* ${{ github.event.inputs.environment }}\n*Version:* `${{ needs.validate.outputs.commit-sha }}`\n*Triggered by:* ${{ github.actor }}"
}
}
]
}
# =====================
# Automatic Rollback on Failure
# =====================
auto-rollback:
name: Auto Rollback
if: false # Disabled by default - enable per environment
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Get Previous Commit
id: previous
run: |
PREVIOUS_SHA=$(git rev-parse HEAD~1)
echo "sha=$PREVIOUS_SHA" >> $GITHUB_OUTPUT
echo "Previous commit: $PREVIOUS_SHA"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install Dependencies
run: bun install
- name: Deploy Previous Version
uses: cloudflare/wrangler-action@v4
with:
api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: deploy --env production
- name: Verify Auto Rollback
run: |
sleep 5
curl -f https://${{ env.WORKER_NAME }}.workers.dev/health || exit 1
# =====================
# Rollback with Version Selection from History
# =====================
# This can be triggered to show recent deployments and select one
---
name: Interactive Rollback
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment'
required: true
type: choice
options:
- production
- staging
jobs:
list-versions:
name: List Recent Deployments
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: List Recent Commits
run: |
echo "## Recent Deployments (Last 10 commits)"
git log -10 --oneline --decorate
echo ""
echo "To rollback, run the 'Rollback Deployment' workflow with one of these commit SHAs"
# =====================
# Blue-Green Rollback (switch traffic)
# =====================
# For blue-green deployments, rollback by switching route
---
name: Blue-Green Rollback
on:
workflow_dispatch:
inputs:
target:
description: 'Target environment (blue/green)'
required: true
type: choice
options:
- blue
- green
jobs:
switch-traffic:
name: Switch to ${{ github.event.inputs.target }}
runs-on: ubuntu-latest
environment:
name: production-rollback
steps:
- name: Switch Route to ${{ github.event.inputs.target }}
run: |
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/${{ secrets.ZONE_ID }}/workers/routes/${{ secrets.ROUTE_ID }}" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type: application/json" \
--data "{\"script\":\"${{ env.WORKER_NAME }}-${{ github.event.inputs.target }}\"}"
- name: Verify Traffic Switch
run: |
sleep 10
curl -f https://${{ env.WORKER_NAME }}.workers.dev/health || exit 1
echo "✅ Traffic switched to ${{ github.event.inputs.target }}"