Skip to content

Retry transient git-fetch failures during evals-state branch checkout - #50058

Merged
pelikhan merged 8 commits into
mainfrom
copilot/deep-report-fix-push-evals-state
Aug 3, 2026
Merged

Retry transient git-fetch failures during evals-state branch checkout#50058
pelikhan merged 8 commits into
mainfrom
copilot/deep-report-fix-push-evals-state

Conversation

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

The push_evals_state job's "Push evals results to git" step failed independently across three unrelated workflows within a ~10h window. Logs showed the identical signature in all three: an HTTP 502 / git-fetch timeout during branch checkout in the shared push_experiment_state.cjs script.

Root cause

  • checkoutOrCreateBranch() (initial git fetch + checkout of the target evals/<workflow> branch) had no retry logic, unlike the later pushSignedCommits() push, which already retries 3x with exponential backoff.
  • A single transient network blip (502, fetch timeout) during checkout was therefore immediately fatal, explaining why the same job/step failed across otherwise unrelated workflows sharing this component.

Fix

  • Added checkoutOrCreateBranchWithRetry(), applying the same retry-with-backoff pattern (3 retries, exponential backoff starting at 1s) already used for the push step, around the checkout call in main().
  • Genuine "missing ref" handling (orphan branch creation) is untouched — only transient failures during the fetch/checkout are retried.
// before
baseRef = checkoutOrCreateBranch(branchName, repoUrl, workspaceDir); // no retry

// after
baseRef = await checkoutOrCreateBranchWithRetry(branchName, repoUrl, workspaceDir); // retries transient fetch/checkout failures

Tests

Added unit tests for checkoutOrCreateBranchWithRetry covering immediate success, retry-then-succeed on transient errors, and exhausting all retries.


Branch refresh requested by PR Sous Chef.
Run: https://github.com/github/gh-aw/actions/runs/30851084299

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 4.22 AIC · ⌖ 6.83 AIC · ⊞ 8.3K ·
Comment /souschef to run again


run: https://github.com/github/gh-aw/actions/runs/30855215929

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 12.2 AIC · ⌖ 8.62 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix shared push_evals_state git-push failure across workflows Retry transient git-fetch failures during evals-state branch checkout Aug 3, 2026
Copilot AI requested a review from pelikhan August 3, 2026 18:31
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR Triage: #50058

  • Category: bug
  • Risk: low
  • Score: 46/100 (Impact 22 + Urgency 12 + Quality 12)
  • Recommended action: defer
  • Batch: pr-batch:draft-small

Draft PR, no CI runs yet. Addresses an intermittent git-fetch flake affecting the evals-state pipeline.

Generated by 🔧 PR Triage Agent · auto · 51.1 AIC · ⌖ 4.08 AIC · ⊞ 8K ·

@pelikhan
pelikhan marked this pull request as ready for review August 3, 2026 19:02
Copilot AI review requested due to automatic review settings August 3, 2026 19:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds retry protection for transient failures when checking out eval-state branches.

Changes:

  • Adds exponential-backoff checkout retries.
  • Adds unit tests for success, recovery, and exhaustion.
  • Exports the retry helper for testing.
Show a summary per file
File Description
actions/setup/js/push_experiment_state.cjs Implements and invokes checkout retries.
actions/setup/js/push_experiment_state.test.cjs Tests retry behavior.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +335 to +337
} catch (err) {
lastError = err;
if (attempt < maxRetries) {
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #50058 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100). The PR modifies only 2 files with no changes in src/, lib/, pkg/, internal/, app/, core/, domain/, services/, or api/ directories.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on two issues.

📋 Key Themes & Highlights

Key Themes

  • Retry scope too broad: checkoutOrCreateBranchWithRetry catches and retries every error from checkoutOrCreateBranch, including non-transient failures (auth errors, invalid repo, permission denied). The PR description says only transient fetch failures are retried, but the implementation does not enforce that boundary.
  • Test coverage gap: Tests cover success, transient-retry-then-succeed, and exhausted-retries paths, but no test documents/pins the behaviour for non-transient errors.

Positive Highlights

  • ✅ Clean injection of checkoutFn via options — makes unit testing straightforward without mocking globals
  • baseDelayMs: 0 in tests eliminates real waiting — good test hygiene
  • ✅ Exponential backoff matches the existing push-step pattern, keeping the codebase consistent
  • ✅ Detailed JSDoc explaining what is and is not retried

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 24 AIC · ⌖ 10.8 AIC · ⊞ 7.1K
Comment /matt to run again

expect(mockCore.warning).toHaveBeenCalledTimes(2);
});
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] Missing test: non-transient errors (auth, invalid repo) are retried just like transient network errors — a test should document this behaviour or the implementation should distinguish error types.

💡 Suggested test
it("retries permission errors even though they are non-transient (documents current behaviour)", async () => {
  const checkoutFn = vi.fn().mockImplementation(() => {
    throw new Error("ERROR: Repository not found.");
  });
  await expect(
    checkoutOrCreateBranchWithRetry("evals/myworkflow", "...url...", "/tmp/workdir", {
      checkoutFn, baseDelayMs: 0, maxRetries: 2,
    })
  ).rejects.toThrow();
  expect(checkoutFn).toHaveBeenCalledTimes(3); // retried 3x despite being non-transient
});

If the intent is to retry everything, this test locks in that design choice. If only transient errors should be retried, an error classifier is needed.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The retry wrapper is a good fix and the test coverage is solid. The existing inline comment correctly identifies the one gap: checkoutOrCreateBranchWithRetry retries all non-missing-ref errors, not just transient network errors — auth failures, bad token, or local workspace errors will also be retried 3× before failing. Consider adding a retryable error predicate (matching patterns like 502, timeout, RPC failed) to avoid masking fast-fail conditions with spurious retries and delays.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 21.3 AIC · ⌖ 9.28 AIC · ⊞ 5.4K

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

REQUEST_CHANGES: retry wrapper can corrupt checkout state on partial-mutation failures

The new checkoutOrCreateBranchWithRetry fixes the reported transient-fetch-failure bug, but introduces a real correctness risk: it retries the entire checkoutOrCreateBranch function, including its orphan-branch creation path, which performs non-idempotent local mutations (checkout --orphan, read-tree --empty, workspace file deletion). A failure partway through that path (e.g. a filesystem error during rmSync) leaves the workspace in a state where blindly re-invoking checkoutOrCreateBranch from scratch can throw a different, confusing error or silently proceed against an inconsistent checkout.

Themes and additional notes
  • There is also a pre-existing open review comment (from Copilot, unaddressed) flagging that the retry wrapper retries all errors (auth failures, invalid refs, etc.), not just transient network failures — this compounds the above issue since non-transient errors could also trigger retries against a half-mutated workspace.
  • Test coverage for the new retry wrapper is solid for the "all transient" and "all persistent" cases, but doesn't cover the orphan-branch/partial-mutation retry scenario described above.
  • The push-step retry pattern this mirrors doesn't have this same idempotency risk since a push failure doesn't mutate local git state destructively before failing.

🔎 Code quality review by PR Code Quality Reviewer · auto · 42.8 AIC · ⌖ 3.79 AIC · ⊞ 7.9K
Comment /review to run again

Comment on lines +316 to +334
* Wraps checkoutOrCreateBranch with retry-with-backoff so transient network
* failures during the initial `git fetch` (e.g. HTTP 502s or timeouts against
* the git remote) don't immediately fail the job. Genuine "missing ref"
* conditions are handled inside checkoutOrCreateBranch itself and are not
* retried here since they are resolved deterministically (orphan branch
* creation), not by retrying.
*
* @param {string} branchName
* @param {string} repoUrl
* @param {string} workspaceDir
* @param {{maxRetries?: number, baseDelayMs?: number, checkoutFn?: typeof checkoutOrCreateBranch}} [options]
* @returns {Promise<string>}
*/
async function checkoutOrCreateBranchWithRetry(branchName, repoUrl, workspaceDir, options = {}) {
const { maxRetries = 3, baseDelayMs = 1000, checkoutFn = checkoutOrCreateBranch } = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return checkoutFn(branchName, repoUrl, workspaceDir);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Retrying after a partial orphan-branch mutation can leave the workspace/repo in a state where the retry itself fails differently or corrupts the checkout.

💡 Explanation and suggested fix

checkoutOrCreateBranch is not idempotent: on the orphan-branch path it runs checkout --orphan, read-tree --empty, and deletes workspace files as side effects before returning. If any of these steps throws (e.g. disk error, git lock contention) after the branch has already been switched to orphan and/or files removed, the wrapper will blindly retry the whole function from scratch. On retry, checkoutOrCreateBranch re-enters the fetch path but the workspace is already in the orphan-branch/partially-cleared state from the prior attempt, so the retry can hit a different error (e.g. "already on branch X", stale index state) instead of a clean transient-network retry, or silently proceed with a half-cleaned working tree.

Suggested fix: make checkoutOrCreateBranch resilient to being re-entered mid-mutation (e.g. git reset --hard / re-checkout at the start of each attempt), or restrict the retry wrapper to only retry errors thrown by the initial fetch step (before any local mutation occurs), rather than retrying the whole function.

// current: retries whole function including orphan-branch mutation
return checkoutFn(branchName, repoUrl, workspaceDir);

// safer: only retry the network fetch, keep local mutation single-attempt

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 3 test(s): 3 design, 0 implementation, 0 violation(s).

📊 Metrics (3 tests)
Metric Value
Analyzed 3 (Go: 0, JS: 3)
✅ Design 3 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 3 (100%)
Duplicate clusters 0
Inflation No (1.2:1)
🚨 Violations 0

Test Classification

Test File Classification Issues
returns immediately on success without warnings push_experiment_state.test.cjs:237 design_test, high_value None
retries transient failures (e.g. fetch timeouts/502s) and eventually succeeds push_experiment_state.test.cjs:250 design_test, high_value None
throws the last error after exhausting all retries push_experiment_state.test.cjs:273 design_test, high_value None

Details

All three tests verify genuine behavioral contracts of the retry-with-exponential-backoff mechanism:

  • Success path: Immediate return without overhead (no retry overhead on success)
  • Transient failure handling: Correct retry and eventual success behavior
  • Exhaustion enforcement: Proper error propagation after retry limit

Mocking strategy is sound (dependency injection via checkoutFn parameter; external calls via core.warning).

Verdict

passed. 0% implementation tests (threshold: 30%). No violations.

🧪 Test quality analysis by Test Quality Sentinel · haiku45 · 16.3 AIC · ⌖ 6.62 AIC · ⊞ 8.5K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%). All tests verify genuine behavioral contracts of the retry-with-exponential-backoff mechanism.

@github-actions github-actions Bot mentioned this pull request Aug 3, 2026
@pelikhan

pelikhan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan

pelikhan commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@copilot apply fix to push_evals_state - refactor helper.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran the pr-finisher flow.

  • Reviews — addressed the outstanding review thread: checkout retries are now gated by isTransientCheckoutError (reuses isTransientError from error_recovery.cjs plus git-specific transport patterns). Auth/permission errors, invalid refs, local checkout/rev-parse errors, and orphan-branch filesystem errors now fail fast, and a new test asserts a deterministic error invokes checkoutFn exactly once.
  • Checks (local) — make agent-report-progress (fmt, lint, recompile, unit tests) passes; lock files in sync.
  • Checks (CI) — unverified on the new HEAD; agent pushes don't trigger workflows.
  • Mergeable — no conflicts.

Hand-off: CI needs to be re-triggered by a maintainer before merge.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot apply fix to push_evals_state - refactor helper.

Done in 2159ac7. The retry logic is now a shared helper in actions/setup/js/git_helpers.cjs:

  • isTransientGitError(error) — wraps isTransientError from error_recovery.cjs and adds git transport signatures (HTTP 5xx, RPC failed, unable to access, the remote end hung up, timeouts, TLS handshake failures).
  • withGitRetry(operation, { maxRetries, baseDelayMs, operationName }) — exponential backoff, rethrows non-transient errors immediately.

push_experiment_state.cjs (used by push_evals_state) now just delegates: checkoutOrCreateBranchWithRetry calls withGitRetry around checkoutOrCreateBranch. Tests for the shared helper live in git_helpers.test.cjs (success, transient retry, deterministic fail-fast, exhaustion). make agent-report-progress passes.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot This PR still needs another pass before maintainers can investigate quickly.

  • Open blocking reviews remain from: github-actions
  • Please address the remaining review feedback, refresh the branch if needed, then run the pr-finisher skill and post a concise merge-readiness summary for the current head.

Generated by PR Sous Chef · run: https://github.com/github/gh-aw/actions/runs/30851084299

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 4.22 AIC · ⌖ 6.83 AIC · ⊞ 8.3K ·
Comment /souschef to run again

…ch path

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot This PR still needs another pass before maintainers can investigate quickly.

  • Open blocking reviews remain from: github-actions...

Addressed the remaining blocking github-actions reviews in ff553f1.

The two open concerns were (1) retrying non-transient errors and (2) replaying the non-idempotent orphan-branch path. Both are now structurally impossible:

  • Retry is scoped to the network git fetch only. The local git checkout / rev-parse and the orphan-branch path (checkout --orphan, read-tree --empty, working-tree cleanup) run exactly once, so a partial local mutation is never replayed.
  • Retryability is classified by isTransientGitError in actions/setup/js/git_helpers.cjs (delegates to isTransientError plus git transport signatures: HTTP 5xx, RPC failed, unable to access, the remote end hung up, timeouts, TLS handshake failures). Auth/permission errors, invalid refs, and filesystem errors rethrow on the first attempt.
  • Retry/backoff itself now lives in the shared withGitRetry helper rather than being inlined.
  • Tests cover: transient fetch retry then successful checkout, deterministic fetch failure invoking the fetch exactly once, and the missing-ref orphan path not being retried; plus helper-level tests in git_helpers.test.cjs.

Merge readiness for head ff553f1:

  • Reviews — blocking feedback addressed above.
  • Checks (local) — make agent-report-progress (fmt, lint, recompile, unit tests) passes; lock files in sync.
  • Checks (CI) — unknown on this head; agent pushes do not trigger workflows.
  • Mergeable — branch merged with main, no conflicts.

Hand-off: CI must be re-triggered by a maintainer before merge.

Copilot AI requested a review from gh-aw-bot August 3, 2026 20:57
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot This PR still needs another pass before maintainers can investigate quickly.

Generated by PR Sous Chef · run: https://github.com/github/gh-aw/actions/runs/30855215929

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 12.2 AIC · ⌖ 8.62 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
@pelikhan
pelikhan merged commit 75e370f into main Aug 3, 2026
37 checks passed
@pelikhan
pelikhan deleted the copilot/deep-report-fix-push-evals-state branch August 3, 2026 22:22
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.84.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[deep-report] Root-cause shared push_evals_state git-push failure across 3 workflows

4 participants