pr-media: read and write the media branch with git, falling back to GraphQL (carries #28265)
Carries #28265 (its commits, with their authorship), plus a fix for the two blocking findings from its review at 332f4a45 (https://github.com/manaflow-ai/cmux/pull/28265#issuecomment-6538577666). #28265's lane has been silent for about 10 hours, and PR screenshots still can't be uploaded from agent hosts.
Order behind #28265 (fanout-infra-3's coordinator): "since this morning the glaeda-gh shim refuses source reads over the API, and that breaks pr-media.py's existence check (gh api repos/manaflow-ai/cmux/contents/...?ref=pr-media). So cmux-ci capture --pr and pr-media uploads fail for every lane."
What #28265 does
scripts/pr-media.py reads and writes the pr-media branch with git through a shared cache clone on the host. The clone is bare, depth 1 and blob-less. A write is one mktree --missing commit on the fetched tip, pushed without force, and a lost race is built again on the new tip. No contents/ read is left. See #28265 for the details: the credential handling, the compare-and-swap without --force, and put_file replacing as it does on base.
What this PR adds
- [P1] The no-retry test counted subprocess's own wait.
mock.patch.object(MODULE.time, "sleep")patchedtime.sleepfor the whole interpreter, andsubprocess.runcalls it while it waits ongit push. The wait before a retry is now its own function,retry_pause(), and the tests patch that function, so they count retries and nothing else. - [P1] The API fallback is back, through a path the broker allows. A
GitTransportErrormeansls-remote,fetchorpushcould not reach or authenticate to the remote, or timed out. When one is raised, the same call goes toApiBranch:- Reads use
gh api graphql: the branch'sref(qualifiedName:), andobject(expression: "<commit sha>:<path>") { __typename oid }for a path's blob sha. Neither reads file content, so neither is thecontents/source read the broker refuses, and neither spends REST quota. Checked on cosmic-gray against the real repository: the blob sha is751f14e7…for28064/cap-51810e73-steps-55d3d0318c-dark.png, the same sha git gives, and the reads return None for a folder and for a missing name. - The write is one
createCommitOnBranchwithexpectedHeadOid. A batch stays one commit, a lost race is retried on the new tip and never forced, and the no-replace check (expect) runs again on every tip. - The ref read sets
GLAEDA_GH_CACHE_TTL=0, because a cached tip would lose every race. Object reads name a commit sha, so a cached answer to one is still correct. - Some failures stay final and never go to the API: a push the remote answered and refused (
[remote rejected],hook declined), a race lost on every attempt, a missing branch, and a path that is a folder. - Git is tried once per run. After it fails, the later calls in that run go straight to the API instead of waiting out another timeout.
- When both git and the API fail, the error says what each one said, and it never says "create it".
- The privacy check is unchanged.
put_filesruns it before either writer.
- Reads use
- [P2, follow-up] #28892: the cache clone keeps every blob that was uploaded.
Tests
python3 tests/test_pr_media.py: 89 tests, OK on this head (cosmic-gray). tests/test_ci_pr_media_privacy.py and tests/test_ci_no_shallow_dev_fetch.py also pass.
The new tests use FakeApi, a stand-in for gh api graphql --input - over the same bare origin. It answers only the ref, object and pull-request queries and createCommitOnBranch, checks expectedHeadOid, and fails the test on any other gh call. The tests:
- With git unreachable: the reads (branch, pull request, blob sha, folder, missing path) go through GraphQL, and no query asks for file content.
- With git unreachable: a batch lands as one commit; an existing file needs
--force; a file taken after the check is never replaced; a lost race is rebuilt on the new tip; a folder is never overwritten; library callers still get the privacy check. - Git is tried once per run.
- The API is never called while git works, when a hook refuses the push, or when every race is lost.
- With no git and no gh login, the error names both failures.
Red and green for the first P1: the review saw the test fail at 332f4a45. On this Mac it passed at that head too, because whether subprocess sleeps while it waits depends on the platform. A Linux run of the suite (cmux-ci test -- python3 tests/test_pr_media.py) is in the Observed section.
Observed / Not run
- Observed: the suites above, run locally; the GraphQL reads against manaflow-ai/cmux (tip, blob sha, folder, missing name, PR #28059 found, #999999 missing).
- Observed after merge: a real upload for #28059 from a feat-cmux-next checkout (git path).
- Not run: a real
createCommitOnBranchagainst GitHub. It is covered only by FakeApi. GitHub's GraphQL request size limit may refuse a very large mp4 on the fallback path; screenshots and gifs (5 MiB cap) are well inside it.
Changelog
none
risk: normal (CI tooling, no app code)
Agent session: agent session
🤖 Generated with Claude Code