Setting up a Cloudflare Workers CI/CD pipeline from scratch
Building a complete deployment pipeline for Cloudflare Workers using GitHub Actions and Wrangler CLI.
Setting up a Cloudflare Workers CI/CD pipeline from scratch
I've been deploying Workers manually for too long. Every time I made a change, I'd run wrangler publish from my terminal and hope nothing broke. That works fine for side projects, but when you're shipping to production, you need a proper CI/CD pipeline.
Here's how I set up automated deployments that actually work.
The Goal
I wanted a pipeline that:
- Runs tests on every push
- Deploys to staging automatically on main branch
- Requires manual approval for production deployments
- Handles secrets and environment variables properly
Setting Up Wrangler Authentication
First, you need to authenticate Wrangler in your CI environment. The cleanest approach is using API tokens instead of your global API key.
Go to the Cloudflare dashboard → My Profile → API Tokens → Create Token. Use the "Custom token" template with these permissions:
- Zone:Zone:Read (for your domain)
- Zone:Zone Settings:Read
- Account:Cloudflare Workers:Edit
Store this token in your GitHub repository secrets as CLOUDFLARE_API_TOKEN.
The GitHub Actions Workflow
Here's the complete workflow file I use (.github/workflows/deploy.yml):
name: Deploy Worker
on:
push:
branches: [main, staging]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run test
- run: npm run lint
deploy-staging:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/staging'
environment: staging
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy to staging
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: 'staging'
deploy-production:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy to production
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
environment: 'production'Managing Multiple Environments
The key to this setup is using Wrangler's environment support. Your wrangler.toml should define different environments:
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-15"
[env.staging]
name = "my-worker-staging"
route = "staging.myapp.com/*"
[env.production]
name = "my-worker-production"
route = "myapp.com/*"
[[env.staging.kv_namespaces]]
binding = "MY_KV"
id = "staging-kv-id"
[[env.production.kv_namespaces]]
binding = "MY_KV"
id = "production-kv-id"This gives you complete isolation between environments -- different Worker names, routes, and even different KV namespaces.
Handling Secrets
For secrets, I use Wrangler's secret management rather than trying to pass them through the workflow. Set them up once per environment:
# Staging secrets
wrangler secret put API_KEY --env staging
# Production secrets
wrangler secret put API_KEY --env productionThe deployment will automatically use the correct secrets for each environment.
Adding Manual Approval
GitHub's environment protection rules let you require manual approval for production deployments. Go to your repository settings → Environments → Create "production" environment → Add required reviewers.
Now production deployments will pause and wait for approval, while staging deployments happen automatically.
Testing the Pipeline
I use Vitest for my Worker tests since it has great TypeScript support and works well with Workers' runtime APIs:
import { describe, it, expect } from 'vitest'
import worker from '../src/index'
describe('Worker', () => {
it('responds with hello world', async () => {
const request = new Request('http://localhost/')
const response = await worker.fetch(request)
expect(response.status).toBe(200)
expect(await response.text()).toContain('Hello World')
})
})The pipeline runs these tests before any deployment, catching issues early.
What I Learned
- Environment separation is crucial -- Do not share KV namespaces or secrets between staging and production
- Manual approval saves you -- I've caught issues in staging that would have broken production
- Keep secrets in Wrangler -- It's more secure than passing them through GitHub Actions
- Test everything -- The pipeline is only as good as your test coverage
This setup has saved me from shipping broken code multiple times. The few minutes to set it up properly pays dividends when you're moving fast and need confidence in your deployments.