| name | dev-cloudflare-pages-ci-setup |
| description | Set up Cloudflare Pages deployment with GitHub Actions workflows. Use when: (1) Deploying a static site to Cloudflare Pages, (2) User says 'cloudflare pages', 'deploy to cloudflare', 'cf pages setup', (3) User wants CI/CD workflows for Cloudflare Pages with PR previews, (4) Setting up wrangler deployment pipelines. |
Cloudflare Pages CI Setup
Set up Cloudflare Pages deployment with GitHub Actions workflows for static sites. Supports production deploys, PR preview deploys, and named preview branches.
Step 1: Gather Project Info
Identify the project's build setup:
cat package.json
ls .github/workflows/ 2>/dev/null
cat wrangler.toml 2>/dev/null
Determine: package manager (pnpm/npm/yarn), build command, output directory (dist/, build/, out/), base path (root / or subpath like /pj/project-name/).
Step 2: Ask User Preferences
- Cloudflare Pages project name (used in
--project-name)
- Which workflows: Main only, Main + PR previews, Main + PR + named previews
- Base path: root
/ or specific subpath
- IFTTT notifications: yes/no
Step 3: Create Cloudflare Configuration
wrangler.toml
compatibility_date = "2024-12-01"
Add wrangler devDependency
pnpm add -D wrangler
For pnpm: add esbuild and workerd to pnpm.onlyBuiltDependencies in package.json.
public/_redirects (if using a base path)
If the site has a base path (e.g., /pj/project-name/), create public/_redirects:
/ /pj/project-name/ 302
Most static site generators (Astro, Next.js, etc.) copy public/ to output, eliminating CI-time redirect generation.
Step 4: Create Workflows
Security Best Practices (apply to all workflows)
- Explicit
permissions blocks (least privilege)
- Pass
${{ }} values via env: blocks, never inline in github-script JavaScript (prevents script injection)
- Quote all shell variable expansions:
"${GITHUB_SHA}"
- Pin wrangler version:
npm install -g wrangler@4 (or pnpm exec wrangler when node_modules available)
- Add
timeout-minutes to all jobs (build: 15, deploy: 20, notify: 5)
- Use
curl -sSf --max-time 10 for external HTTP calls
Deploy Retry (apply to all deploy steps)
Cloudflare Pages API occasionally returns transient errors (504 Gateway Timeout on /upload-token). Wrap all wrangler pages deploy commands in a bash retry loop:
- name: Deploy to Cloudflare Pages
run: |
for attempt in 1 2 3; do
echo "Deploy attempt $attempt/3..."
if wrangler pages deploy deploy \
--project-name=PROJECT_NAME \
--branch=main \
--commit-hash="${GITHUB_SHA}" \
--commit-message="Production deploy: ${GITHUB_SHA}"; then
echo "Deploy succeeded on attempt $attempt"
exit 0
fi
if [ "$attempt" -lt 3 ]; then
echo "Deploy failed, retrying in 150 seconds..."
sleep 150
fi
done
echo "Deploy failed after 3 attempts"
exit 1
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- 3 attempts, 150s (2.5 min) delay between retries
- Increase
timeout-minutes on deploy jobs to 20 (from 10) to accommodate retries
- For steps that set
GITHUB_OUTPUT (preview URLs), move the output logic inside the success branch of the if block
- Works with both
npx wrangler@4 and pnpm exec wrangler variants
Production Deploy (main-deploy.yml)
Trigger: push to main. Concurrency: production-deploy, cancel-in-progress: false.
The notify job below follows the canonical IFTTT payload contract owned by /dev-ci-ifttt-notify:
| Field | Content | Example |
|---|
value1 | <project>: <emoji> <status> | my-app: ✅ Deploy succeeded |
value2 | Run URL for tapping through | https://github.com/.../runs/123 |
value3 | (unused / empty) | "" |
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: pnpm install --frozen-lockfile
- run: pnpm build
- uses: actions/upload-artifact@v4
with: { name: dist-out, path: dist/, retention-days: 1 }
deploy:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/download-artifact@v4
{ , }
[, ]
PR Preview Deploy (pr-checks.yml)
Trigger: pull_request to main. Concurrency: per-PR, cancel-in-progress: true.
permissions:
contents: read
pull-requests: write
Build job identical to production. Preview job:
- Download artifact to
deploy/
- Deploy with
--branch="pr-${PR_NUMBER}"
- Preview URL:
https://pr-${PR_NUMBER}.PROJECT_NAME.pages.dev
- Post/update PR comment using
actions/github-script@v8 with marker <!-- cf-preview-pr -->
- Pass deploy URL via
env:: const deployUrl = process.env.DEPLOY_URL;
Named Preview Deploy (preview-deploy.yml)
Trigger: push to preview and expreview/**. Concurrency: per-branch, cancel-in-progress: true.
permissions:
contents: read
pull-requests: write
statuses: write
Single-job workflow (build + deploy in one job):
- Convert branch slashes to hyphens for deploy branch name
- Deploy directly from build output (no copy step needed)
- Use
pnpm exec wrangler (node_modules available in same job)
- Set commit status via
createCommitStatus API
- Comment on associated PR if one exists, using marker
<!-- cf-preview-branch -->
- Use distinct markers from pr-checks.yml to prevent collision
Step 5: Required Secrets
| Secret | Required | Purpose |
|---|
CLOUDFLARE_API_TOKEN | Yes | Wrangler authentication |
CLOUDFLARE_ACCOUNT_ID | Yes | Cloudflare account identifier |
IFTTT_PROD_NOTIFY | No | IFTTT webhook URL (skipped if not set) |
Creating Cloudflare API Token
- Cloudflare dashboard > My Profile > API Tokens > Create Token > Custom token
- Permissions: Account > Cloudflare Pages > Edit
- Account Resources: Include the target account
The Cloudflare Pages project is auto-created on first deploy via wrangler pages deploy.
Step 6: Verify
pnpm build
Companion Skills
/dev-blacksmith-migration — Move heavy build jobs off ubuntu-latest onto Blacksmith/cloud runners
/dev-ci-ifttt-notify — Add IFTTT webhook notifications