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.
Install to Claude Code
npx -y skills add Takazudo/claude-resources --skill dev-cloudflare-pages-ci-setup --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Dev Cloudflare Pages Ci Setup?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/takazudo-dev-cloudflare-pages-ci-setup)More formats (shields.io, HTML) on the badges page.
---
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:
```bash
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
1. **Cloudflare Pages project name** (used in `--project-name`)
2. **Which workflows**: Main only, Main + PR previews, Main + PR + named previews
3. **Base path**: root `/` or specific subpath
4. **IFTTT notifications**: yes/no
## Step 3: Create Cloudflare Configuration
### wrangler.toml
```toml
# Cloudflare Pages project configuration
compatibility_date = "2024-12-01"
```
### Add wrangler devDependency
```bash
pnpm add -D wrangler # or npm
```
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:
```yaml
- 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) | `""` |
```yaml
permissions:
contents: read
jobs:
build:
# Heavy job — candidate for a larger cloud runner; see /dev-blacksmith-migration
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
# fetch-depth: 0 if project needs git history (e.g., doc history, changelogs)
- 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
with: { name: dist-out, path: deploy/ }
- run: npm install -g wrangler@4
- name: Deploy to Cloudflare Pages (production)
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 }}
notify: # Optional IFTTT notification
needs: [build, deploy]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Notify via IFTTT
if: env.IFTTT_PROD_NOTIFY != ''
env:
IFTTT_PROD_NOTIFY: ${{ secrets.IFTTT_PROD_NOTIFY }}
BUILD_RESULT: ${{ needs.build.result }}
DEPLOY_RESULT: ${{ needs.deploy.result }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
run: |
if [ "$DEPLOY_RESULT" = "success" ]; then STATUS="✅ succeeded"
elif [ "$BUILD_RESULT" = "failure" ]; then STATUS="❌ failed (build)"
elif [ "$DEPLOY_RESULT" = "failure" ]; then STATUS="❌ failed (deploy)"
else STATUS="⚠️ cancelled"; fi
RUN_URL="${SERVER_URL}/${REPO}/actions/runs/${RUN_ID}"
curl -sSf --max-time 10 -X POST "$IFTTT_PROD_NOTIFY" \
-H 'Content-Type: application/json' \
-d "{
\"value1\": \"PROJECT_NAME: ${STATUS}\",
\"value2\": \"${RUN_URL}\",
\"value3\": \"\"
}" || echo "::warning::IFTTT notification failed"
```
### PR Preview Deploy (pr-checks.yml)
Trigger: pull_request to `main`. Concurrency: per-PR, cancel-in-progress: true.
```yaml
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.
```yaml
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
1. Cloudflare dashboard > My Profile > API Tokens > Create Token > Custom token
2. Permissions: Account > Cloudflare Pages > Edit
3. Account Resources: Include the target account
The Cloudflare Pages project is auto-created on first deploy via `wrangler pages deploy`.
## Step 6: Verify
```bash
pnpm build # Verify build works locally
```
## Companion Skills
- **`/dev-blacksmith-migration`** — Move heavy build jobs off `ubuntu-latest` onto Blacksmith/cloud runners
- **`/dev-ci-ifttt-notify`** — Add IFTTT webhook notifications
Scanned 9/8/2026
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!