Deploying to Cloudflare From GitHub Actions With Wrangler
Cloudflare offers two ways to put a static site on its network: Pages, the original static hosting product, and Workers with Static Assets, which serves files from a Worker and is where Cloudflare's new features land first. Both can build from a connected Git repository. Many teams prefer to build in their own CI instead, so the deploy is the last step of a pipeline that already ran the tests, link checks and performance budgets — and so a failing check stops the deploy. Wrangler, Cloudflare's CLI, makes that a few lines in a GitHub Actions workflow.
This guide sets up deploys for an Astro documentation site on Workers Static Assets from GitHub Actions: a scoped API token, production deploys from main, preview versions for pull requests, dependency and build caching, and a one-click rollback. It notes the Pages equivalents where they differ. It belongs to Cloudflare Pages Edge Caching Setup; the build side is covered in Building Astro Sites with GitHub Actions.
Prerequisites
- A static build that outputs to a directory (here
dist/). - A Cloudflare account, and a
wrangler.toml(orwrangler.jsonc) in the repository. - Permission to add GitHub Actions secrets.
Step 1: Configure the Worker
# wrangler.toml
name = "docs-site"
compatibility_date = "2026-09-01"
[assets]
directory = "./dist"
not_found_handling = "404-page"
html_handling = "auto-trailing-slash"
[[routes]]
pattern = "docs.example.com"
custom_domain = true
With only an [assets] block and no main script, the Worker serves the files directly. not_found_handling = "404-page" serves 404.html with a 404 status; html_handling controls trailing slashes. Migrating from Cloudflare Pages to Workers Static Assets covers the differences from Pages in detail.
Step 2: Create a Scoped Token
In the Cloudflare dashboard, create an API token from the "Edit Cloudflare Workers" template, then narrow it: one account, one zone. Store it and the account ID as repository secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID. Wrangler reads both from the environment. For Pages, the template is "Cloudflare Pages: Edit".
A token scoped this way can deploy this site and nothing else; a leaked global API key could change DNS for every domain on the account.
Step 3: The Workflow
name: deploy
on:
push: { branches: [main] }
pull_request:
concurrency: { group: deploy-${{ github.ref }}, cancel-in-progress: true }
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions: { contents: read, pull-requests: write, deployments: write }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version-file: .nvmrc, cache: npm }
- run: npm ci
- uses: actions/cache@v4
with: { path: node_modules/.astro, key: astro-${{ hashFiles('package-lock.json') }}-${{ github.sha }}, restore-keys: astro-${{ hashFiles('package-lock.json') }}- }
- run: npm run build
- run: npm test # link check, HTML validation, budgets
- name: Deploy production
if: github.ref == 'refs/heads/main'
run: npx wrangler@4 deploy
env: { CLOUDFLARE_API_TOKEN: '${{ secrets.CLOUDFLARE_API_TOKEN }}', CLOUDFLARE_ACCOUNT_ID: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}' }
- name: Upload preview version
if: github.event_name == 'pull_request'
id: preview
run: |
out=$(npx wrangler@4 versions upload --message "PR #${{ github.event.number }}" 2>&1)
echo "$out"
echo "url=$(echo "$out" | grep -oE 'https://[a-z0-9-]+-docs-site\.[a-z0-9-]+\.workers\.dev' | head -1)" >> "$GITHUB_OUTPUT"
env: { CLOUDFLARE_API_TOKEN: '${{ secrets.CLOUDFLARE_API_TOKEN }}', CLOUDFLARE_ACCOUNT_ID: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}' }
Pinning wrangler@4 avoids a new major version changing behaviour mid-pipeline; the concurrency group cancels an older run of the same branch, as in Concurrency Groups to Cancel Stale Builds.
Step 4: Comment the Preview URL
A short step posts or updates a comment on the pull request:
- if: github.event_name == 'pull_request' && steps.preview.outputs.url
uses: marocchino/sticky-pull-request-comment@v2
with:
header: preview
message: "Preview: ${{ steps.preview.outputs.url }}"
Preview versions on Workers are uploaded but not deployed — production traffic keeps going to the current deployment — and each has its own URL. For Pages, wrangler pages deploy dist --branch "$GITHUB_HEAD_REF" creates a branch alias URL instead. Preview URLs should not be indexed, as covered in Fixing Preview Deploys Leaking Into Search Results.
Step 5: One-Click Rollback
A separate workflow with a manual trigger lists deployments and rolls back:
name: rollback
on: workflow_dispatch
jobs:
rollback:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx wrangler@4 deployments list | head -20 && npx wrangler@4 rollback --message "rollback via Actions" -y
env: { CLOUDFLARE_API_TOKEN: '${{ secrets.CLOUDFLARE_API_TOKEN }}', CLOUDFLARE_ACCOUNT_ID: '${{ secrets.CLOUDFLARE_ACCOUNT_ID }}' }
wrangler rollback without a version ID returns to the previous deployment. It took 4 seconds end to end in testing, as described in Rolling Back a Bad Static Deploy in Under a Minute. On Pages, the equivalent is promoting an earlier deployment from the dashboard or the API.
Step 6: Verify the Deploy
After wrangler deploy, a smoke test requests a handful of URLs on the production domain and checks status codes, a build ID in a meta tag, and a few headers. A failure triggers the rollback workflow through the API and alerts the team. Post-Deploy Verification Checklists lists what to check.
Workers Static Assets or Pages?
Both products serve static files from the same network with similar performance. The differences that mattered for this site were operational:
The team chose Workers Static Assets for a new project because every setting lives in wrangler.toml and is reviewed in pull requests. An existing Pages project would need no change to use the same workflow: the build, tests and comment steps stay identical, and only the deploy and preview commands differ.
Secrets and Forks
Pull requests from forks do not receive repository secrets, so the preview step cannot authenticate for outside contributions. The workflow skips the preview upload when github.event.pull_request.head.repo.fork is true and posts a comment explaining that a maintainer can trigger a preview by applying a label, which starts a separate pull_request_target workflow that checks out the fork's code into a sandboxed build without secrets and uploads only the built artifact for a second, trusted job to deploy. Running untrusted code with deploy credentials is the one mistake this setup must never make.
Measured Impact
Compared with the previous setup, Cloudflare's Git integration building on Cloudflare's runners:
| Measure | Git integration | Actions + Wrangler | Tool |
|---|---|---|---|
| Commit to live (main) | 4 min 50 s | 2 min 20 s | Timestamps |
| Deploys with failing link checks reaching production | 3 in a quarter | 0 | Incident log |
| Preview per pull request | Branch builds, no checks | Version + checks + comment | Workflow |
| Rollback | Dashboard, ~2 min | Workflow, 4 s | Timing |
| Files uploaded per deploy (typical) | All | Changed only (~40) | Wrangler output |
Wrangler uploads only assets whose hashes are new, so a typical content change uploaded about 40 files out of 9,000.
Pitfalls & Rollback
- Global API keys in CI. Use a token scoped to one account and zone.
- Unpinned Wrangler. A major version can change commands or defaults.
- Deploying before tests. Order the steps so a failure stops the deploy.
- Indexed previews. Add noindex headers on non-production hosts.
- Rollback: the rollback workflow above; the previous deployment stays available for instant reactivation.
Conclusion
Deploying to Cloudflare with Wrangler from GitHub Actions puts the deploy at the end of the pipeline that already tests the site: a scoped token, a production deploy from main, preview versions with URLs on pull requests, and a rollback that takes seconds. For this 2,100-page site it halved the time from commit to live and stopped broken builds reaching production.
FAQ
Why deploy with Wrangler from GitHub Actions instead of Cloudflare's Git integration?
Because the build then runs in the same CI pipeline as tests, link checks and performance budgets, and the deploy happens only if they pass. The Git integration builds on Cloudflare's own runners with its own settings and cannot be gated on your checks. Wrangler from Actions also makes it easy to pin tool versions and cache dependencies.
What permissions does the Cloudflare API token need?
For Workers Static Assets, Account Workers Scripts Edit, plus Zone Workers Routes Edit if the Worker is attached to routes on a zone. For Pages, Account Cloudflare Pages Edit. Scope the token to the one account and, where possible, the one zone, and store it as a GitHub Actions secret.
How do I get a preview URL for each pull request?
With Workers, upload a new version without deploying it using wrangler versions upload, which returns a preview URL for that version. With Pages, deploy to a branch alias with wrangler pages deploy and the branch name. Post the URL to the pull request in a comment.
How do I roll back a bad deploy?
List recent deployments and roll back to a previous version with wrangler rollback for Workers, or by promoting an earlier deployment for Pages. Both switch traffic in seconds because previous versions are kept. A rollback job in the workflow, triggered manually, makes it a one-click operation.
Related
- Parent: Cloudflare Pages Edge Caching Setup — hosting static sites on Cloudflare.
- Deploying Astro to Cloudflare Workers Static Assets — the Worker configuration in depth.
- Automating Eleventy Deployments on Cloudflare Pages — the Pages flow for Eleventy.
- Securing Deploy Credentials with GitHub OIDC — credential hygiene for CI.
- Automating Preview Deploy Pipelines with GitHub Actions — previews in general.
Filed underHow-tosCloudflareGitHub Actions