GitHub Actions with Cloudflare
Cloudflare Pages vs Workers Static Assets
Section titled “Cloudflare Pages vs Workers Static Assets”Cloudflare offers two approaches for deploying static sites with optional server-side functionality:
When to Use Each Approach
Section titled “When to Use Each Approach”| Use Case | Recommendation |
|---|---|
| Simple static site or blog | Pages - easier setup, free preview deployments |
| Documentation site (like this one) | Pages - works great with Astro/Starlight |
| Need Durable Objects | Workers Static Assets - required |
| Need Cron Triggers | Workers Static Assets - required |
| Need Logpush/Tail Workers | Workers Static Assets - required |
| Multi-platform deployment | Workers Static Assets - more flexible routing |
| Custom domain routing logic | Workers Static Assets - routes array in config |
Compatibility Matrix (Key Differences)
Section titled “Compatibility Matrix (Key Differences)”| Feature | Pages | Workers 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.
Cloudflare Pages Workflow (This Site)
Section titled “Cloudflare Pages Workflow (This Site)”Workflow Overview
Section titled “Workflow Overview”The workflow runs on:
- Push to
mainbranch - Pull requests to
mainbranch - Manual trigger via
workflow_dispatch
Only deployments to production occur on main branch pushes or manual triggers.
Workflow Configuration
Section titled “Workflow Configuration”Concurrency Control
Section titled “Concurrency Control”concurrency: group: "pages" cancel-in-progress: trueEnsures only one deployment runs at a time, cancelling any in-progress deployments when a new one starts.
Job Environment
Section titled “Job Environment”- Runner:
ubuntu-latest - Package Manager: Bun
- Diagram Rendering: D2 CLI
- Deployment Target: Cloudflare Pages
Step-by-Step Breakdown
Section titled “Step-by-Step Breakdown”1. Repository Checkout
Section titled “1. Repository Checkout”- name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 2- Downloads repository code
fetch-depth: 2fetches the last 2 commits (needed for git information extraction)
2. Bun Environment Setup
Section titled “2. Bun Environment Setup”- name: Setup Bun environment uses: oven-sh/setup-bun@v1- Installs and configures Bun runtime
- Uses the official Bun setup action
3. Dependency Caching
Section titled “3. Dependency Caching”Three separate caching layers optimize build times:
Bun Dependencies Cache
Section titled “Bun Dependencies Cache”- 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_modulesand Bun’s internal cache - Cache key includes OS, lockfile hash, and package.json hash
- Invalidates when dependencies change
D2 CLI Cache
Section titled “D2 CLI Cache”- 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
Astro Build Cache
Section titled “Astro Build Cache”- 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
4. Dependency Installation
Section titled “4. Dependency Installation”- name: Install project dependencies run: bun install- Installs all project dependencies using Bun
- Benefits from dependency caching if available
5. D2 CLI Installation
Section titled “5. D2 CLI Installation”- 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-d2plugin during Astro build
6. Build with Retry Logic
Section titled “6. Build with Retry Logic”- 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
7. Git Information Extraction
Section titled “7. Git Information Extraction”- 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_OUTPUTOnly 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
8. Cloudflare Pages Deployment
Section titled “8. Cloudflare Pages Deployment”- 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 deploycommand - 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.
Project Configuration
Section titled “Project Configuration”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 (likedist)routes: Explicit custom domain configuration
Deploy Command
Section titled “Deploy Command”- 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"Cache Purge (Required for Proxied Zones)
Section titled “Cache Purge (Required for Proxied Zones)”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"]}'Required Secrets
Section titled “Required Secrets”Configure these secrets in your GitHub repository settings:
For Cloudflare Pages
Section titled “For Cloudflare Pages”| Secret Name | Description |
|---|---|
CLOUDFLARE_WRANGLER_TOKEN | Cloudflare API token with Pages:Edit permissions |
CLOUDFLARE_ACCOUNT_ID | Your Cloudflare account ID |
Additional for Workers Static Assets
Section titled “Additional for Workers Static Assets”| Secret Name | Description |
|---|---|
CLOUDFLARE_ZONE_ID | Zone ID for your domain (for cache purge) |
CLOUDFLARE_CACHE_PURGE_TOKEN | API token with Zone:Cache Purge permissions |
Full Workflow Examples
Section titled “Full Workflow Examples”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: bunname: Build and Deploy to Workers
on: push: branches: [main] pull_request: branches: [main]
jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4
- name: Setup Bun environment uses: oven-sh/setup-bun@v1
- name: Cache dependencies uses: actions/cache@v4 with: path: | ~/.bun/install/cache node_modules key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb', '**/package.json') }} restore-keys: | ${{ runner.os }}-bun-
- name: Install project dependencies run: bun install
- name: Build project run: bun run build
- name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist retention-days: 7
deploy: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4
- name: Setup Bun environment uses: oven-sh/setup-bun@v1
- name: Install dependencies run: bun install
- name: Download built artifacts uses: actions/download-artifact@v4 with: name: dist path: dist
- name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_WRANGLER_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} command: deploy packageManager: bun
purge-cache: needs: deploy if: success() runs-on: ubuntu-latest steps: - name: Purge Cloudflare Cache 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"]}'Migrating from Pages to Workers
Section titled “Migrating from Pages to Workers”If you need to migrate an existing Pages project to Workers Static Assets:
- Create
wrangler.jsoncwith your asset directory and routes - Update workflow to use
wrangler deployinstead ofwrangler pages deploy - Add cache purge step if using proxied zones
- Update DNS or configure custom domains via the
routesarray
For complete migration guidance, see Cloudflare’s migration guide.