Cleanup preview DNS records #87
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Cleanup preview DNS records | |
| on: | |
| # Run when branches are deleted | |
| delete: | |
| # Run when PRs are closed (merged or abandoned) | |
| pull_request: | |
| types: [closed] | |
| # Weekly cleanup for any orphaned records | |
| schedule: | |
| - cron: '0 3 * * 0' # Sunday 3am UTC | |
| workflow_dispatch: | |
| jobs: | |
| cleanup: | |
| runs-on: ubuntu-latest | |
| # Only run for branch deletions (not tag deletions) | |
| if: github.event_name != 'delete' || github.event.ref_type == 'branch' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Delete preview domain | |
| if: github.event_name == 'pull_request' || github.event_name == 'delete' | |
| env: | |
| CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CF_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} | |
| run: | | |
| if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ACCOUNT_ID" ]; then | |
| echo "::warning::Cloudflare secrets not fully configured, skipping" | |
| exit 0 | |
| fi | |
| # Determine branch name based on event type | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BRANCH=$(echo "${{ github.head_ref }}" | tr '/' '-' | tr '[:upper:]' '[:lower:]' | cut -c1-28) | |
| else | |
| BRANCH=$(echo "${{ github.event.ref }}" | tr '/' '-' | tr '[:upper:]' '[:lower:]' | cut -c1-28) | |
| fi | |
| FQDN="${BRANCH}-demo.osc.earth" | |
| PROJECT="osa-demo" | |
| # Skip protected branches | |
| if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "develop" ]; then | |
| echo "Skipping protected branch: ${BRANCH}" | |
| exit 0 | |
| fi | |
| # Remove Pages custom domain first (404 is expected if already removed) | |
| echo "Removing Pages custom domain: ${FQDN}" | |
| HTTP_CODE=$(curl -s -o /tmp/pages_resp.json -w "%{http_code}" -X DELETE \ | |
| "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/${PROJECT}/domains/${FQDN}" \ | |
| -H "Authorization: Bearer ${CF_API_TOKEN}" \ | |
| -H "Content-Type: application/json") | |
| if [ "$HTTP_CODE" = "200" ]; then | |
| echo "Pages custom domain removed." | |
| elif [ "$HTTP_CODE" = "404" ]; then | |
| echo "Pages domain not found (already removed)." | |
| else | |
| echo "::warning::Failed to remove Pages domain ${FQDN} (HTTP ${HTTP_CODE})" | |
| fi | |
| # Then remove DNS record | |
| echo "Looking for DNS record: ${FQDN}" | |
| RECORDS=$(curl -sf -X GET \ | |
| "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?name=${FQDN}&type=CNAME" \ | |
| -H "Authorization: Bearer ${CF_API_TOKEN}" \ | |
| -H "Content-Type: application/json") || { | |
| echo "::warning::Failed to query DNS records for ${FQDN}" | |
| exit 0 | |
| } | |
| RECORD_ID=$(echo "$RECORDS" | jq -r '.result[0].id // empty') | |
| if [ -n "$RECORD_ID" ]; then | |
| echo "Deleting DNS record: ${FQDN} (${RECORD_ID})" | |
| if curl -sf -X DELETE \ | |
| "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${RECORD_ID}" \ | |
| -H "Authorization: Bearer ${CF_API_TOKEN}" \ | |
| -H "Content-Type: application/json"; then | |
| echo "Deleted." | |
| else | |
| echo "::warning::Failed to delete DNS record ${RECORD_ID} for ${FQDN}" | |
| fi | |
| else | |
| echo "No DNS record found for ${FQDN}" | |
| fi | |
| - name: Periodic cleanup of orphaned records | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| env: | |
| CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CF_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| if [ -z "$CF_ZONE_ID" ] || [ -z "$CF_API_TOKEN" ] || [ -z "$CF_ACCOUNT_ID" ]; then | |
| echo "::warning::Cloudflare secrets not fully configured, skipping" | |
| exit 0 | |
| fi | |
| PROJECT="osa-demo" | |
| echo "Fetching all *-demo.osc.earth CNAME records..." | |
| RECORDS=$(curl -sf -X GET \ | |
| "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=CNAME&per_page=100" \ | |
| -H "Authorization: Bearer ${CF_API_TOKEN}" \ | |
| -H "Content-Type: application/json") || { | |
| echo "::error::Failed to fetch DNS records from Cloudflare" | |
| exit 1 | |
| } | |
| # Pre-fetch all branch names to avoid per-record API calls | |
| # If this fails, abort to avoid accidentally deleting active records | |
| BRANCHES=$(gh api "repos/${{ github.repository }}/branches" --paginate -q '.[].name') || { | |
| echo "::error::Failed to list branches from GitHub API. Aborting to avoid accidental deletions." | |
| exit 1 | |
| } | |
| # Filter to *-demo.osc.earth records (excluding demo.osc.earth itself) | |
| echo "$RECORDS" | jq -r '.result[] | select(.name | endswith("-demo.osc.earth")) | "\(.id) \(.name)"' | while read -r RECORD_ID FQDN; do | |
| # Extract branch name: remove -demo.osc.earth suffix | |
| BRANCH=$(echo "$FQDN" | sed 's/-demo\.osc\.earth$//') | |
| # Skip protected branches | |
| if [ "$BRANCH" = "develop" ]; then | |
| echo "Keeping: ${FQDN} (protected branch)" | |
| continue | |
| fi | |
| # Check if any branch sanitizes to this name | |
| FOUND=false | |
| while IFS= read -r REMOTE_BRANCH; do | |
| SANITIZED=$(echo "$REMOTE_BRANCH" | tr '/' '-' | tr '[:upper:]' '[:lower:]' | cut -c1-28) | |
| if [ "$SANITIZED" = "$BRANCH" ]; then | |
| FOUND=true | |
| break | |
| fi | |
| done <<< "$BRANCHES" | |
| if [ "$FOUND" = "true" ]; then | |
| echo "Keeping: ${FQDN} (branch exists)" | |
| else | |
| echo "Deleting orphaned: ${FQDN} (no matching branch)" | |
| # Remove Pages custom domain (may not exist, log but continue) | |
| if ! curl -sf -X DELETE \ | |
| "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/${PROJECT}/domains/${FQDN}" \ | |
| -H "Authorization: Bearer ${CF_API_TOKEN}" \ | |
| -H "Content-Type: application/json"; then | |
| echo "::warning::Failed to remove Pages domain ${FQDN} (may not exist)" | |
| fi | |
| # Remove DNS record | |
| if curl -sf -X DELETE \ | |
| "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${RECORD_ID}" \ | |
| -H "Authorization: Bearer ${CF_API_TOKEN}" \ | |
| -H "Content-Type: application/json"; then | |
| echo "Deleted: ${FQDN}" | |
| else | |
| echo "::warning::Failed to delete DNS record ${RECORD_ID} for ${FQDN}" | |
| fi | |
| fi | |
| done | |
| echo "Cleanup complete." |