Deploys files to a Cloudflare Pages project using direct upload and sets GitHub deployment status.
Advantages of direct upload compared with linking GitHub to Cloudflare:
- No need to link Cloudflare Pages as a GitHub App to your entire GitHub account.
- Full control over build process. Using CI/CD such as GitHub Actions is much more flexible than Cloudflare for doing the build and adding other jobs.
Action input | Required | Description |
---|---|---|
cloudflare-account-id | Yes | Cloudflare account ID that contains the Pages project |
cloudflare-api-token | Yes | Cloudflare API token with Cloudflare Pages read/write permission. |
cloudflare-environment | Yes | The name of the environment (Cloudflare calls it a branch) you want for the Cloudflare Pages deployment. Does not have to match git branch. |
cloudflare-pages-project-name | Yes | The name of the Cloudflare Pages project you want to deploy to. |
cloudflare-publish-directory | Yes | The directory of static files to upload to Cloudflare Pages. |
Action outputs | Description |
---|---|
github-deployment-id | The created GitHub deployment identifier |
cloudflare-pages-deployment-id | The created Cloudflare Pages deployment identifier. |
cloudflare-pages-url | The URL where Cloudflare Pages was deployed to. |
name: Deploy to Cloudflare Pages production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build
run: make build
- id: Deploy
uses: eddiegroves/cloudflare-pages-deploy-action@7eb85e27aa014332055f718f151a7071ce50eae4
with:
cloudflare-publish-directory: dist/
cloudflare-environment: production
cloudflare-pages-project-name: eddie-cf-pages-example
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
The action is developed as a composite action using bash or python steps. All
steps are placed inline into the run
sections to avoid issues with PATHs and
external files.
The process is three steps:
- Create GitHub deployment
- Create Cloudflare Pages deployment
- Poll deployment status
βββββββββ ββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββ ββββββββ ββββββββββββ βGitHub β βcreate-github-β βcreate-cloudflare-β βpoll-deployment-β βGitHubβ βCloudflareβ βActionsβ βdeployment β βpages-deployment β βstatus β βAPI β βAPI β βββββ¬ββββ ββββββββ¬ββββββββ ββββββββββ¬ββββββββββ βββββββββ¬βββββββββ ββββ¬ββββ ββββββ¬ββββββ β Start step β β β β β βββββββββββββββββββββ>β β β β β β β β β β β β β β Create GitHub deployment β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ> β β β β β β β β β β β β β β β <β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β Deployment ID β β β β β β<β β β β β β β β β β β β β β β β β β β β β β Start step β β β β βββββββββββββββββββββββββββββββββββββββββββββββββ>β β β β β β β β β β β β β β Create Pages deployment β β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ> β β β β β β β β β β β β β β β <β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β<β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β Start step β β β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ>β β β β β β β β β β β β β β β βββββββββ€ββββββββββββββββͺββββββββββββββββββββββͺββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββββββββββββββββͺββββββββββββββββββββͺββββββββββββββββ β LOOP β up to 5 minutes β β β β β β βββββββββ β β β β β β β β β β β β Find matching deployment β β β β β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ> β β β β β β β β β β β β β β id, stage, status β β β β β β β β <β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β ββββββββ€βββββββͺββββββββββββββββββββββͺββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββββββββββββββββͺββββββββββββββ β β β β ALT β status == failure β β β β β β β β ββββββββ β β β β β β β β β β β β β β Create GitHub deployment status (failed) β β β β β β β β β β βββββββββββββββββββββββββββββββββββββββββ> β β β β β β β β β β β β β β β β β β β β β β β β β β β β β <β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β<β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β ββ β β β β β βββββββββββββββͺββββββββββββββββββββββͺββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββββββββββββββββͺββββββββββββββ β β β β β β β β β β β β β β β β β β β ββββββββ€βββββββͺββββββββββββββββββββββͺββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββββββββββββββββͺββββββββββββββ β β β β ALT β stage == deploy and status == success β β β β β β β ββββββββ β β β β β β β β β β β β β β Create GitHub deployment status (success)β β β β β β β β β β βββββββββββββββββββββββββββββββββββββββββ> β β β β β β β β β β β β β β β β β β β β β β β β β β β β β <β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β id, Cloudflare Pages url β β β β β β β β<β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β ββ β β β β β βββββββββββββββͺββββββββββββββββββββββͺββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββββββββββββββββͺββββββββββββββ β β β β β β β β β β β β β β β Create GitHub deployment status (pending)β β β β β β β β βββββββββββββββββββββββββββββββββββββββββ> β β β β β β β β β β β β β β β β β β β β β β β <β β β β β β β β β β β β β β β β β β β β β β β βββββββββββββββββββββββββͺββββββββββββββββββββββͺββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββͺβββββββββββββββββββββββββββββββββββββββββββͺββββββββββββββββββββͺββββββββββββββββ β β β β β β β β β β Create GitHub deployment status (error) β β β β β β βββββββββββββββββββββββββββββββββββββββββ> β β β β β β β β β β β β β β<β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β ββ β β βββββ΄ββββ ββββββββ΄ββββββββ ββββββββββ΄ββββββββββ βββββββββ΄βββββββββ ββββ΄ββββ ββββββ΄ββββββ βGitHub β βcreate-github-β βcreate-cloudflare-β βpoll-deployment-β βGitHubβ βCloudflareβ βActionsβ βdeployment β βpages-deployment β βstatus β βAPI β βAPI β βββββββββ ββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββ ββββββββ ββββββββββββ
Create Github deployment is responsible for calling the create deployments GitHub API and outputting the newly created deployment identifier.
It is implemented in bash and a standalone example can be tested and viewed: create-gh-deployment.sh
GitHub deployments are used purely for convenience purposes by populating GitHub deployment history and filling the environment section on the repository page.
Environment variable | Required | Description | Value | Resources |
---|---|---|---|---|
INPUT_ENVIRONMENT | Yes | The deployment target environment, e.g test, dev, qa, production. | inputs.cloudflare-environment | GitHub create deployment API |
INPUT_GITHUB_REF | Yes | The ref for the deploy. This can be a branch, tag, or SHA. | github.ref | Required by GitHub Create a deployment API |
INPUT_GITHUB_REPOSITORY | Yes | The owner and repository name. For example, eddiegroves/hello-world. | github.repository | GitHub Actions contexts |
GITHUB_TOKEN | Yes | A token to authenticate on behalf of the GitHub Action. | github.token | GitHub Actions contexts |
GitHub outputs | Description |
---|---|
github-deployment-id | The created GitHub deployment identifier |
# * Inputs
#
# |-------------------------+----------------------------------------------------------------------|
# | Environment variable | Description |
# |-------------------------+----------------------------------------------------------------------|
# | INPUT_ENVIRONMENT | The deployment target environment, e.g test, dev, qa, staging. |
# | INPUT_GITHUB_REF | The ref for the deploy. This can be a branch, tag, or SHA. |
# | INPUT_GITHUB_REPOSITORY | The owner and repository name. For example, eddiegroves/hello-world. |
# | GITHUB_TOKEN | A token to authenticate on behalf of the GitHub Action. |
# |-------------------------+----------------------------------------------------------------------|
#
# * Outputs
#
# |----------------------+------------------------------------------|
# | GitHub outputs | Description |
# |----------------------+------------------------------------------|
# | github-deployment-id | The created GitHub deployment identifier |
# |----------------------+------------------------------------------|
errors=""
declare -a inputs=(<<create-gh-deployment-inputs-list()>>)
for input_name in "${inputs[@]}"
do
if [ -z ${!input_name+x} ]; then
errors="${errors}environment variable $input_name is required\n"
fi
done
if [ ! -z "${errors}" ]; then
printf "$errors"
exit 1
fi
json_string=$( jq -n \
--arg environment "$INPUT_ENVIRONMENT" \
--arg github_ref "$INPUT_GITHUB_REF" \
'<<create-gh-deployment-json-jq-single-line>>')
curl --silent \
-X POST \
https://api.github.com/repos/$INPUT_GITHUB_REPOSITORY/deployments \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-d "$json_string" | jq '.id' | xargs -I {} echo "::set-output name=github-deployment-id::{}"
Create Cloudflare Pages deployment deploys a directory of static assets as a Pages deployment.
It is implemented in bash and a standalone example can be tested and viewed:
The implementation uses Cloudflare Wrangler CLI tool which is an npm package.
Name | Homepage | Description |
---|---|---|
wrangler | https://github.com/cloudflare/wrangler2#readme | Command-line interface for all things Cloudflare Workers |
Environment variable | Required | Description | Value | Resources |
---|---|---|---|---|
CLOUDFLARE_ACCOUNT_ID | Yes | Used by Wranlger to identify the Cloudflare Pages account. | inputs.cloudflare-account-id | Wrangler source |
CLOUDFLARE_API_TOKEN | Yes | Used by Wranlger to authenticate to Cloudflare API. | inputs.cloudflare-api-token | Wrangler source |
INPUT_CLOUDFLARE_PROJECT_NAME | Yes | The name of the Cloudflare Pages project you want to deploy to. | inputs.cloudflare-pages-project-name | Wrangler pages publish command |
INPUT_ENVIRONMENT | Yes | The deployment target environment, e.g test, dev, qa, staging. Cloudflare refers to this as a branch. | inputs.cloudflare-environment | Wrangler pages publish command |
INPUT_PUBLISH_DIRECTORY | Yes | The directory of static files to upload. | inputs.cloudflare-publish-directory | Wrangler pages publish command |
INPUT_COMMIT_HASH | Yes | The SHA to attach to this deployment. | github.sha | Wrangler pages publish command |
INPUT_MESSAGE | Yes | The commit message to attach to this deployment. | github.event.head_commit.message | Wrangler pages publish command https://stackoverflow.com/a/63619526/19596450 github.event.head_commit object available for push event |
No outputs are created.
# * Input
#
# |-------------------------------+-----------------------------------------------------------------|
# | Environment variable | Description |
# |-------------------------------+-----------------------------------------------------------------|
# | CLOUDFLARE_ACCOUNT_ID | Used by Wranlger to identify the Cloudflare Pages account. |
# | CLOUDFLARE_API_TOKEN | Used by Wranlger to authenticate to Cloudflare API. |
# | INPUT_CLOUDFLARE_PROJECT_NAME | The name of the Cloudflare Pages project you want to deploy to. |
# | INPUT_ENVIRONMENT | The deployment target environment, e.g test, dev, qa, staging. |
# | INPUT_PUBLISH_DIRECTORY | The directory of static files to upload. |
# | INPUT_COMMIT_HASH | The SHA to attach to this deployment. |
# | INPUT_MESSAGE | The commit message to attach to this deployment. |
# |-------------------------------+-----------------------------------------------------------------|
#
# * Output
#
# No outputs are created.
errors=""
declare -a inputs=(<<create-cf-deployment-status-inputs-list()>>)
for input_name in "${inputs[@]}"
do
if [ -z ${!input_name+x} ]; then
errors="${errors}environment variable $input_name is required\n"
fi
done
if [ ! -z "${errors}" ]; then
printf "$errors"
exit 1
fi
npx [email protected] pages publish $INPUT_PUBLISH_DIRECTORY \
--project-name=$INPUT_CLOUDFLARE_PROJECT_NAME \
--branch=$INPUT_ENVIRONMENT \
--commit-hash=$INPUT_COMMIT_HASH \
--commit-message="$INPUT_MESSAGE"
Environment variable | Required | Description | Value | Resources |
---|---|---|---|---|
INPUT_COMMIT_HASH | Yes | The SHA to attach to this deployment. | github.sha | |
INPUT_GITHUB_DEPLOYMENT_ID | Yes | The GitHub deployment identifier for this deployment. | steps.create-gh-deployment.outputs.github-deployment-id | |
INPUT_GITHUB_REPOSITORY | Yes | The owner and repository name. For example, eddiegroves/hello-world. | github.repository | GitHub Actions contexts |
INPUT_CLOUDFLARE_PROJECT_NAME | Yes | The name of the Cloudflare Pages project you want to deploy to. | inputs.cloudflare-pages-project-name | Wrangler pages publish command |
GITHUB_TOKEN | Yes | A token to authenticate on behalf of the GitHub Action. | github.token | GitHub Actions contexts |
CLOUDFLARE_ACCOUNT_ID | Yes | Used by Wrangler to identify the Cloudflare Pages account. | inputs.cloudflare-account-id | Wrangler source |
CLOUDFLARE_API_TOKEN | Yes | Used by Wrangler to authenticate to Cloudflare API. | inputs.cloudflare-api-token | Wrangler source |
GitHub outputs | Description |
---|---|
cloudflare-pages-deployment-id | The created Cloudflare Pages deployment identifier. |
cloudflare-pages-url | The URL where Cloudflare Pages was deployed to. |
# * Inputs
#
# |-------------------------------+----------+----------------------------------------------------------------------|
# | Environment variable | Required | Description |
# |-------------------------------+----------+----------------------------------------------------------------------|
# | INPUT_COMMIT_HASH | Yes | The SHA to attach to this deployment. |
# | INPUT_GITHUB_DEPLOYMENT_ID | Yes | The GitHub deployment identifier for this deployment. |
# | INPUT_GITHUB_REPOSITORY | Yes | The owner and repository name. For example, eddiegroves/hello-world. |
# | INPUT_CLOUDFLARE_PROJECT_NAME | Yes | The name of the Cloudflare Pages project you want to deploy to. |
# | GITHUB_TOKEN | Yes | A token to authenticate on behalf of the GitHub Action. |
# | CLOUDFLARE_ACCOUNT_ID | Yes | Used by Wrangler to identify the Cloudflare Pages account. |
# | CLOUDFLARE_API_TOKEN | Yes | Used by Wrangler to authenticate to Cloudflare API. |
# |-------------------------------+----------+----------------------------------------------------------------------|
#
# * Outputs
# |--------------------------------+-----------------------------------------------------|
# | GitHub outputs | Description |
# |--------------------------------+-----------------------------------------------------|
# | cloudflare-pages-deployment-id | The created Cloudflare Pages deployment identifier. |
# | cloudflare-pages-url | The URL where Cloudflare Pages was deployed to. |
# |--------------------------------+-----------------------------------------------------|
import http.client
import json
import subprocess
import os
from time import sleep
from typing import Literal
commit_hash = os.environ["INPUT_COMMIT_HASH"]
github_deployment_id = os.environ["INPUT_GITHUB_DEPLOYMENT_ID"]
github_repository = os.environ["INPUT_GITHUB_REPOSITORY"]
cloudflare_project_name = os.environ["INPUT_CLOUDFLARE_PROJECT_NAME"]
github_token = os.environ["GITHUB_TOKEN"]
cloudflare_api_token = os.environ["CLOUDFLARE_API_TOKEN"]
cloudflare_account_id = os.environ["CLOUDFLARE_ACCOUNT_ID"]
def create_github_deployment_status(
description: str,
environment: str,
state: Literal[
"error", "failure", "inactive", "in_progress", "queued", "pending", "success"
],
log_url: str = None,
environment_url: str = None,
):
payload = {"description": description, "environment": environment, "state": state}
if log_url:
payload["log_url"] = log_url
if environment_url:
payload["environment_url"] = environment_url
headers = {
"User-Agent": "eddiegroves/action/cloudflare-deploy",
"Content-Type": "application/json",
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {github_token}",
}
githubApi = http.client.HTTPSConnection("api.github.com")
githubApi.request(
"POST",
f"/repos/{github_repository}/deployments/{github_deployment_id}/statuses",
json.dumps(payload),
headers,
)
def list_cloudflare_deployments():
headers = {
"User-Agent": "eddiegroves/action/cloudflare-deploy",
"Accept": "application/json",
"Authorization": f"Bearer {cloudflare_api_token}",
}
cloudflareApi = http.client.HTTPSConnection("api.cloudflare.com")
cloudflareApi.request(
"GET",
f"/client/v4/accounts/{cloudflare_account_id}/pages/projects/{cloudflare_project_name}/deployments",
"",
headers,
)
res = cloudflareApi.getresponse()
data = res.read().decode("utf-8")
p = subprocess.run(
[
"jq",
"-r",
"--arg",
"commit",
commit_hash,
".result[] | select(.deployment_trigger.metadata.commit_hash == $commit) | { id, project_name, environment, url: (.aliases[0] // .url), stage: .latest_stage.name, status: .latest_stage.status }",
],
input=data,
capture_output=True,
text=True,
check=False,
)
return json.loads(p.stdout)
count = 0
while count < 60:
deployment = list_cloudflare_deployments()
if deployment["status"] == "failed":
print("π’ sad face")
create_github_deployment_status(
description="Cloudflare Pages deployment failed.",
environment="production",
state="failure",
log_url=f"https://dash.cloudflare.com?to=/:account/pages/view/{cloudflare_project_name}/{deployment['id']}",
)
exit(1)
if deployment["stage"] == "deploy" and deployment["status"] == "success":
print("π€ happy face")
create_github_deployment_status(
description="Cloudflare Pages deployment success.",
environment="production",
state="success",
log_url=f"https://dash.cloudflare.com?to=/:account/pages/view/{cloudflare_project_name}/{deployment['id']}",
environment_url=deployment["url"],
)
print(f"::set-output name=cloudflare-pages-deployment-id::{deployment['id']}")
print(f"::set-output name=cloudflare-pages-url::{deployment['url']}")
exit(0)
print(f"π§ {deployment['stage']} {deployment['status']}")
create_github_deployment_status(
description="Cloudflare Pages deployment in progress.",
environment="production",
state="in_progress",
log_url=f"https://dash.cloudflare.com?to=/:account/pages/view/{cloudflare_project_name}/{deployment['id']}",
)
sleep(5)
count = count + 1
print("π super sad")
create_github_deployment_status(
description="Cloudflare Pages deployment status unknown.",
environment="production",
state="error",
)
exit(1)