Files
sure/.github/workflows/preview-cleanup.yml

217 lines
7.9 KiB
YAML

name: Cleanup PR Previews
on:
# Run hourly to check for expired previews
schedule:
- cron: '0 * * * *'
# Immediately cleanup when PR is closed
pull_request:
types: [closed, unlabeled]
# Allow manual trigger
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to cleanup (optional, cleans all expired if empty)'
required: false
type: string
permissions:
contents: read
deployments: write
jobs:
cleanup-on-close:
name: Cleanup closed PR preview
if: github.event_name == 'pull_request' && (github.event.action == 'closed' || (github.event.action == 'unlabeled' && github.event.label.name == 'preview-cf'))
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "24"
- name: Install Wrangler
run: npm install -g wrangler
- name: Delete preview Worker
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
WORKER_NAME="sure-preview-${{ github.event.pull_request.number }}"
echo "Deleting Worker: $WORKER_NAME"
# Delete the worker (this also stops any running containers)
wrangler delete --name "$WORKER_NAME" --force || echo "Worker may not exist"
- name: Delete GitHub Deployment
uses: actions/github-script@v7
with:
script: |
const environment = `preview-pr-${{ github.event.pull_request.number }}`;
const description = context.payload.action === 'closed'
? 'PR closed - preview deleted'
: 'preview-cf label removed - preview deleted';
try {
// Get deployments for this environment
const { data: deployments } = await github.rest.repos.listDeployments({
owner: context.repo.owner,
repo: context.repo.repo,
environment: environment
});
// Mark all deployments as inactive
for (const deployment of deployments) {
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.id,
state: 'inactive',
description
});
}
console.log(`Marked ${deployments.length} deployments as inactive`);
} catch (error) {
console.log('No deployments to cleanup or error:', error.message);
}
cleanup-expired:
name: Cleanup expired previews
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "24"
- name: Install Wrangler
run: npm install -g wrangler
- name: Cleanup expired previews
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_INPUT: ${{ inputs.pr_number }}
run: |
# If specific PR number provided, only cleanup that one
if [ -n "$PR_INPUT" ]; then
if [[ "$PR_INPUT" =~ ^[1-9][0-9]*$ ]]; then
PR_NUM="$PR_INPUT"
WORKER_NAME="sure-preview-$PR_NUM"
echo "Manually deleting Worker: $WORKER_NAME"
wrangler delete --name "$WORKER_NAME" --force || echo "Worker may not exist"
# Cleanup GitHub deployment for this PR
echo "Cleaning up GitHub deployment for PR #$PR_NUM"
gh api \
-X GET "/repos/${{ github.repository }}/deployments?environment=preview-pr-$PR_NUM" \
--jq '.[].id' 2>/dev/null | while read -r DEPLOY_ID; do
if [ -n "$DEPLOY_ID" ]; then
gh api \
-X POST "/repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses" \
-f state=inactive \
-f description="Preview manually deleted" || true
fi
done || echo "No deployments to cleanup or error occurred"
else
echo "Invalid PR number input '$PR_INPUT'; skipping manual cleanup"
fi
exit 0
fi
# Get list of all preview workers
echo "Fetching list of preview workers..."
# Use Cloudflare API to list workers and read modified_on from the list response.
# The per-script endpoint returns raw script content, not JSON metadata.
WORKERS_RESPONSE=$(curl -fsS -X GET \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json") || {
echo "Failed to fetch preview worker list from Cloudflare"
exit 1
}
if ! echo "$WORKERS_RESPONSE" | jq -e '.success == true and (.result | type == "array")' >/dev/null 2>&1; then
echo "Cloudflare API returned an invalid worker list response"
echo "$WORKERS_RESPONSE" | jq -c '.errors // .'
exit 1
fi
WORKERS=$(echo "$WORKERS_RESPONSE" | jq -r '
.result[]
| select(.id | startswith("sure-preview-"))
| [.id, (.modified_on // "")]
| @tsv
')
if [ -z "$WORKERS" ]; then
echo "No preview workers found"
exit 0
fi
echo "Found preview workers:"
echo "$WORKERS" | cut -f1
# Check each worker's deployment time
CUTOFF_TIME=$(date -d '24 hours ago' +%s)
while IFS=$'\t' read -r WORKER MODIFIED_ON; do
[ -n "$WORKER" ] || continue
echo "Checking $WORKER..."
if [ -z "$MODIFIED_ON" ]; then
echo "No modified_on timestamp for $WORKER; skipping"
continue
fi
if ! MODIFIED_TS=$(date -d "$MODIFIED_ON" +%s 2>/dev/null); then
echo "Invalid modified_on timestamp for $WORKER ($MODIFIED_ON); skipping"
continue
fi
if [ "$MODIFIED_TS" -lt "$CUTOFF_TIME" ]; then
echo "Worker $WORKER is older than 24 hours, deleting..."
if wrangler delete --name "$WORKER" --force; then
# Extract PR number and cleanup GitHub deployment
PR_NUM=$(echo "$WORKER" | sed 's/sure-preview-//')
if [[ "$PR_NUM" =~ ^[1-9][0-9]*$ ]]; then
echo "Cleaning up GitHub deployment for PR #$PR_NUM"
gh api \
-X GET "/repos/${{ github.repository }}/deployments?environment=preview-pr-$PR_NUM" \
--jq '.[].id' 2>/dev/null | while read -r DEPLOY_ID; do
gh api \
-X POST "/repos/${{ github.repository }}/deployments/$DEPLOY_ID/statuses" \
-f state=inactive \
-f description="Preview expired after 24 hours" || true
done || echo "No deployments to cleanup or error occurred"
else
echo "Could not extract a valid PR number from $WORKER; skipping deployment cleanup"
fi
else
echo "Failed to delete $WORKER; skipping deployment status update"
fi
else
echo "Worker $WORKER is still within 24-hour window, keeping..."
fi
done <<< "$WORKERS"
echo "Cleanup complete"