Skip to content

perf: send GitHub only the changed paths when publishing a tree - #73

Merged
zhaozhaozz merged 1 commit into
mainfrom
perf/incremental-tree
Oct 28, 2026
Merged

zhaozhaozz merged 1 commit into
mainfrom
perf/incremental-tree

Conversation

@zhaozhaozz

Copy link
Copy Markdown
Collaborator

A development task on a large private repository failed after five automatic attempts. Every attempt failed on the same request:

GitHub 请求 POST /repos/<owner>/<repo>/git/trees 返回 HTTP 502,写入结果尚未确认:Server Error
... HTTP 504 ... We couldn't respond to your request in time.

This was not a rate limit, which answers 403/429 and is reported as github_rate_limited. When a candidate has no skipped files, Publisher.branch() sent GitHub the complete list of every path in the repository and no base_tree, so GitHub had to rebuild the whole tree. On a large repository that takes longer than GitHub's ten-second request limit, so each retry hit the same timeout. GitHub's status page shows no incident at the time.

Change

  • The tree request now carries only the changed, added and deleted paths (deletions as sha: null) on top of the snapshot's source_tree. Unchanged files are inherited from the base tree, as skipped entries already were. GitHub's answer is still checked against the candidate's tree SHA.
  • A path that turns from a file into a directory, or back, is not expressed as an update; those candidates use the complete listing as before. If GitHub's incremental tree does not match the expected SHA, the complete listing is sent once before github_tree_mismatch.
  • Automatic retry_wait retries back off: 5, 15, 45 and 135 seconds, instead of 5 seconds each. A short GitHub or model outage is outlasted rather than using up all five attempts in about a minute and a half. Rate limits still follow Retry-After.
  • When the retries are used up, the next step now says that a request repeatedly answered with 502/504 while GitHub's status is normal is likely too large or too slow, and should be reported.

Validation

  • npm run check
  • Full Node suite: 271 tests passed. Playwright: e2e/failure-retry.spec.mjs passed.
  • New unit tests:
    • With 5000 unchanged files, the tree request carries only the one changed file, one added file, one deleted file and one mode change, on top of the source tree.
    • A file that becomes a directory uses the complete listing.
    • A mismatched incremental tree falls back once, then fails with github_tree_mismatch.
  • The GitHub fixture now applies base_tree like GitHub (null SHA removes a path). The development flow test asserts the published tree is one entry on top of the source tree, and the existing development tests still publish matching trees through it.
  • The retry test asserts the 5/15/45/135-second delays.

A development task on a large repository failed after five attempts with
POST /git/trees answering 502 or 504 ("We couldn't respond to your request
in time"). Without skipped files the bot listed every path of the
repository and no base tree, so GitHub had to rebuild the whole tree and
ran past its ten-second request limit on every retry. It was not a rate
limit: those answer 403/429.

The tree now carries only changed, added and deleted paths on top of the
snapshot's source tree, and the result is still checked against the
candidate's tree SHA. A path that turns from a file into a directory or
back, or a tree GitHub builds differently, falls back to the complete
listing.

Automatic retries now wait 5, 15, 45 and 135 seconds instead of five
seconds each, and the give-up guidance mentions oversized or slow requests.

@agent-ci-infra-bot agent-ci-infra-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit: 82093c8
Coverage: Snapshot: 199 files, 3383335 bytes; skipped 0 entries; Initial model context: 22 files; other accepted files available through tools; Git workspace contains a fixed snapshot, not full upstream history

Comment thread src/github/publisher.ts
const before=[...upstream.keys(),...skipped.map((f:any)=>f.path)],files=new Set(before),directories=new Set(before.flatMap(parents));
const reshaped=changes.some((f:any)=>directories.has(f.path)||parents(f.path).some(path=>files.has(path)));
const create=(body:any)=>this.github.request('POST',this.github.path('/git/trees'),body);
let tree=candidate.source_tree&&changes.length+deletions.length&&!reshaped?await create({base_tree:candidate.source_tree,tree:[...changes,...deletions]}):null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F1 — medium, suggestion — robustness — a rejected incremental tree request never falls back to the complete listing

  • Where: src/github/publisher.ts:90
  • Problem: the fallback on the next line only runs when GitHub answers with a tree SHA different from expected. If GitHub rejects the POST that carries base_tree instead (4xx such as 422 for an entry it cannot apply on top of the base tree, or a 5xx), create() throws and the complete listing is never tried. Before this change a repository without skipped files always sent a complete listing without base_tree, so there was no base tree for GitHub to refuse.
  • Conditions: a candidate whose delta GitHub refuses to apply to source_tree — a reshape the local reshaped check does not model, or a base tree GitHub no longer resolves. The local check covers file↔directory conversion for every changed/deleted path of the snapshot, so this is hardening rather than a known failing input; I could not produce a concrete rejection on a real repository.
  • Impact: the publish fails (github_http_422 → github_rejected) or repeats the identical request through all five automatic retries, which is the same failure loop this PR is removing, just with a different trigger.
  • Fix: treat a rejected incremental request like a SHA mismatch — try the complete listing once, then fail:
let tree=null;
if(candidate.source_tree&&changes.length+deletions.length&&!reshaped){
  try{tree=await create({base_tree:candidate.source_tree,tree:[...changes,...deletions]});}
  catch(error){if(!(error instanceof TaskError))throw error;tree=null;}
}

An unreferenced tree object left behind is harmless; trees are content-addressed, so the following complete-listing request cannot corrupt the branch.

@agent-ci-infra-bot

Copy link
Copy Markdown

The PR sends GitHub an incremental tree (base_tree plus changed/added/deleted entries, with reshaped candidates and SHA mismatches falling back to the complete listing) and makes automatic retry_wait retries back off 5/15/45/135 s with clearer give-up guidance. The logic, docs and tests look sound: I ran npm run check and the full Node suite (271 pass, including the new incremental/reshape/mismatch and backoff tests), and the head commit's CI checks are green. Relevance: current — the PR base is still main's head (4868e67) and no merged work overlaps. Labels: enhancement (improves the existing publish path and retry handling; this repository has no type/perf axis labels).

Reviewed commit: 82093c8
Coverage: Snapshot: 199 files, 3383335 bytes; skipped 0 entries; Initial model context: 22 files; other accepted files available through tools; Git workspace contains a fixed snapshot, not full upstream history

@agent-ci-infra-bot agent-ci-infra-bot Bot added bot-reviewed enhancement New feature or request labels Oct 28, 2026
@zhaozhaozz
zhaozhaozz added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 48e7a3e Oct 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-reviewed enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant