From 0621383a4a06a5fcb1dc1d161c1efe29dee200c1 Mon Sep 17 00:00:00 2001 From: Martin Simon Date: Thu, 3 Sep 2026 08:29:39 +0200 Subject: [PATCH] Report what GitHub said when the commit request fails, and retry a transient one --fail-with-body writes the error body and exits non-zero. Under set -e that exit skipped the reporting below it and the EXIT trap then deleted the file, so an auth failure or a 502 surfaced as curl's generic "returned error: NNN" and nothing GitHub actually said. The careful error handling that exists only ever ran on HTTP 200, which is where GraphQL puts most of its errors -- but not 401s, rate limits or 5xx. The request also had no timeout and no retry, and it runs after the archive has already been published to R2: a transient failure here leaves the bucket ahead of both state branches until the next ingest repairs it. A retry is safe rather than merely convenient, because expectedHeadOid makes the mutation conditional -- if the first attempt did land, the retry is refused for the right reason instead of committing twice. --- commit-branch.sh | 21 +++++++++++++++++++-- tests/run.sh | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/commit-branch.sh b/commit-branch.sh index 034da93..03aebe7 100755 --- a/commit-branch.sh +++ b/commit-branch.sh @@ -119,10 +119,27 @@ sys.stderr.write(f"{len(additions)} addition(s), {len(deletions)} deletion(s)\n" log "committing to $BRANCH, $(wc -c < "$payload") byte payload" -curl -sS --fail-with-body -X POST "$API/graphql" \ +# --fail-with-body writes the body and exits non-zero, and under set -e that +# exit skipped the reporting below and the trap deleted the file: an auth +# failure or a 502 showed curl's generic "returned error: NNN" and nothing +# GitHub actually said. The body is printed here instead, where it is still on +# disk. +# +# --retry covers the transient half. This runs after the archive has already +# been published to R2, so failing here leaves the bucket ahead of the state +# branches until the next ingest repairs it. A retry is safe rather than +# merely convenient: expectedHeadOid makes the mutation conditional, so if the +# first attempt did land, the retry is refused for the right reason instead of +# committing twice. +if ! curl -sS --fail-with-body -X POST "$API/graphql" \ + --max-time 120 --retry 3 --retry-connrefused --retry-all-errors \ -H "Authorization: bearer $GITHUB_TOKEN" \ -H 'Content-Type: application/json' \ - --data @"$payload" > "$response" + --data @"$payload" > "$response"; then + log "FATAL: the GraphQL request to $API failed. What it returned:" + cat "$response" >&2 + exit 1 +fi python3 - "$response" <<'PY' import json, sys diff --git a/tests/run.sh b/tests/run.sh index 0c02d1c..07682c1 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -38,7 +38,7 @@ eq() { # label expected actual # assertions vanish rather than fail -- which is exactly what happened when # these groups were first lifted out of pkghaus/apt: eq() was left behind and # the suite still printed "all tests passed". Bump this when adding one. -EXPECTED_ASSERTIONS=5 +EXPECTED_ASSERTIONS=8 echo "the signed-commit payload describes every change, deletions included" ( @@ -139,6 +139,47 @@ echo "a tree with no changes makes no commit" exit $((fail > 0)) ) || fail=$((fail + 1)) +echo "an HTTP failure reports what GitHub said, not just curl's exit code" +( + work="$(mktemp -d)" + export GITHUB_TOKEN=fake GITHUB_REPOSITORY=pkghaus/apt + + repo="$work/repo"; mkdir -p "$repo"; cd "$repo" + git init -q -b archive . + printf 'one\n' > keep.txt + git add -A + git -c user.name=t -c user.email=t@example.invalid commit -qm base + printf 'two\n' > keep.txt + + # curl as --fail-with-body behaves on an HTTP error: the body is written + # to the output AND the exit status is non-zero. That combination is what + # used to lose the message -- set -e took the exit before anything printed + # the body, and the trap then deleted the file. + mkdir -p "$work/bin" + cat > "$work/bin/curl" <<'FAKE' +#!/bin/sh +printf '{"message":"Bad credentials","documentation_url":"https://docs.github.com/graphql"}' +exit 22 +FAKE + chmod +x "$work/bin/curl" + + out="$(PATH="$work/bin:$PATH" "$ROOT/commit-branch.sh" "$repo" archive "test" 2>&1)" \ + && rc=0 || rc=$? + + eq "the run fails" "1" "${rc:-0}" + case "$out" in + *"Bad credentials"*) ok "GitHub's own message reaches the log" ;; + *) no "GitHub's own message reaches the log" "got [$out]" ;; + esac + case "$out" in + *FATAL*) ok "and it is labelled as the failure it is" ;; + *) no "and it is labelled as the failure it is" "got [$out]" ;; + esac + + cd /; rm -rf "$work" + exit $((fail > 0)) +) || fail=$((fail + 1)) + echo ran="$(wc -l < "$TALLY")" if [ "$ran" -ne "$EXPECTED_ASSERTIONS" ]; then