diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1306ae7..d041440 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -395,17 +395,74 @@ jobs: # corrupt the Authorization header. CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')" - REL_ID="$(curl -sSf -X POST "${API}/releases" \ - -H "Authorization: token ${CI_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "$(jq -n --arg tag "$TAG" --arg body "$BODY" \ - '{tag_name:$tag, name:$tag, body:$body, draft:false, prerelease:false}')" \ - | jq -r '.id')" + PAYLOAD="$(jq -n --arg tag "$TAG" --arg body "$BODY" \ + '{tag_name:$tag, name:$tag, body:$body, draft:false, prerelease:false}')" + + # This POST is the step that orphaned tag v0.1.1 (run 75): it landed one + # second after the tag push and Gitea answered 500, having not finished + # processing the pushed tag. Re-running the workflow published the same + # four assets untouched, so the failure was a race, not a bad request. + # + # Two things went wrong there, and both are fixed here. + # + # 1. `curl -sSf` prints NO response body on an error status, so all the + # log carried was "curl: (22) ... error: 500" and the cause had to be + # inferred from timestamps. Capture the body and print it. + # 2. Nothing retried, so a transient 5xx became a permanent orphan tag. + # The plan step CAN recover one, but only on a run that reaches it -- + # and a later push with no releasable commits stands down before it + # gets there, so in practice the tag sits until a human notices. + # + # 4xx is deliberately NOT retried: a bad token or a malformed body does + # not improve by being sent again, and retrying only turns a clear + # failure into a slow one. + REL_ID="" + for attempt in 1 2 3 4 5; do + HTTP="$(curl -s -o /tmp/rel.json -w '%{http_code}' -X POST "${API}/releases" \ + -H "Authorization: token ${CI_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "${PAYLOAD}" || echo 000)" + + if [ "$HTTP" = "201" ] || [ "$HTTP" = "200" ]; then + REL_ID="$(jq -r '.id' /tmp/rel.json)" + break + fi + + echo "::warning::POST /releases attempt ${attempt} returned HTTP ${HTTP}" + echo "--- response body ---" + cat /tmp/rel.json || true + echo + echo "---------------------" + + case "$HTTP" in + 4*) echo "::error::HTTP ${HTTP} is a client error - not retrying."; exit 1 ;; + esac + + if [ "$attempt" = 5 ]; then + echo "::error::POST /releases still failing after 5 attempts. Tag ${TAG} is pushed but has no release." + echo "::error::Re-run this workflow - the plan step detects the orphan tag and republishes it." + exit 1 + fi + sleep $(( attempt * 5 )) + done + + if [ -z "$REL_ID" ] || [ "$REL_ID" = "null" ]; then + echo "::error::Release created but no id came back; refusing to upload assets blind." + exit 1 + fi echo "Created release ${TAG} (id=${REL_ID})" for f in "${BIN}-linux-x86_64" "${BIN}-linux-aarch64" "${BIN}-windows-x86_64.exe" SHA256SUMS; do - curl -sSf -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \ + # Same treatment. An upload that fails quietly leaves a release whose + # SHA256SUMS does not cover every binary it advertises, which is worse + # than no release at all -- that file IS the trust anchor. + HTTP="$(curl -s -o /tmp/asset.json -w '%{http_code}' -X POST "${API}/releases/${REL_ID}/assets?name=${f}" \ -H "Authorization: token ${CI_TOKEN}" \ - -F "attachment=@dist/${f}" >/dev/null + -F "attachment=@dist/${f}" || echo 000)" + if [ "$HTTP" != "201" ] && [ "$HTTP" != "200" ]; then + echo "::error::uploading ${f} returned HTTP ${HTTP}" + cat /tmp/asset.json || true + exit 1 + fi echo " uploaded ${f}" done