ci(r2): stream PUT bodies so the 30 s timeout bounds idle gaps, not the whole nightly DMG upload
The NEXT nightly's publish step failed four times in a row in run https://github.com/manaflow-ai/cmux/actions/runs/41814490749 (job 126531167053):
R2 upload of s3://cmux-nightly-next/nightly-next/next/cmux-nightly-next-macos-arm64-4180181136201.dmg attempt 1 of 4 failed (<urlopen error The write operation timed out>)
Cause: _upload_once passes the whole file as one bytes body with timeout=30. http.client sends a bytes body with a single sock.sendall(), and since Python 3.5 a socket timeout bounds the whole sendall, not each idle gap. So any object that takes more than 30 s to send fails every attempt, however healthy the link is. The appcasts are small and pass, and the DMG is big enough that it can't.
Fix: send PUT bodies (plain, write-once and --replace-sha256) in 1 MiB pieces with an explicit Content-Length. Each piece gets the timeout (CMUX_R2_UPLOAD_IDLE_SECONDS, default 30), so a transfer that keeps moving finishes. The SigV4 signature covers the body's SHA-256, not its framing, so signing and the write-once/If-Match semantics are unchanged.
Test: test_a_slow_large_put_is_bounded_by_idle_gaps_not_total_time. The local TLS endpoint drains a 12 MiB body over about 5 s with a 2 s idle timeout. With the old framing it fails with the production error (<urlopen error The write operation timed out>), and with the fix it passes. It also asserts the body is not sent chunked. python3 tests/test_r2_upload_requests.py: 27 OK.
Small change, confined to one script and its test.
Agent session: agent session
🤖 Generated with Claude Code
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes the NEXT nightly's R2 publish step, which failed all four attempts with "The write operation timed out" because the 30 s socket timeout bounded the entire sendall() of the DMG's body. PUT bodies now stream in 1 MiB chunks with an explicit Content-Length, so the timeout (CMUX_R2_UPLOAD_IDLE_SECONDS, default 30) only applies to idle gaps and transfers that keep moving finish.
- Adds a test that slowly drains a 12 MiB body under the idle timeout and asserts the upload succeeds without chunked transfer encoding.