Skip to content

Commit f6500d0

Browse files
committed
Initial commit
0 parents  commit f6500d0

4 files changed

Lines changed: 264 additions & 0 deletions

File tree

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Cloudflare Pages Deploy
2+
3+
Composite action for Forgejo that deploys to Cloudflare Pages and posts a preview URL comment on pull requests — replicating the GitHub-native Cloudflare Pages integration.
4+
5+
## Usage
6+
7+
```yaml
8+
- name: Deploy to Cloudflare Pages
9+
uses: https://git.systemscape.de/Systemscape/cf-pages-deploy@v1
10+
with:
11+
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
12+
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
13+
project-name: my-site
14+
directory: public
15+
```
16+
17+
On pull requests, the action automatically comments (or updates) a preview URL on the PR:
18+
19+
> ### Cloudflare Pages Preview
20+
>
21+
> | | |
22+
> |---|---|
23+
> | **Preview URL** | https://my-branch.my-site.pages.dev |
24+
> | **Commit** | `abc1234` |
25+
> | **Environment** | preview |
26+
27+
## Inputs
28+
29+
| Input | Required | Default | Description |
30+
|---|---|---|---|
31+
| `cloudflare-api-token` | Yes | | Cloudflare API token with Pages permissions |
32+
| `cloudflare-account-id` | Yes | | Cloudflare account ID |
33+
| `project-name` | Yes | | Cloudflare Pages project name |
34+
| `directory` | Yes | | Directory of static assets to deploy |
35+
| `forgejo-token` | No | `${{ github.token }}` | Forgejo API token for PR comments |
36+
| `comment-marker` | No | `<!-- cf-pages-deploy -->` | HTML comment marker for idempotent comment updates |
37+
38+
## Outputs
39+
40+
| Output | Description |
41+
|---|---|
42+
| `deployment-url` | The deployment URL (hash-based, e.g. `https://abc123.my-site.pages.dev`) |
43+
| `deployment-alias-url` | Branch-specific alias URL (e.g. `https://my-branch.my-site.pages.dev`) |
44+
| `environment` | `production` or `preview` |
45+
46+
## Full workflow example
47+
48+
```yaml
49+
name: Build and deploy
50+
51+
on:
52+
push:
53+
branches: [main]
54+
pull_request:
55+
56+
jobs:
57+
deploy:
58+
runs-on: ubuntu-latest
59+
steps:
60+
- uses: actions/checkout@v4
61+
62+
- name: Build
63+
uses: https://github.com/shalzz/zola-deploy-action@v0.21.0
64+
env:
65+
BUILD_ONLY: true
66+
OUT_DIR: public
67+
68+
- name: Deploy to Cloudflare Pages
69+
uses: https://git.systemscape.de/Systemscape/cf-pages-deploy@v1
70+
with:
71+
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
72+
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
73+
project-name: my-site
74+
directory: public
75+
```
76+
77+
## Setting up the Cloudflare API token
78+
79+
1. Go to the [Cloudflare dashboard API tokens page](https://dash.cloudflare.com/profile/api-tokens)
80+
2. Create a token with the **Cloudflare Pages: Edit** permission
81+
3. Add the token as a secret named `CLOUDFLARE_API_TOKEN` in your Forgejo repository settings
82+
83+
## How it works
84+
85+
1. **Deploy**: Runs `npx wrangler pages deploy` with the correct `--branch` derived from the CI environment (PR source branch or push branch), avoiding the detached-HEAD alias URL issue common in CI
86+
2. **Comment**: On pull request events, creates or updates a comment on the PR with the preview URL using the Forgejo API. Uses an HTML comment marker to find and update existing comments instead of creating duplicates
87+
88+
## Requirements
89+
90+
The runner image must have `curl`, `jq`, and `node`/`npx` installed.

action.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: 'Cloudflare Pages Deploy'
2+
description: 'Deploy to Cloudflare Pages and comment preview URL on Forgejo PRs'
3+
4+
inputs:
5+
cloudflare-api-token:
6+
description: 'Cloudflare API token with Pages permissions'
7+
required: true
8+
cloudflare-account-id:
9+
description: 'Cloudflare account ID'
10+
required: true
11+
project-name:
12+
description: 'Cloudflare Pages project name'
13+
required: true
14+
directory:
15+
description: 'Directory of static assets to deploy'
16+
required: true
17+
forgejo-token:
18+
description: 'Forgejo API token for PR comments (defaults to GITHUB_TOKEN)'
19+
required: false
20+
default: ${{ github.token }}
21+
comment-marker:
22+
description: 'HTML comment marker for idempotent comment updates'
23+
required: false
24+
default: '<!-- cf-pages-deploy -->'
25+
26+
outputs:
27+
deployment-url:
28+
description: 'The deployment URL (hash-based)'
29+
value: ${{ steps.deploy.outputs.deployment-url }}
30+
deployment-alias-url:
31+
description: 'Branch/PR-specific alias URL (preview URL)'
32+
value: ${{ steps.deploy.outputs.deployment-alias-url }}
33+
environment:
34+
description: 'Deployment environment (production or preview)'
35+
value: ${{ steps.deploy.outputs.environment }}
36+
37+
runs:
38+
using: 'composite'
39+
steps:
40+
- name: Deploy to Cloudflare Pages
41+
id: deploy
42+
shell: bash
43+
env:
44+
CLOUDFLARE_API_TOKEN: ${{ inputs.cloudflare-api-token }}
45+
CLOUDFLARE_ACCOUNT_ID: ${{ inputs.cloudflare-account-id }}
46+
INPUT_PROJECT_NAME: ${{ inputs.project-name }}
47+
INPUT_DIRECTORY: ${{ inputs.directory }}
48+
run: ${{ github.action_path }}/deploy.sh
49+
50+
- name: Comment preview URL on PR
51+
if: github.event_name == 'pull_request'
52+
shell: bash
53+
env:
54+
INPUT_FORGEJO_TOKEN: ${{ inputs.forgejo-token }}
55+
INPUT_COMMENT_MARKER: ${{ inputs.comment-marker }}
56+
INPUT_DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-alias-url || steps.deploy.outputs.deployment-url }}
57+
INPUT_ENVIRONMENT: ${{ steps.deploy.outputs.environment }}
58+
run: ${{ github.action_path }}/comment.sh

comment.sh

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
DEPLOYMENT_URL="${INPUT_DEPLOYMENT_URL:-}"
5+
if [ -z "$DEPLOYMENT_URL" ]; then
6+
echo "No deployment URL, skipping comment"
7+
exit 0
8+
fi
9+
10+
API_BASE="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
11+
PR_NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
12+
SHORT_SHA="${GITHUB_SHA:0:7}"
13+
MARKER="${INPUT_COMMENT_MARKER}"
14+
ENVIRONMENT="${INPUT_ENVIRONMENT:-preview}"
15+
16+
BODY="${MARKER}
17+
### Cloudflare Pages Preview
18+
19+
| | |
20+
|---|---|
21+
| **Preview URL** | ${DEPLOYMENT_URL} |
22+
| **Commit** | \`${SHORT_SHA}\` |
23+
| **Environment** | ${ENVIRONMENT} |"
24+
25+
# Find existing comment with our marker
26+
COMMENT_ID=$(curl -sSf \
27+
-H "Authorization: token ${INPUT_FORGEJO_TOKEN}" \
28+
"${API_BASE}/issues/${PR_NUMBER}/comments" \
29+
| jq -r --arg marker "$MARKER" '.[] | select(.body | contains($marker)) | .id' | head -n1)
30+
31+
if [ -n "$COMMENT_ID" ] && [ "$COMMENT_ID" != "null" ]; then
32+
echo "Updating comment ${COMMENT_ID}"
33+
curl -sSf -X PATCH \
34+
-H "Authorization: token ${INPUT_FORGEJO_TOKEN}" \
35+
-H "Content-Type: application/json" \
36+
-d "$(jq -n --arg body "$BODY" '{body: $body}')" \
37+
"${API_BASE}/issues/comments/${COMMENT_ID}"
38+
else
39+
echo "Creating new comment"
40+
curl -sSf -X POST \
41+
-H "Authorization: token ${INPUT_FORGEJO_TOKEN}" \
42+
-H "Content-Type: application/json" \
43+
-d "$(jq -n --arg body "$BODY" '{body: $body}')" \
44+
"${API_BASE}/issues/${PR_NUMBER}/comments"
45+
fi

deploy.sh

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Determine branch name explicitly to avoid detached HEAD producing "head.<project>.pages.dev"
5+
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
6+
BRANCH="$GITHUB_HEAD_REF"
7+
else
8+
BRANCH="$GITHUB_REF_NAME"
9+
fi
10+
11+
# Wrangler >= 3.81.0 writes structured JSON output to this directory
12+
WRANGLER_OUTPUT_DIR=$(mktemp -d)
13+
export WRANGLER_OUTPUT_FILE_DIRECTORY="$WRANGLER_OUTPUT_DIR"
14+
15+
OUTPUT=$(npx wrangler pages deploy "$INPUT_DIRECTORY" \
16+
--project-name="$INPUT_PROJECT_NAME" \
17+
--branch="$BRANCH" \
18+
--commit-hash="$GITHUB_SHA" 2>&1) || {
19+
echo "::error::Wrangler deploy failed"
20+
echo "$OUTPUT"
21+
exit 1
22+
}
23+
24+
echo "$OUTPUT"
25+
26+
# --- Extract outputs ---
27+
28+
DEPLOYMENT_URL=""
29+
ALIAS_URL=""
30+
ENVIRONMENT=""
31+
32+
# Primary: parse structured JSON artifacts (wrangler >= 3.81.0)
33+
for f in "$WRANGLER_OUTPUT_DIR"/*.json; do
34+
[ -f "$f" ] || continue
35+
while IFS= read -r line; do
36+
MATCH=$(echo "$line" | jq -r 'select(.type == "pages-deploy-detailed") | [.url, .alias, .environment] | map(. // "") | @tsv' 2>/dev/null) || continue
37+
[ -z "$MATCH" ] && continue
38+
IFS=$'\t' read -r DEPLOYMENT_URL ALIAS_URL ENVIRONMENT <<< "$MATCH"
39+
break 2
40+
done < "$f"
41+
done
42+
43+
# Fallback: parse stdout (older wrangler versions)
44+
if [ -z "$DEPLOYMENT_URL" ]; then
45+
DEPLOYMENT_URL=$(echo "$OUTPUT" | sed -n 's/.*Take a peek over at \(https:\/\/[^ ]*\).*/\1/p')
46+
# Handle timeout/unknown status case
47+
if [ -z "$DEPLOYMENT_URL" ]; then
48+
DEPLOYMENT_URL=$(echo "$OUTPUT" | sed -n 's/.*Visit your deployment at \(https:\/\/[^ ]*\).*/\1/p')
49+
fi
50+
fi
51+
52+
if [ -z "$ALIAS_URL" ]; then
53+
ALIAS_URL=$(echo "$OUTPUT" | sed -n 's/.*alias URL: \(https:\/\/[^ ]*\).*/\1/p')
54+
fi
55+
56+
if [ -z "$ENVIRONMENT" ]; then
57+
if [ -n "$ALIAS_URL" ] && [ "$ALIAS_URL" != "$DEPLOYMENT_URL" ]; then
58+
ENVIRONMENT="preview"
59+
else
60+
ENVIRONMENT="production"
61+
fi
62+
fi
63+
64+
rm -rf "$WRANGLER_OUTPUT_DIR"
65+
66+
# Write step outputs
67+
{
68+
echo "deployment-url=${DEPLOYMENT_URL}"
69+
echo "deployment-alias-url=${ALIAS_URL}"
70+
echo "environment=${ENVIRONMENT}"
71+
} >> "$GITHUB_OUTPUT"

0 commit comments

Comments
 (0)