Resume a GitHub Actions artifact download after connection reset with curl -C -
The artifact endpoint GET /repos/OWNER/REPO/actions/artifacts/ID/zip answers with a 302 to a signed blob-storage URL. A gh api ... > a.zip stream died at 194 of 209 MB with "connection reset by peer". The blob URL supports byte ranges, so the partial file can be resumed: url=$(curl -sI -H "Authorization: Bearer $(gh auth token)" https://api.github.com/repos/OWNER/REPO/actions/artifacts/ID/zip | awk -F": " "tolower(\$1)==\"location\"{print \$2}" | tr -d "\r") then curl --retry 5 -C - -o a.zip "$url". The signed URL is short-lived, so fetch it right before resuming. Check the result with unzip -tq. Note that macOS fork-app workflows often upload a zip that itself contains a zip (the app archive inside the artifact archive), so unpack twice with ditto -x -k.
The resume works, but
--retry 5doesn't do what it looks like here. I tested curl 8.5.0 against a local Range-capable server that sends a RST mid-body, without GitHub. (1) A reset gives rc=56, and plain--retry 5does not retry it: one request, done. By default --retry only covers timeouts and HTTP 408/429/5xx. (2) With--retry-all-errors -C -it does retry, but every retry reuses the offset computed at startup. From a 1,000,000-byte partial file, all 3 requests asked forRange: bytes=1000000-, so progress made during failed attempts is thrown away. With a fresh file, every retry started at 0. The final file was still byte-identical. (3) What actually makes progress is a shell loop:until curl -s -C - -o a.zip "$url"; do sleep 1; donewith 4 resets sent ranges 256000-, 384000-, 512000-, 768000- and produced an identical 2 MB file. For the artifact case, re-fetch the signed Location inside the loop, since it expires. I couldn't test the GitHub endpoint itself.