Skip to content

Preview reconcile

Preview reconcile #6

name: Preview reconcile
# `cleanup-preview.yml` deletes a preview when its pull request closes. That request can fail — most
# plausibly because Coolify was unreachable at the time — and nothing afterwards notices, so the
# preview keeps its cloned database and git checkout indefinitely. This re-checks on a schedule.
#
# Deleting an already-deleted preview answers 404, so 404 is the expected result rather than a fault.
on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
inputs:
days:
description: "How far back to look for closed pull requests"
type: number
default: 45
permissions:
contents: read
pull-requests: read
concurrency:
group: preview-reconcile
cancel-in-progress: false
jobs:
reconcile:
name: "Preview / Reconcile orphans"
runs-on: ubuntu-latest
if: vars.COOLIFY_URL != '' && vars.COOLIFY_APP_UUID != ''
timeout-minutes: 20
env:
COOLIFY_URL: ${{ vars.COOLIFY_URL }}
COOLIFY_APP_UUID: ${{ vars.COOLIFY_APP_UUID }}
COOLIFY_TOKEN: ${{ secrets.COOLIFY_API_TOKEN }}
GH_TOKEN: ${{ github.token }}
DAYS: ${{ inputs.days || 45 }}
steps:
- name: Reclaim previews whose pull request has closed
run: |
set -euo pipefail
if [ -z "${COOLIFY_TOKEN}" ]; then
echo "::notice::COOLIFY_API_TOKEN is not configured; nothing to reconcile against."
exit 0
fi
since=$(date -u -d "-${DAYS} days" +%Y-%m-%d)
closed=$(gh pr list --repo "${GITHUB_REPOSITORY}" --state closed \
--search "closed:>=${since}" --limit 500 --json number --jq '.[].number')
# --limit truncates; it is not the window. Passing --search also switches gh to
# relevance ordering, so a truncated page would drop arbitrary pull requests.
if [ "$(printf '%s\n' "${closed}" | grep -c .)" -ge 500 ]; then
echo "::error::More than 500 pull requests closed since ${since}; raise --limit."
exit 1
fi
reclaimed=0
checked=0
failed=0
summary="${RUNNER_TEMP}/summary.md"
body="${RUNNER_TEMP}/response.json"
{
echo "| PR | Result |"
echo "|---:|---|"
} > "${summary}"
while read -r number; do
[ -n "${number:-}" ] || continue
checked=$((checked + 1))
status=$(curl -sS -o "${body}" -w '%{http_code}' -X DELETE \
-H "Authorization: Bearer ${COOLIFY_TOKEN}" \
-H 'Accept: application/json' \
--retry 3 --retry-connrefused --max-time 15 \
"${COOLIFY_URL}/api/v1/applications/${COOLIFY_APP_UUID}/previews/${number}") || status=000
case "${status}" in
404)
: # already clean
;;
2*)
reclaimed=$((reclaimed + 1))
echo "| #${number} | reclaimed |" >> "${summary}"
echo "::warning::Reclaimed an orphaned preview for closed PR #${number}; cleanup on close did not reach Coolify."
;;
*)
# Stop on the first failure: an unreachable Coolify is one finding, not one per
# pull request. The summary below still reports what was reclaimed before it.
failed=1
echo "| #${number} | HTTP ${status} |" >> "${summary}"
echo "::error::Coolify returned HTTP ${status} for PR #${number}: $(head -c 200 "${body}" | tr '\n' ' ')"
break
;;
esac
done <<< "${closed}"
{
echo "## Preview reconcile"
echo
echo "Checked **${checked}** pull requests closed in the last ${DAYS} days."
echo
if [ "${reclaimed}" -eq 0 ] && [ "${failed}" -eq 0 ]; then
echo "No orphaned previews."
else
cat "${summary}"
fi
} >> "${GITHUB_STEP_SUMMARY}"
echo "checked=${checked} reclaimed=${reclaimed} failed=${failed}"
[ "${failed}" -eq 0 ]