Skip to content

Commit 6dc9f97

Browse files
fix(publish): verify uploads via the metadata API, not S3 HEAD (#310)
Three red publish attempts today (run 29129693269) on an ISO that was published and intact. s3.us.archive.org started answering HEAD with a 307 to the storage node; without -L the probe read the redirect's own headers (content-length: 403) so the size never matched, and following the redirect is no better (the node answers HEAD with Content-Length: 1). The PUTs themselves succeeded (rc=0): only the verify kept lying. already_uploaded() now asks https://archive.org/metadata/<identifier> for the file's size and md5: no redirects, values computed by IA from the bytes it actually stored, and it appears only once ingest is done, which is exactly the completeness signal we want. The 2026-07-06 multipart-etag heuristics become obsolete. Every mismatch now logs its reason: the silent return 1 cost hours of blind diagnosis today. Tested live against margine-live-iso-20260711: match path returns already-uploaded, and a wrong local md5 correctly reports the mismatch.
1 parent 5be12b4 commit 6dc9f97

1 file changed

Lines changed: 28 additions & 18 deletions

File tree

.github/scripts/ia-upload-iso.sh

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,25 +63,35 @@ echo "hashing local file (md5, for the verify short-circuit)..."
6363
MD5="$(md5sum "$FILE" | awk '{print $1}')"
6464

6565
already_uploaded() {
66-
# Complete when the S3 object's size matches and, if an ETag comes
67-
# back, its md5 matches too (missing/opaque ETag falls back to size).
68-
local headers size etag
69-
headers="$(curl -fsSI -m 60 \
70-
-H "authorization: LOW ${IA_S3_ACCESS}:${IA_S3_SECRET}" \
71-
"${BASE}/${IDENTIFIER}/${REMOTE}" 2>/dev/null)" || return 1
72-
size="$(printf '%s' "$headers" | tr -d '\r' | awk 'tolower($1)=="content-length:" {print $2}' | tail -1)"
73-
etag="$(printf '%s' "$headers" | tr -d '\r' | awk 'tolower($1)=="etag:" {gsub(/"/, "", $2); print $2}' | tail -1)"
74-
[ "$size" = "$SIZE" ] || return 1
75-
# IA stores a large file with a MULTIPART etag (e.g. "<hash>-<parts>"),
76-
# not the plain content md5, so the etag is only authoritative when it IS
77-
# a 32-hex md5. Otherwise trust the size match (a truncated upload changes
78-
# the size). Without this, a multi-GB ISO uploads fine but the verify never
79-
# matches and the job false-fails even though the ISO published (run
80-
# 28779615535, 2026-07-06: the 8.9 GB stable ISO landed + IA derived the
81-
# torrent, yet the publish job went red and the site bump never ran).
82-
if printf '%s' "$etag" | grep -qE '^[0-9a-f]{32}$' && [ "$etag" != "$MD5" ]; then
83-
return 1
66+
# Complete when the item METADATA lists the file with matching size and
67+
# md5. This replaced the S3 HEAD probe on 2026-07-11 after it silently
68+
# broke the verify for a whole day (run 29129693269, 3 red attempts on
69+
# an ISO that was PUBLISHED and intact): s3.us.archive.org started
70+
# answering HEAD with a 307 to the storage node, and without -L the
71+
# probe read the redirect's own headers (content-length: 403) so the
72+
# size never matched; following the redirect is no better, the node
73+
# answers HEAD with Content-Length: 1. The metadata API has neither
74+
# problem: no redirects, and explicit size + md5 that IA computed from
75+
# the bytes it actually stored. The old etag heuristics (multipart
76+
# etags, 2026-07-06) are obsolete with it. Metadata appears only after
77+
# IA finishes ingesting, which is exactly the "complete" we want; the
78+
# caller's retry loop rides out the ingest delay. Every mismatch is
79+
# LOGGED: the silent `return 1` cost hours of blind diagnosis today.
80+
local meta size md5
81+
meta="$(curl -fsS -m 60 "https://archive.org/metadata/${IDENTIFIER}" 2>/dev/null)" || {
82+
echo "verify: metadata fetch failed (transient?)"; return 1; }
83+
size="$(jq -r --arg n "$REMOTE" '.files[]? | select(.name==$n) | .size' <<<"$meta" 2>/dev/null | head -1)"
84+
md5="$(jq -r --arg n "$REMOTE" '.files[]? | select(.name==$n) | .md5' <<<"$meta" 2>/dev/null | head -1)"
85+
if [ -z "$size" ] || [ "$size" = "null" ]; then
86+
echo "verify: ${REMOTE} not in item metadata yet (still ingesting?)"; return 1
87+
fi
88+
if [ "$size" != "$SIZE" ]; then
89+
echo "verify: size mismatch (remote ${size} vs local ${SIZE})"; return 1
90+
fi
91+
if [ -n "$md5" ] && [ "$md5" != "null" ] && [ "$md5" != "$MD5" ]; then
92+
echo "verify: md5 mismatch (remote ${md5} vs local ${MD5}) — remote copy is corrupt, re-uploading"; return 1
8493
fi
94+
echo "verify: metadata match (size + md5) — remote object is complete"
8595
return 0
8696
}
8797

0 commit comments

Comments
 (0)