Skip to content

GitHub Actions with Cloudflare


Cloudflare offers two approaches for deploying static sites with optional server-side functionality:

d2 diagram
Use CaseRecommendation
Simple static site or blogPages - easier setup, free preview deployments
Documentation site (like this one)Pages - works great with Astro/Starlight
Need Durable ObjectsWorkers Static Assets - required
Need Cron TriggersWorkers Static Assets - required
Need Logpush/Tail WorkersWorkers Static Assets - required
Multi-platform deploymentWorkers Static Assets - more flexible routing
Custom domain routing logicWorkers Static Assets - routes array in config
FeaturePagesWorkers Static Assets
Static asset serving
Server-side functions✅ (Pages Functions)✅ (Worker script)
Preview deployments⏳ (coming soon)
Durable Objects
Cron Triggers
Logpush
Tail Workers
Queue consumers
*.pages.dev subdomain
*.workers.dev subdomain
Cache purge needed🟡 (for proxied zones)

For the complete matrix, see Cloudflare’s official documentation.


d2 diagram

The workflow runs on:

  • Push to main branch
  • Pull requests to main branch
  • Manual trigger via workflow_dispatch

Only deployments to production occur on main branch pushes or manual triggers.

concurrency:
group: "pages"
cancel-in-progress: true

Ensures only one deployment runs at a time, cancelling any in-progress deployments when a new one starts.

  • Runner: ubuntu-latest
  • Package Manager: Bun
  • Diagram Rendering: D2 CLI
  • Deployment Target: Cloudflare Pages
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
  • Downloads repository code
  • fetch-depth: 2 fetches the last 2 commits (needed for git information extraction)
- name: Setup Bun environment
uses: oven-sh/setup-bun@v1
  • Installs and configures Bun runtime
  • Uses the official Bun setup action

Three separate caching layers optimize build times:

- name: Cache Bun dependencies
uses: actions/cache@v4
id: bun-cache
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-
${{ runner.os }}-bun-
  • Caches node_modules and Bun’s internal cache
  • Cache key includes OS, lockfile hash, and package.json hash
  • Invalidates when dependencies change
- name: Cache D2 CLI
uses: actions/cache@v4
id: d2-cache
with:
path: ~/.local
key: ${{ runner.os }}-d2-v0.7.1
restore-keys: |
${{ runner.os }}-d2-
  • Caches D2 CLI binary for diagram rendering
  • Version-pinned cache key ensures updates when D2 version changes
  • Prevents re-downloading on each run
- name: Cache Astro build
uses: actions/cache@v4
with:
path: |
.astro
dist
key: ${{ runner.os }}-astro-${{ github.sha }}
restore-keys: |
${{ runner.os }}-astro-
  • Caches Astro’s internal cache and build output
  • Uses commit SHA as key for precise cache invalidation
- name: Install project dependencies
run: bun install
  • Installs all project dependencies using Bun
  • Benefits from dependency caching if available
- name: Install D2 CLI for diagram rendering
if: steps.d2-cache.outputs.cache-hit != 'true'
run: curl -fsSL https://d2lang.com/install.sh | sh
  • Only runs if D2 cache missed
  • Installs D2 CLI needed for rendering diagrams in MDX files
  • D2 is used by the remark-d2 plugin during Astro build
- name: Build project with retry
env:
MAX_ATTEMPTS: 3
RETRY_INTERVAL: 30
PATH: ${{ github.workspace }}/.local/bin:~/.local/bin:/usr/local/bin:/usr/bin:/bin
run: |
export PATH="$HOME/.local/bin:$PATH"
d2 --version || { echo "D2 CLI not found"; exit 1; }
attempt=1
until bun run build || [ $attempt -eq $MAX_ATTEMPTS ]; do
echo "Build attempt $attempt failed. Retrying in $RETRY_INTERVAL seconds..."
sleep $RETRY_INTERVAL
attempt=$((attempt + 1))
done
if [ $attempt -eq $MAX_ATTEMPTS ] && ! bun run build; then
echo "Build failed after $MAX_ATTEMPTS attempts."
exit 1
fi
  • Ensures D2 is in PATH for diagram rendering
  • Verifies D2 CLI is available before building
  • Implements retry mechanism for transient build failures
  • Attempts build up to 3 times with 30-second delays
- name: Get git information
id: git-info
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
run: |
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "COMMIT_MESSAGE=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT
echo "BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT
echo "COMMIT_DIRTY=$(if [[ -n $(git status -s) ]]; then echo true; else echo false; fi)" >> $GITHUB_OUTPUT

Only runs for deployments. Extracts:

  • COMMIT_HASH: Full commit SHA
  • COMMIT_MESSAGE: Last commit subject line (avoids multiline issues with wrangler)
  • BRANCH_NAME: Current branch name
  • COMMIT_DIRTY: Whether working directory has uncommitted changes
- name: Deploy to Cloudflare Pages
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_WRANGLER_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=erfi-dev-docs --commit-hash "${{ steps.git-info.outputs.COMMIT_HASH }}" --commit-message "${{ steps.git-info.outputs.COMMIT_MESSAGE }}" --branch "${{ steps.git-info.outputs.BRANCH_NAME }}" --commit-dirty ${{ steps.git-info.outputs.COMMIT_DIRTY }}
wranglerVersion: '4.28.1'
packageManager: bun
  • Deploys built assets to Cloudflare Pages
  • Uses wrangler pages deploy command
  • Passes git information for deployment metadata
  • Version-pinned Wrangler for reproducibility

Workers Static Assets Workflow (Alternative)

Section titled “Workers Static Assets Workflow (Alternative)”

For projects requiring Durable Objects, Cron Triggers, or other Workers-only features, use the Workers Static Assets approach.

d2 diagram

Workers Static Assets requires a wrangler.jsonc (or wrangler.toml) configuration file:

{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "my-site",
"compatibility_date": "2025-12-06",
"main": "src/index.ts", // Optional: Worker script
"assets": {
"directory": "./dist"
},
"routes": [
{
"pattern": "example.com",
"custom_domain": true
},
{
"pattern": "www.example.com",
"custom_domain": true
}
]
}

Key differences from Pages:

  • main: Optional entry point for Worker script (handle API routes, etc.)
  • assets.directory: Points to your build output (like dist)
  • routes: Explicit custom domain configuration
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_WRANGLER_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy # Note: not "pages deploy"
packageManager: bun
wranglerVersion: "4.53.0"

Unlike Pages, Workers deployments to proxied zones may require manual cache purging:

- name: Purge Cloudflare Cache
if: success()
run: |
curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/purge_cache" \
-H "Authorization: Bearer ${{ secrets.CLOUDFLARE_CACHE_PURGE_TOKEN }}" \
-H "Content-Type: application/json" \
--data '{"hosts":["example.com", "www.example.com"]}'

Configure these secrets in your GitHub repository settings:

Secret NameDescription
CLOUDFLARE_WRANGLER_TOKENCloudflare API token with Pages:Edit permissions
CLOUDFLARE_ACCOUNT_IDYour Cloudflare account ID
Secret NameDescription
CLOUDFLARE_ZONE_IDZone ID for your domain (for cache purge)
CLOUDFLARE_CACHE_PURGE_TOKENAPI token with Zone:Cache Purge permissions

name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Bun environment
uses: oven-sh/setup-bun@v1
- name: Cache Bun dependencies
uses: actions/cache@v4
id: bun-cache
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-
${{ runner.os }}-bun-
- name: Cache D2 CLI
uses: actions/cache@v4
id: d2-cache
with:
path: ~/.local
key: ${{ runner.os }}-d2-v0.7.1
restore-keys: |
${{ runner.os }}-d2-
- name: Cache Astro build
uses: actions/cache@v4
with:
path: |
.astro
dist
key: ${{ runner.os }}-astro-${{ github.sha }}
restore-keys: |
${{ runner.os }}-astro-
- name: Install project dependencies
run: bun install
- name: Install D2 CLI for diagram rendering
if: steps.d2-cache.outputs.cache-hit != 'true'
run: curl -fsSL https://d2lang.com/install.sh | sh
- name: Build project with retry
env:
MAX_ATTEMPTS: 3
RETRY_INTERVAL: 30
run: |
export PATH="$HOME/.local/bin:$PATH"
d2 --version || { echo "D2 CLI not found"; exit 1; }
attempt=1
until bun run build || [ $attempt -eq $MAX_ATTEMPTS ]; do
echo "Build attempt $attempt failed. Retrying in $RETRY_INTERVAL seconds..."
sleep $RETRY_INTERVAL
attempt=$((attempt + 1))
done
if [ $attempt -eq $MAX_ATTEMPTS ] && ! bun run build; then
echo "Build failed after $MAX_ATTEMPTS attempts."
exit 1
fi
- name: Get git information
id: git-info
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
run: |
echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "COMMIT_MESSAGE=$(git log -1 --pretty=%s)" >> $GITHUB_OUTPUT
echo "BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT
echo "COMMIT_DIRTY=$(if [[ -n $(git status -s) ]]; then echo true; else echo false; fi)" >> $GITHUB_OUTPUT
- name: Deploy to Cloudflare Pages
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_WRANGLER_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=erfi-dev-docs --commit-hash "${{ steps.git-info.outputs.COMMIT_HASH }}" --commit-message "${{ steps.git-info.outputs.COMMIT_MESSAGE }}" --branch "${{ steps.git-info.outputs.BRANCH_NAME }}" --commit-dirty ${{ steps.git-info.outputs.COMMIT_DIRTY }}
wranglerVersion: '4.28.1'
packageManager: bun

If you need to migrate an existing Pages project to Workers Static Assets:

  1. Create wrangler.jsonc with your asset directory and routes
  2. Update workflow to use wrangler deploy instead of wrangler pages deploy
  3. Add cache purge step if using proxied zones
  4. Update DNS or configure custom domains via the routes array

For complete migration guidance, see Cloudflare’s migration guide.