Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions .github/workflows/pages-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
npm i --no-save wrangler@3.114.17 > /dev/null
npm i --no-save wrangler@3.114.17
npx wrangler pages deploy _site --project-name=synthorg-pr-preview --branch="pr-${PR_NUMBER}"

- name: Comment preview URL
Expand Down Expand Up @@ -223,6 +223,7 @@ jobs:
pull-requests: write
steps:
- name: Delete preview comment
continue-on-error: true # Don't block deployment cleanup if comment deletion fails
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
Expand Down Expand Up @@ -257,24 +258,76 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
BRANCH="pr-${PR_NUMBER}"
# List deployments for this branch (per_page=100 to cover busy PRs)
# Note: Cloudflare prevents deleting the latest deployment per branch — that one will remain
DEPLOYMENTS=$(curl -sf \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments?per_page=100" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
| jq -r ".result[] | select(.deployment_trigger.metadata.branch == \"${BRANCH}\") | .id") || true
API_BASE="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments"

# Paginate through all deployments for this branch
# Note: Cloudflare prevents deleting the most recent deployment per project — that one will remain
echo "Fetching deployments for branch ${BRANCH}..."
PAGE=1
DEPLOYMENTS=""
while :; do
CURL_EXIT=0
RESPONSE=$(curl -s -w "\n%{http_code}" \
"${API_BASE}?per_page=100&page=${PAGE}" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") || CURL_EXIT=$?
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')

# Fix: detect curl transport failures (DNS, timeout, connection refused → HTTP code 000)
if [ "$CURL_EXIT" -ne 0 ] || [ "$HTTP_CODE" = "000" ]; then
echo "::error::Failed to reach Cloudflare API (curl exit code ${CURL_EXIT}, HTTP ${HTTP_CODE})"
exit 1
fi

# exit 1: if we can't list deployments, the token/account is likely misconfigured — that warrants attention
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::error::Failed to list deployments (HTTP ${HTTP_CODE})"
echo "$BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || echo "$BODY" | head -c 500
exit 1
fi

# Validate response structure before filtering
if ! echo "$BODY" | jq -e '.result' > /dev/null 2>&1; then
echo "::warning::Unexpected API response structure — .result field missing"
echo "$BODY" | head -c 500
exit 0
fi

# Wrangler direct-upload deployments store branch in deployment_trigger.metadata.branch
PAGE_IDS=$(echo "$BODY" | jq -r \
--arg branch "$BRANCH" \
'.result[] | select(.deployment_trigger.metadata.branch == $branch) | .id')
if [ -n "$PAGE_IDS" ]; then
DEPLOYMENTS="${DEPLOYMENTS}${DEPLOYMENTS:+$'\n'}${PAGE_IDS}"
fi

PAGE_COUNT=$(echo "$BODY" | jq -r '.result | length')
[ "$PAGE_COUNT" -lt 100 ] && break
PAGE=$((PAGE + 1))
done
Comment on lines +268 to +307
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No upper bound on pagination loop

The while : loop has no maximum iteration guard. If the Cloudflare API were to unexpectedly return exactly 100 results on every page (e.g., due to an API bug or a project with thousands of deployments), the loop would run indefinitely and consume the job's entire timeout-minutes: 5 budget before being killed. A simple safety ceiling (e.g. 20 pages = 2 000 deployments) would prevent runaway execution:

Suggested change
while :; do
CURL_EXIT=0
RESPONSE=$(curl -s -w "\n%{http_code}" \
"${API_BASE}?per_page=100&page=${PAGE}" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") || CURL_EXIT=$?
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
# Fix: detect curl transport failures (DNS, timeout, connection refused → HTTP code 000)
if [ "$CURL_EXIT" -ne 0 ] || [ "$HTTP_CODE" = "000" ]; then
echo "::error::Failed to reach Cloudflare API (curl exit code ${CURL_EXIT}, HTTP ${HTTP_CODE})"
exit 1
fi
# exit 1: if we can't list deployments, the token/account is likely misconfigured — that warrants attention
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::error::Failed to list deployments (HTTP ${HTTP_CODE})"
echo "$BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || echo "$BODY" | head -c 500
exit 1
fi
# Validate response structure before filtering
if ! echo "$BODY" | jq -e '.result' > /dev/null 2>&1; then
echo "::warning::Unexpected API response structure — .result field missing"
echo "$BODY" | head -c 500
exit 0
fi
# Wrangler direct-upload deployments store branch in deployment_trigger.metadata.branch
PAGE_IDS=$(echo "$BODY" | jq -r \
--arg branch "$BRANCH" \
'.result[] | select(.deployment_trigger.metadata.branch == $branch) | .id')
if [ -n "$PAGE_IDS" ]; then
DEPLOYMENTS="${DEPLOYMENTS}${DEPLOYMENTS:+$'\n'}${PAGE_IDS}"
fi
PAGE_COUNT=$(echo "$BODY" | jq -r '.result | length')
[ "$PAGE_COUNT" -lt 100 ] && break
PAGE=$((PAGE + 1))
done
PAGE=1
DEPLOYMENTS=""
MAX_PAGES=20
while [ "$PAGE" -le "$MAX_PAGES" ]; do

Then update the break condition comment to note the ceiling. This is particularly relevant because cleanup jobs are inherently lower priority and a hung loop silently burns CI minutes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/pages-preview.yml
Line: 268-307

Comment:
**No upper bound on pagination loop**

The `while :` loop has no maximum iteration guard. If the Cloudflare API were to unexpectedly return exactly 100 results on every page (e.g., due to an API bug or a project with thousands of deployments), the loop would run indefinitely and consume the job's entire `timeout-minutes: 5` budget before being killed. A simple safety ceiling (e.g. 20 pages = 2 000 deployments) would prevent runaway execution:

```suggestion
          PAGE=1
          DEPLOYMENTS=""
          MAX_PAGES=20
          while [ "$PAGE" -le "$MAX_PAGES" ]; do
```

Then update the break condition comment to note the ceiling. This is particularly relevant because cleanup jobs are inherently lower priority and a hung loop silently burns CI minutes.

How can I resolve this? If you propose a fix, please make it concise.


if [ -z "$DEPLOYMENTS" ]; then
echo "No deployments found for branch ${BRANCH}"
echo "Available branches:"
echo "$BODY" | jq -r '[.result[].deployment_trigger.metadata.branch // "null"] | unique[]' 2>/dev/null || true
exit 0
Comment on lines 309 to 313
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Available-branches diagnostic shows only the last fetched page

When no matching deployments are found after iterating multiple pages, $BODY holds only the last page of results. If the target branch happened to appear on an earlier page (e.g. offset by many non-matching deployments), the diagnostic output would miss it entirely and display branches from a completely unrelated page. For a single-page result set this is fine, but it can silently mislead debugging in a paginated scenario.

Consider collecting unique branch names across pages during the loop, e.g.:

ALL_BRANCHES="${ALL_BRANCHES} $(echo "$BODY" | jq -r '.result[].deployment_trigger.metadata.branch // "null"')"

and then printing echo "$ALL_BRANCHES" | tr ' ' '\n' | sort -u in the diagnostic block.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/pages-preview.yml
Line: 309-313

Comment:
**Available-branches diagnostic shows only the last fetched page**

When no matching deployments are found after iterating multiple pages, `$BODY` holds only the last page of results. If the target branch happened to appear on an earlier page (e.g. offset by many non-matching deployments), the diagnostic output would miss it entirely and display branches from a completely unrelated page. For a single-page result set this is fine, but it can silently mislead debugging in a paginated scenario.

Consider collecting unique branch names across pages during the loop, e.g.:
```bash
ALL_BRANCHES="${ALL_BRANCHES} $(echo "$BODY" | jq -r '.result[].deployment_trigger.metadata.branch // "null"')"
```
and then printing `echo "$ALL_BRANCHES" | tr ' ' '\n' | sort -u` in the diagnostic block.

How can I resolve this? If you propose a fix, please make it concise.

fi

COUNT=0
for DEPLOY_ID in $DEPLOYMENTS; do
echo "Deleting deployment ${DEPLOY_ID}..."
RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments/${DEPLOY_ID}?force=true" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${HTTP_CODE}) — may be the latest deployment (Cloudflare restriction)"
DEL_CURL_EXIT=0
DEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"${API_BASE}/${DEPLOY_ID}?force=true" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") || DEL_CURL_EXIT=$?
DEL_CODE=$(echo "$DEL_RESPONSE" | tail -1)

if [ "$DEL_CURL_EXIT" -ne 0 ] || [ "$DEL_CODE" = "000" ]; then
echo "::warning::Failed to reach Cloudflare API for deletion of ${DEPLOY_ID} (curl transport error)"
elif [ "$DEL_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${DEL_CODE}) — may be the most recent deployment (Cloudflare restriction)"
else
COUNT=$((COUNT + 1))
fi
Comment on lines +320 to 331
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete requests also ignore curl exit status. If curl fails, DEL_CODE may be empty/"000", the -ge test becomes unreliable, and the script can incorrectly increment COUNT (reporting deletions that didn’t happen). Check the curl return code and ensure DEL_CODE is a valid HTTP status before treating the deletion as successful.

Copilot uses AI. Check for mistakes.
Comment on lines 317 to 331
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deletion error body not extracted or logged

On a failed deletion (e.g. HTTP 409, 403), DEL_RESPONSE already holds the full response body, but it's never split or printed. This means the Cloudflare error message (e.g. "cannot delete the most recent deployment for a project") is silently discarded, leaving only the numeric HTTP code in the warning. By contrast, the fetch path properly extracts and logs the error body.

Suggested change
for DEPLOY_ID in $DEPLOYMENTS; do
echo "Deleting deployment ${DEPLOY_ID}..."
RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments/${DEPLOY_ID}?force=true" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${HTTP_CODE}) — may be the latest deployment (Cloudflare restriction)"
DEL_CURL_EXIT=0
DEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"${API_BASE}/${DEPLOY_ID}?force=true" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") || DEL_CURL_EXIT=$?
DEL_CODE=$(echo "$DEL_RESPONSE" | tail -1)
if [ "$DEL_CURL_EXIT" -ne 0 ] || [ "$DEL_CODE" = "000" ]; then
echo "::warning::Failed to reach Cloudflare API for deletion of ${DEPLOY_ID} (curl transport error)"
elif [ "$DEL_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${DEL_CODE}) — may be the most recent deployment (Cloudflare restriction)"
else
COUNT=$((COUNT + 1))
fi
for DEPLOY_ID in $DEPLOYMENTS; do
echo "Deleting deployment ${DEPLOY_ID}..."
DEL_CURL_EXIT=0
DEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"${API_BASE}/${DEPLOY_ID}?force=true" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") || DEL_CURL_EXIT=$?
DEL_CODE=$(echo "$DEL_RESPONSE" | tail -1)
DEL_BODY=$(echo "$DEL_RESPONSE" | sed '$d')
if [ "$DEL_CURL_EXIT" -ne 0 ] || [ "$DEL_CODE" = "000" ]; then
echo "::warning::Failed to reach Cloudflare API for deletion of ${DEPLOY_ID} (curl transport error)"
elif [ "$DEL_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${DEL_CODE}) — may be the most recent deployment (Cloudflare restriction)"
echo "$DEL_BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || echo "$DEL_BODY" | head -c 500
else
COUNT=$((COUNT + 1))
fi
done
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/pages-preview.yml
Line: 317-331

Comment:
**Deletion error body not extracted or logged**

On a failed deletion (e.g. HTTP 409, 403), `DEL_RESPONSE` already holds the full response body, but it's never split or printed. This means the Cloudflare error message (e.g. `"cannot delete the most recent deployment for a project"`) is silently discarded, leaving only the numeric HTTP code in the warning. By contrast, the fetch path properly extracts and logs the error body.

```suggestion
          for DEPLOY_ID in $DEPLOYMENTS; do
            echo "Deleting deployment ${DEPLOY_ID}..."
            DEL_CURL_EXIT=0
            DEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
              "${API_BASE}/${DEPLOY_ID}?force=true" \
              -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}") || DEL_CURL_EXIT=$?
            DEL_CODE=$(echo "$DEL_RESPONSE" | tail -1)
            DEL_BODY=$(echo "$DEL_RESPONSE" | sed '$d')

            if [ "$DEL_CURL_EXIT" -ne 0 ] || [ "$DEL_CODE" = "000" ]; then
              echo "::warning::Failed to reach Cloudflare API for deletion of ${DEPLOY_ID} (curl transport error)"
            elif [ "$DEL_CODE" -ge 400 ]; then
              echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${DEL_CODE}) — may be the most recent deployment (Cloudflare restriction)"
              echo "$DEL_BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || echo "$DEL_BODY" | head -c 500
            else
              COUNT=$((COUNT + 1))
            fi
          done
```

How can I resolve this? If you propose a fix, please make it concise.

done
echo "Cleaned up preview deployments for ${BRANCH}"
echo "Deleted ${COUNT} preview deployments for ${BRANCH}"
Comment on lines +261 to +333
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Cloudflare Pages deployments list API pagination per_page

💡 Result:

Cloudflare Pages “List deployments” supports page-based pagination using the query params:

  • page — which page to fetch
  • per_page — how many deployments to return per page
  • (also) env — filter by "production" or "preview" deployments

Example:

curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/$PROJECT_NAME/deployments?page=1&per_page=50&env=preview" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"

The response includes result_info with count, page, per_page, and total_count (the docs’ example shows a default per_page of 20 when you don’t set it). [1]

Endpoint reference (Pages REST API): /client/v4/accounts/{account_id}/pages/projects/{project_name}/deployments. [2]

Note: This endpoint’s docs show total_count but not total_pages; if you need it, compute ceil(total_count / per_page). [1]

Sources:
[1] Cloudflare API – Pages → Projects → Deployments → “Get Deployments” (list)
[2] Cloudflare Pages docs – REST API (Pages API overview + deployments endpoint example)


Paginate the deployment listing before declaring cleanup complete.

The deployments endpoint is paginated. Fetching only ?per_page=100 inspects page 1 only, so older pr-* deployments on subsequent pages are missed and remain live while this step reports that nothing matched.

🔁 Minimal pagination sketch
-          RESPONSE=$(curl -s -w "\n%{http_code}" \
-            "${API_BASE}?per_page=100" \
-            -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
-          HTTP_CODE=$(echo "$RESPONSE" | tail -1)
-          BODY=$(echo "$RESPONSE" | sed '$d')
-
-          if [ "$HTTP_CODE" -ge 400 ]; then
-            echo "::warning::Failed to list deployments (HTTP ${HTTP_CODE})"
-            echo "$BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || true
-            exit 0
-          fi
-
-          # Wrangler direct-upload deployments store branch in deployment_trigger.metadata.branch
-          DEPLOYMENTS=$(echo "$BODY" | jq -r \
-            --arg branch "$BRANCH" \
-            '.result[] | select(.deployment_trigger.metadata.branch == $branch) | .id' 2>/dev/null) || true
+          PAGE=1
+          DEPLOYMENTS=""
+          while :; do
+            RESPONSE=$(curl -s -w "\n%{http_code}" \
+              "${API_BASE}?per_page=100&page=${PAGE}" \
+              -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
+            HTTP_CODE=$(echo "$RESPONSE" | tail -1)
+            BODY=$(echo "$RESPONSE" | sed '$d')
+
+            if [ "$HTTP_CODE" -ge 400 ]; then
+              echo "::warning::Failed to list deployments (HTTP ${HTTP_CODE})"
+              echo "$BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || true
+              exit 0
+            fi
+
+            PAGE_IDS=$(echo "$BODY" | jq -r \
+              --arg branch "$BRANCH" \
+              '.result[] | select(.deployment_trigger.metadata.branch == $branch) | .id' 2>/dev/null) || true
+            if [ -n "$PAGE_IDS" ]; then
+              DEPLOYMENTS="${DEPLOYMENTS}${DEPLOYMENTS:+$'\n'}${PAGE_IDS}"
+            fi
+
+            PAGE_COUNT=$(echo "$BODY" | jq -r '.result | length' 2>/dev/null || echo 0)
+            [ "$PAGE_COUNT" -lt 100 ] && break
+            PAGE=$((PAGE + 1))
+          done
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
API_BASE="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments"
# List deployments for this branch (per_page=100 to cover busy PRs)
# Note: Cloudflare prevents deleting the latest deployment per branch — that one will remain
DEPLOYMENTS=$(curl -sf \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments?per_page=100" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
| jq -r ".result[] | select(.deployment_trigger.metadata.branch == \"${BRANCH}\") | .id") || true
# Note: Cloudflare prevents deleting the most recent deployment per project — that one will remain
echo "Fetching deployments for branch ${BRANCH}..."
RESPONSE=$(curl -s -w "\n%{http_code}" \
"${API_BASE}?per_page=100" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Failed to list deployments (HTTP ${HTTP_CODE})"
echo "$BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || true
exit 0
fi
# Wrangler direct-upload deployments store branch in deployment_trigger.metadata.branch
DEPLOYMENTS=$(echo "$BODY" | jq -r \
--arg branch "$BRANCH" \
'.result[] | select(.deployment_trigger.metadata.branch == $branch) | .id' 2>/dev/null) || true
if [ -z "$DEPLOYMENTS" ]; then
echo "No deployments found for branch ${BRANCH}"
echo "Available branches:"
echo "$BODY" | jq -r '[.result[].deployment_trigger.metadata.branch // "null"] | unique[]' 2>/dev/null || true
exit 0
fi
COUNT=0
for DEPLOY_ID in $DEPLOYMENTS; do
echo "Deleting deployment ${DEPLOY_ID}..."
RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments/${DEPLOY_ID}?force=true" \
DEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"${API_BASE}/${DEPLOY_ID}?force=true" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${HTTP_CODE}) — may be the latest deployment (Cloudflare restriction)"
DEL_CODE=$(echo "$DEL_RESPONSE" | tail -1)
if [ "$DEL_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${DEL_CODE}) — may be the most recent deployment (Cloudflare restriction)"
else
COUNT=$((COUNT + 1))
fi
done
echo "Cleaned up preview deployments for ${BRANCH}"
echo "Deleted ${COUNT} preview deployments for ${BRANCH}"
API_BASE="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/synthorg-pr-preview/deployments"
# List deployments for this branch (per_page=100 to cover busy PRs)
# Note: Cloudflare prevents deleting the most recent deployment per project — that one will remain
echo "Fetching deployments for branch ${BRANCH}..."
PAGE=1
DEPLOYMENTS=""
while :; do
RESPONSE=$(curl -s -w "\n%{http_code}" \
"${API_BASE}?per_page=100&page=${PAGE}" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -ge 400 ]; then
echo "::warning::Failed to list deployments (HTTP ${HTTP_CODE})"
echo "$BODY" | jq -r '.errors[]?.message // empty' 2>/dev/null || true
exit 0
fi
PAGE_IDS=$(echo "$BODY" | jq -r \
--arg branch "$BRANCH" \
'.result[] | select(.deployment_trigger.metadata.branch == $branch) | .id' 2>/dev/null) || true
if [ -n "$PAGE_IDS" ]; then
DEPLOYMENTS="${DEPLOYMENTS}${DEPLOYMENTS:+$'\n'}${PAGE_IDS}"
fi
PAGE_COUNT=$(echo "$BODY" | jq -r '.result | length' 2>/dev/null || echo 0)
[ "$PAGE_COUNT" -lt 100 ] && break
PAGE=$((PAGE + 1))
done
if [ -z "$DEPLOYMENTS" ]; then
echo "No deployments found for branch ${BRANCH}"
echo "Available branches:"
echo "$BODY" | jq -r '[.result[].deployment_trigger.metadata.branch // "null"] | unique[]' 2>/dev/null || true
exit 0
fi
COUNT=0
for DEPLOY_ID in $DEPLOYMENTS; do
echo "Deleting deployment ${DEPLOY_ID}..."
DEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
"${API_BASE}/${DEPLOY_ID}?force=true" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")
DEL_CODE=$(echo "$DEL_RESPONSE" | tail -1)
if [ "$DEL_CODE" -ge 400 ]; then
echo "::warning::Failed to delete deployment ${DEPLOY_ID} (HTTP ${DEL_CODE}) — may be the most recent deployment (Cloudflare restriction)"
else
COUNT=$((COUNT + 1))
fi
done
echo "Deleted ${COUNT} preview deployments for ${BRANCH}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/pages-preview.yml around lines 260 - 302, The listing only
fetches a single page (per_page=100) so older deployments on subsequent pages
are missed; change the logic around API_BASE/RESPONSE/DEPLOYMENTS to loop over
pages (e.g. add a page counter/query param) fetching
"${API_BASE}?per_page=100&page=$PAGE" until an empty result set or no more
pages, concatenating .result[] ids into DEPLOYMENTS (or accumulate into a
DEPLOYMENTS variable/array), preserve the existing HTTP_CODE/ERROR handling for
each page, and then proceed to delete all accumulated DEPLOYMENTS and report
COUNT as before.

2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac553fd0d31 # v4
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
Loading