Deploy your build output to Cloudflare Pages with Wrangler, while tracking every release through GitHub Environments and GitHub Deployment. On a pull request, it creates a preview deployment and comments the URL on the PR.
Features
- Deploy to Cloudflare Pages.
- Track releases with GitHub Environments & GitHub Deployment.
- Comment the deployment URL on pull requests.
- Delete old deployments with the companion
/deleteaction. - Run Wrangler from a subfolder via the
working-directoryinput — handy for monorepos wherefunctionsisn't in the repo root.
- Create a Cloudflare Pages project and an API token that can edit it.
- Manually create your GitHub Environments (for example
productionandpreview) — the action can't create them for you. See Setup. - Add the Cloudflare values as repository secrets/variables, then add a workflow like the one below (this mirrors the official template in .github/workflow-templates/deploy.yml):
name: Cloudflare Pages Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
# Deny all permissions by default; grant only what each job needs.
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: read # Only required for a private repo.
contents: read
deployments: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Deploy to Cloudflare Pages
uses: andykenward/github-actions-cloudflare-pages@46d86e1caa6b86365a41d335db65a6936a1beb39 #v3.5.0
with:
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-project-name: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: dist
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: ${{ (github.ref == 'refs/heads/main' && 'production') || 'preview' }}The github-environment expression deploys the main branch to production and every other branch to preview. For a line-by-line breakdown of this expression — and how it relates to the Cloudflare branch input — see GitHub Environments.
Create a Cloudflare Pages project, then give the action three values (store the token as a repository secret and the rest as variables or secrets):
cloudflare-api-token— an API token with permission to edit Cloudflare Pages.cloudflare-account-id— your Cloudflare account ID.cloudflare-project-name— the Pages project to upload to.
Important
This action does not create GitHub Environments. Creating them requires the GitHub API administration:write permission, which the action can't request — so you must create them manually. See Creating an environment.
Create each environment you reference (for example production and preview), then select one per run with the github-environment input. A common pattern switches on the branch:
github-environment: ${{ (github.ref == 'refs/heads/main' && 'production') || 'preview' }}GitHub Actions has no condition ? a : b ternary, so this uses the &&/|| idiom to get the same result. Read it as "if on main, use production, otherwise use preview":
github.refis the full ref of the branch that triggered the run, e.g.refs/heads/mainorrefs/heads/my-feature.github.ref == 'refs/heads/main'istrueonly on themainbranch.A && BreturnsBwhenAis true, so onmainthe expression so far is'production'; on any other branch it isfalse.X || 'preview'returnsXunlessXis falsy, so afalseleft side falls through to'preview'.
To map more branches to environments, extend the same pattern — for example, send main to production, staging to staging, and everything else to preview:
github-environment: >-
${{ (github.ref == 'refs/heads/main' && 'production')
|| (github.ref == 'refs/heads/staging' && 'staging')
|| 'preview' }}Note
github-environment only sets the GitHub Environment the deployment is recorded against. Whether Cloudflare treats the upload as a production or preview deployment is decided separately, by the branch name — Cloudflare promotes the deployment to production only when the branch matches your Pages project's production branch. By default the branch is detected from the GitHub context; use the branch input to override it. The two inputs are independent, so make sure your branch logic and github-environment logic agree on what counts as "production".
When using the workflow's built-in GITHUB_TOKEN for the github-token input, grant these permissions:
permissions:
actions: read # Only required for a private GitHub repo.
contents: read
deployments: write
pull-requests: write| Input | Required | Description |
|---|---|---|
cloudflare-api-token |
yes | Cloudflare API Token |
cloudflare-account-id |
yes | Cloudflare Account ID |
cloudflare-project-name |
yes | Cloudflare Pages project to upload to |
directory |
yes | Directory of static files to upload |
github-token |
yes | Github API key, make sure to add the required permissions for this action. |
github-environment |
yes | GitHub environment to deploy to. You need to manually create this for the github repo |
pr-number |
no | GitHub pull request number to comment on. If not set, the action auto-detects from the event payload. |
working-directory |
no | Directory to run wrangler cli from |
wrangler-version |
no | Wrangler version to use. Otherwise a default version from the action will be used. |
branch |
no | Branch name to use for Cloudflare Pages deployment. If not set, the branch is automatically detected from the GitHub context. |
| Output | Description |
|---|---|
id |
Cloudflare Pages deployed id |
url |
Cloudflare Pages deployed url |
environment |
Cloudflare Pages deployed environment production or preview |
alias |
Cloudflare Pages deployed alias. Falls back to deployed url if deployed alias is null |
wrangler |
Wrangler cli output |
Ready-to-use GitHub Workflow Templates live in .github/workflow-templates/:
Pull requests from forks don't have access to secrets in the initial pull_request workflow. Use a second workflow triggered by workflow_run to deploy from the original repository context after the first workflow succeeds, and set the pr-number input so the action can find the right pull request to comment on.
name: Deploy PR Preview (Fork Safe)
on:
workflow_run:
workflows: ['CI']
types: [completed]
jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: read
deployments: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_sha }}
- name: Deploy to Cloudflare Pages
uses: andykenward/github-actions-cloudflare-pages@46d86e1caa6b86365a41d335db65a6936a1beb39 #v3.5.0
with:
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-project-name: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: dist
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: preview
pr-number: # The PR numberThe action supports the workflow_run event and uses its head commit SHA and branch for the deployment metadata.
You can override the automatically detected branch name with the branch input. This is useful with workflow_run: a fork pull request opened from the fork's main branch would otherwise deploy to your project's production branch and overwrite the production deployment. Giving each pull request its own branch name (for example pr-123) keeps it on a separate Cloudflare Pages preview.
Do not build the branch name from github.event.workflow_run.pull_requests[0].number — that array is empty for pull requests from forks (community discussion #25220), which is the exact case this is meant to cover. Instead, save the PR number in the triggering pull_request workflow and read it back from an artifact in the workflow_run workflow.
In the pull_request workflow (the one named in workflows: of the workflow_run trigger), save the PR number alongside your build output:
- name: Save PR number
run: echo "${{ github.event.number }}" > pr-number.txt
- name: Upload PR number
uses: actions/upload-artifact@v4
with:
name: pr-number
path: pr-number.txtThen, in the workflow_run workflow, download it and pass it to both branch and pr-number:
jobs:
deploy:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
actions: read
contents: read
deployments: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Download PR number
uses: actions/download-artifact@v4
with:
name: pr-number
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Read PR number
id: pr
run: echo "number=$(cat pr-number.txt)" >> "$GITHUB_OUTPUT"
- name: Deploy to Cloudflare Pages
uses: andykenward/github-actions-cloudflare-pages@46d86e1caa6b86365a41d335db65a6936a1beb39 #v3.5.0
with:
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-project-name: ${{ vars.CLOUDFLARE_PROJECT_NAME }}
directory: dist
github-token: ${{ secrets.GITHUB_TOKEN }}
github-environment: preview
branch: pr-${{ steps.pr.outputs.number }}
pr-number: ${{ steps.pr.outputs.number }}This creates a Cloudflare Pages preview deployment with a branch name like pr-123, so each pull request — including those from forks — gets its own preview environment instead of overwriting production.
Use the companion sub-action andykenward/github-actions-cloudflare-pages/delete to remove old deployments.
The GitHub Deployment payload this action creates includes the Cloudflare metadata the delete action needs:
{
"payload": {
"cloudflare": {
"id": "123",
"projectName": "cloudflare-pages-project-name",
"accountId": "123"
},
"url": "https://example.com",
"commentId": "1234"
}
}GitHub provides two debug log levels — see Action Debugging. Enable them by setting a repository secret:
- Step debug logs: set
ACTIONS_STEP_DEBUGtotrue. Debug events then appear in the downloaded logs and web logs. - Runner diagnostic logs: set
ACTIONS_RUNNER_DEBUGtotrue. Extra diagnostic files then appear in therunner-diagnostic-logsfolder of the log archive.
Upgrading from an older version? Check CHANGELOG.md for breaking changes.
