From 3300ed13eadfbed3acf1c9aa611be6d399eabace Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 19 Sep 2026 20:37:56 +0200 Subject: [PATCH] ci(release): only a confirmed 404 excuses a failed draft delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile and Codex both flagged the same hole in the sweep's error handling: `gh api` exits 1 for every failure alike, so a rate limit, permission loss or outage that hit the DELETE would hit the follow-up GET too — and the GET's nonzero exit was read as proof the draft was already gone. `failed` stayed 0 and the sweep went green over a draft it never deleted. Read the response status instead of the exit code. `gh api -i` prints the status line even for an error status, so a genuine 404 (the event job racing the sweep to the same draft) is distinguishable from everything else; a request that never got a response leaves the status empty, which is not 404 and so stays a failure. Verified against the live API: a deleted release re-checks as 404 and is excused, an existing release re-checks as 200 and fails the job, and an unreachable host yields no status and fails the job. Co-Authored-By: Claude Opus 5 --- .github/workflows/cleanup-pr-draft.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cleanup-pr-draft.yml b/.github/workflows/cleanup-pr-draft.yml index 9fdfc1fe7..4b442cfe3 100644 --- a/.github/workflows/cleanup-pr-draft.yml +++ b/.github/workflows/cleanup-pr-draft.yml @@ -166,10 +166,27 @@ jobs: if gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/${release_id}" >/dev/null; then echo "Deleted ${tag} (release ${release_id}) for closed PR #${pr_number}." - elif ! gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}" >/dev/null 2>&1; then + continue + fi + + # The delete failed. Only a release GitHub confirms is + # gone (404) excuses that — the event job racing us to + # the same draft. `gh api` exits 1 for every failure + # alike, so a rate limit or outage hitting both calls + # would otherwise read as "already deleted" and leave a + # green sweep behind an undeleted draft. Read the status + # line instead: `-i` prints it even on an error status, + # and a request that never got a response leaves it + # empty, which is not 404 and so stays a failure. + recheck_status="$( + gh api -i "repos/${GITHUB_REPOSITORY}/releases/${release_id}" 2>/dev/null | + sed -n '1s#^HTTP/[0-9.]* \([0-9]\{3\}\).*#\1#p' || true + )" + + if [ "${recheck_status}" = "404" ]; then echo "Draft ${tag} (release ${release_id}) was already gone." else - echo "::error::Failed to delete draft ${tag} (release ${release_id})." + echo "::error::Failed to delete draft ${tag} (release ${release_id}); re-check returned ${recheck_status:-no HTTP status}." failed=1 fi done <<< "${drafts}"