action: extend shallow-unreachable deepen-retry to checkout_pr fetches (#734)
extracts the deepen-retry helper from `GitFetchTool` into shared `$gitFetchWithDeepen` and applies it to every fetch in `checkoutPrBranch` (baseRef, pull/N/head, before_sha temp branch). on shallow clones with deep PR ancestry — the failure mode behind ~10 of 51 `heuristic:very-slow` runs in 24h on `remotion-dev/remotion` — the baseRef fetch was throwing `Could not read <sha>` to the agent before the compare-api deepen block could run. agents then burned 10+ minutes retrying `checkout_pr` and falling back to ad-hoc shell `git fetch --deepen` workarounds. also splits the analyzer's `heuristic:git-error-recovered` into `heuristic:git-shallow-unreachable` and `heuristic:git-shallow-lock` buckets so future audits surface this without manual log-grep. closes #656.
This commit is contained in:
committed by
pullfrog[bot]
parent
2960d51493
commit
4ad649ebb9
+25
-10
@@ -5,7 +5,7 @@ import type { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
|
||||
import { type } from "arktype";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { countLines, createDiffCoverageState } from "../utils/diffCoverage.ts";
|
||||
import { $git } from "../utils/gitAuth.ts";
|
||||
import { $git, $gitFetchWithDeepen } from "../utils/gitAuth.ts";
|
||||
import { executeLifecycleHook } from "../utils/lifecycle.ts";
|
||||
import { computeIncrementalDiff } from "../utils/rangeDiff.ts";
|
||||
import { retry } from "../utils/retry.ts";
|
||||
@@ -259,10 +259,10 @@ async function ensureBeforeShaReachable(params: EnsureBeforeShaParams): Promise<
|
||||
sha: params.sha,
|
||||
ref: tempBranch,
|
||||
});
|
||||
await $git(
|
||||
"fetch",
|
||||
await $gitFetchWithDeepen(
|
||||
["--no-tags", ...(params.isShallow ? ["--depth=1"] : []), "origin", tempBranch],
|
||||
{ token: params.gitToken }
|
||||
{ token: params.gitToken },
|
||||
`before_sha temp branch ${tempBranch}`
|
||||
);
|
||||
log.debug(`» fetched before_sha via temp branch ${tempBranch}`);
|
||||
return true;
|
||||
@@ -410,9 +410,17 @@ export async function checkoutPrBranch(
|
||||
toolState.checkoutSha = $("git", ["rev-parse", "HEAD"], { log: false }).trim();
|
||||
const alreadyOnBranch = toolState.checkoutSha === pr.headSha;
|
||||
|
||||
// fetch base branch so origin/<base> exists for diff operations
|
||||
// fetch base branch so origin/<base> exists for diff operations.
|
||||
// wrap with deepen-retry: on shallow clones (the actions/checkout default
|
||||
// is depth=1), repos with deep PR ancestry can't reach the baseRef tip in
|
||||
// a single round trip, surfacing as `Could not read <sha>` / `remote did
|
||||
// not send all necessary objects` (issue #656).
|
||||
log.debug(`» fetching base branch (${pr.baseRef})...`);
|
||||
await $git("fetch", ["--no-tags", "origin", pr.baseRef], { token: gitToken });
|
||||
await $gitFetchWithDeepen(
|
||||
["--no-tags", "origin", pr.baseRef],
|
||||
{ token: gitToken },
|
||||
`base branch ${pr.baseRef}`
|
||||
);
|
||||
|
||||
// alreadyOnBranch only matches for repeated checkout_pr calls for the same PR in one session
|
||||
// (without the tip moving), or if an external setup already checked out the PR head.
|
||||
@@ -426,14 +434,21 @@ export async function checkoutPrBranch(
|
||||
// -B creates or resets the branch to match origin/baseBranch
|
||||
$("git", ["checkout", "-B", pr.baseRef, `origin/${pr.baseRef}`], { log: false });
|
||||
|
||||
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs)
|
||||
// fetch PR branch using pull/{n}/head refspec (works for both fork and same-repo PRs).
|
||||
// two transient classes wrap this fetch:
|
||||
// - shallow-unreachable (`Could not read <sha>` etc.) — handled by the
|
||||
// inner `$gitFetchWithDeepen` deepen-retry (one shot, see issue #656)
|
||||
// - pull/N/head webhook race (`couldn't find remote ref pull/N/head`) —
|
||||
// handled by the outer retry below (see issue #591)
|
||||
log.debug(`» fetching PR #${pr.number} (${localBranch})...`);
|
||||
await retry(
|
||||
async () => {
|
||||
try {
|
||||
await $git("fetch", ["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`], {
|
||||
token: gitToken,
|
||||
});
|
||||
await $gitFetchWithDeepen(
|
||||
["--no-tags", "origin", `+pull/${pr.number}/head:${localBranch}`],
|
||||
{ token: gitToken },
|
||||
`PR #${pr.number}`
|
||||
);
|
||||
} catch (e) {
|
||||
// on the webhook race, check whether the PR still matches what we
|
||||
// dispatched on. if it's been closed/merged or the head SHA moved,
|
||||
|
||||
+2
-32
@@ -2,7 +2,7 @@ import { regex } from "arkregex";
|
||||
import { type } from "arktype";
|
||||
import type { StoredPushDest } from "../toolState.ts";
|
||||
import { log } from "../utils/cli.ts";
|
||||
import { $git } from "../utils/gitAuth.ts";
|
||||
import { $git, $gitFetchWithDeepen } from "../utils/gitAuth.ts";
|
||||
import { executeLifecycleHook } from "../utils/lifecycle.ts";
|
||||
import { $ } from "../utils/shell.ts";
|
||||
import type { ToolContext } from "./server.ts";
|
||||
@@ -510,21 +510,6 @@ const GitFetch = type({
|
||||
depth: type.number.describe("Fetch depth (for shallow clones)").optional(),
|
||||
});
|
||||
|
||||
// when an agent-supplied depth is too shallow to reach the merge base, git
|
||||
// surfaces "Could not read <sha>" and "remote did not send all necessary
|
||||
// objects". detect both wordings so a single deepen retry can recover before
|
||||
// the error reaches the agent (issue #564). git emits the full OID via
|
||||
// oid_to_hex, so the bound is 40 (SHA-1) or 64 (SHA-256).
|
||||
const SHALLOW_UNREACHABLE_PATTERNS: RegExp[] = [
|
||||
/Could not read [a-f0-9]{40,64}/,
|
||||
/remote did not send all necessary objects/,
|
||||
];
|
||||
|
||||
// large enough to clear the merge base on most real-world PRs without
|
||||
// downloading the full history; matches the fallback used by checkoutPrBranch
|
||||
// when the compare API is unavailable.
|
||||
const DEEPEN_RETRY_DEPTH = 1000;
|
||||
|
||||
export function GitFetchTool(ctx: ToolContext) {
|
||||
return tool({
|
||||
name: "git_fetch",
|
||||
@@ -538,22 +523,7 @@ export function GitFetchTool(ctx: ToolContext) {
|
||||
if (params.depth !== undefined) {
|
||||
fetchArgs.push(`--depth=${params.depth}`);
|
||||
}
|
||||
try {
|
||||
await $git("fetch", fetchArgs, { token: ctx.gitToken });
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const isShallowUnreachable = SHALLOW_UNREACHABLE_PATTERNS.some((p) => p.test(msg));
|
||||
const isShallow =
|
||||
isShallowUnreachable &&
|
||||
$("git", ["rev-parse", "--is-shallow-repository"], { log: false }).trim() === "true";
|
||||
if (!isShallow) throw err;
|
||||
log.info(
|
||||
`» git_fetch hit shallow-unreachable error, retrying with --deepen=${DEEPEN_RETRY_DEPTH}`
|
||||
);
|
||||
await $git("fetch", [`--deepen=${DEEPEN_RETRY_DEPTH}`, "--no-tags", "origin", params.ref], {
|
||||
token: ctx.gitToken,
|
||||
});
|
||||
}
|
||||
await $gitFetchWithDeepen(fetchArgs, { token: ctx.gitToken }, "git_fetch");
|
||||
return { success: true, ref: params.ref };
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user