From 0c7406f2d7ecc74f412d7d59cffdaf65b0311464 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:19:30 +0000 Subject: [PATCH 1/4] Initial plan From 76b61d7ed7c7d8a95754dd0f2e8cf426919c5190 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:06:17 +0000 Subject: [PATCH 2/4] fix: bundle includes refs/heads/ when agent is on target branch during non-main dispatch Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 28 +++++- actions/setup/js/create_pull_request.test.cjs | 87 +++++++++++++++++-- actions/setup/js/generate_git_bundle.cjs | 33 +++++-- actions/setup/js/generate_git_bundle.test.cjs | 83 ++++++++++++++++++ 4 files changed, 218 insertions(+), 13 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index ed854bbebd6..0c4a08df281 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -260,7 +260,33 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran .find(line => /^[0-9a-f]{40}\s+HEAD$/.test(line)); if (headLine) { core.info(`Bundle has no refs/heads entries; fetching HEAD directly into ${bundleTempRef}`); - await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]); + // Use getExecOutput with ignoreReturnCode so we can read actual stderr + // and perform prerequisite recovery before failing — same pattern as the + // initial bundle fetch above. When the agent ran on a non-default branch + // the bundle prerequisite is that branch tip, which isn't reachable from + // the local main-only checkout, so a bare exec() would throw and lose the + // "lacks these prerequisite commits" text needed for recovery. + const headBundleFetch = await execApi.getExecOutput("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`], { ignoreReturnCode: true }); + if (headBundleFetch.exitCode !== 0) { + const headFetchErrorOutput = headBundleFetch.stderr || `exit code ${headBundleFetch.exitCode}`; + const headPrereqCommits = extractBundlePrerequisiteCommits(headFetchErrorOutput); + if (headPrereqCommits.length > 0) { + core.warning(`HEAD bundle fetch failed due to ${headPrereqCommits.length} missing prerequisite commit(s); fetching prerequisites from origin and retrying`); + core.info(`Prerequisite commits: ${summarizeListForLog(headPrereqCommits)}`); + const useBlobFilter = await isShallowOrSparseCheckout(execApi); + const headPrereqFetchArgs = useBlobFilter + ? ["fetch", "--filter=blob:none", "origin", ...headPrereqCommits] + : ["fetch", "origin", ...headPrereqCommits]; + if (useBlobFilter) { + core.info("Using --filter=blob:none for prerequisite fetch (shallow or sparse checkout detected)"); + } + await execApi.exec("git", headPrereqFetchArgs); + core.info("Fetched HEAD bundle prerequisite commits from origin successfully"); + await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]); + } else { + throw new Error(`Failed to apply HEAD-only bundle: ${headFetchErrorOutput}`); + } + } } else { throw new Error(`Failed to resolve bundle branch ref from list-heads: bundle contains no refs/heads entries and no HEAD ref`); } diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index b00493457ca..4fda25db21e 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -496,10 +496,14 @@ index 0000000..abc1234 if (cmd === "git" && args[0] === "rev-list") { return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); } - // Initial bundle fetch fails because the JSONL branch ref is absent from the bundle - if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + // Initial bundle fetch (refs/heads/* refspec) fails because the JSONL branch ref is absent + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode && typeof args[2] === "string" && args[2].startsWith("refs/heads/")) { return Promise.resolve({ exitCode: 1, stderr: "fatal: couldn't find remote ref refs/heads/docs/update-migration-version-2026-05-19-4fe3b9f7f99fc1d6", stdout: "" }); } + // HEAD-based bundle fetch (fallback path) succeeds — no prerequisite errors + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode && typeof args[2] === "string" && args[2].startsWith("HEAD:")) { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } // Bundle contains only HEAD — no refs/heads/* entry (the bug scenario) if (cmd === "git" && args[0] === "bundle" && args[1] === "list-heads" && args[2] === bundlePath) { return Promise.resolve({ @@ -520,14 +524,87 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.getExecOutput).toHaveBeenCalledWith("git", ["bundle", "list-heads", bundlePath]); - // Should have fetched using HEAD: as the refspec - const headFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && typeof args[2] === "string" && args[2].startsWith("HEAD:")); + // HEAD-based bundle fetch is now performed via getExecOutput (ignoreReturnCode: true) + // so the code can distinguish prerequisite errors from other failures. + const headFetchCall = global.exec.getExecOutput.mock.calls.find(([, args, opts]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && typeof args[2] === "string" && args[2].startsWith("HEAD:") && opts && opts.ignoreReturnCode); if (!headFetchCall) { - throw new Error("expected HEAD-based bundle fetch call"); + throw new Error("expected HEAD-based bundle fetch call via getExecOutput"); } expect(headFetchCall[1][2]).toMatch(/^HEAD:refs\/bundles\/create-pr-docs-update-migration-version-2026-05-19-4fe3b9f7f99fc1d6-[a-f0-9]{8}$/); }); + it("should fetch prerequisite commits from origin and retry when HEAD-only bundle has missing prerequisites (non-main dispatch scenario)", async () => { + // Simulates: worker was dispatched from a non-main branch; its bundle has only HEAD + // (no refs/heads/* entry) AND the prerequisite is the feature-branch tip, which is + // not reachable from the local main-only shallow checkout in safe_outputs. + // Fix: the fallback HEAD fetch path must do the same prerequisite recovery as the + // initial fetch path. + const branchName = "docs/update-migration-version-2026-05-19-4fe3b9f7f99fc1d6"; + const patchPath = canonicalPatchPath(branchName); + fs.writeFileSync( + patchPath, + `From abc123 Mon Sep 17 00:00:00 2001 +From: Test Author +Date: Mon, 1 Jan 2024 00:00:00 +0000 +Subject: [PATCH] Test commit + +diff --git a/test.txt b/test.txt +new file mode 100644 +index 0000000..abc1234 +--- /dev/null ++++ b/test.txt +@@ -0,0 +1 @@ ++Hello World +-- +2.34.1 +` + ); + const bundlePath = canonicalBundlePath(branchName); + fs.writeFileSync(bundlePath, "bundle content"); + + const featureBranchTip = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; + + global.exec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "true\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-list") { + return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); + } + // Initial bundle fetch (named ref) fails — bundle only has HEAD + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode && typeof args[2] === "string" && args[2].startsWith("refs/heads/")) { + return Promise.resolve({ exitCode: 1, stderr: `fatal: couldn't find remote ref refs/heads/${branchName}`, stdout: "" }); + } + // HEAD-based bundle fetch fails because prerequisite (feature-branch tip) is missing + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode && typeof args[2] === "string" && args[2].startsWith("HEAD:")) { + return Promise.resolve({ exitCode: 1, stderr: `error: Repository lacks these prerequisite commits:\nerror: ${featureBranchTip}`, stdout: "" }); + } + // Bundle contains only HEAD — no refs/heads/* entry + if (cmd === "git" && args[0] === "bundle" && args[1] === "list-heads" && args[2] === bundlePath) { + return Promise.resolve({ + exitCode: 0, + stdout: `ac85f4047717ec43c931d750575f5251c45dc705 HEAD\n`, + stderr: "", + }); + } + if (cmd === "git" && args && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ base_branch: "main", preserve_branch_name: true }); + const result = await handler({ title: "Test PR", body: "Test body", branch: branchName }, {}); + + expect(result.success).toBe(true); + // Feature-branch tip prerequisite is fetched from origin + expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", "--filter=blob:none", "origin", featureBranchTip]); + // After prerequisite recovery, HEAD bundle is retried via exec + const bundleRetryFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && typeof args[2] === "string" && args[2].startsWith("HEAD:")); + expect(bundleRetryFetchCalls.length).toBe(1); + }); + it("should fetch prerequisite commits and retry bundle fetch when prerequisites are missing", async () => { const patchPath = canonicalPatchPath("feature/test"); fs.writeFileSync( diff --git a/actions/setup/js/generate_git_bundle.cjs b/actions/setup/js/generate_git_bundle.cjs index b42c03b9dbc..643385f0188 100644 --- a/actions/setup/js/generate_git_bundle.cjs +++ b/actions/setup/js/generate_git_bundle.cjs @@ -310,15 +310,34 @@ async function generateGitBundle(branchName, baseBranch, options = {}) { // refs/heads/ — required by create_pull_request.cjs when applying the bundle. let rangeEnd = "HEAD"; if (branchName) { + // Check whether the current branch already IS branchName. + // `git branch -f` refuses to update the currently checked-out branch + // ("cannot force update the branch used by worktree"), which would cause + // rangeEnd to fall back to "HEAD" and produce a bundle with only a HEAD + // ref instead of refs/heads/. This is the common case when + // a worker agent checks out the new branch, commits on it, and then calls + // create_pull_request — HEAD is already pointing to branchName. + let currentBranch = ""; try { - // Use -f (force) to overwrite any stale local branch from previous runs, - // since Strategy 1 verified the named branch does not exist as a proper local ref. - // Use -- so a branch name beginning with "-" is not parsed as another option. - execGitSync(["branch", "-f", "--", branchName, "HEAD"], { cwd }); + currentBranch = execGitSync(["rev-parse", "--abbrev-ref", "HEAD"], { cwd }).trim(); + } catch { + // Unable to determine current branch; fall through to git branch -f attempt. + } + + if (currentBranch === branchName) { rangeEnd = branchName; - debugLog(`Strategy 2: Created local branch '${branchName}' pointing to HEAD for bundle ref`); - } catch (branchErr) { - debugLog(`Strategy 2: Could not create branch '${branchName}': ${getErrorMessage(branchErr)}, using HEAD`); + debugLog(`Strategy 2: HEAD is already on '${branchName}', using as range end directly`); + } else { + try { + // Use -f (force) to overwrite any stale local branch from previous runs, + // since Strategy 1 verified the named branch does not exist as a proper local ref. + // Use -- so a branch name beginning with "-" is not parsed as another option. + execGitSync(["branch", "-f", "--", branchName, "HEAD"], { cwd }); + rangeEnd = branchName; + debugLog(`Strategy 2: Created local branch '${branchName}' pointing to HEAD for bundle ref`); + } catch (branchErr) { + debugLog(`Strategy 2: Could not create branch '${branchName}': ${getErrorMessage(branchErr)}, using HEAD`); + } } } execGitSync(["bundle", "create", bundlePath, `${githubSha}..${rangeEnd}`], { cwd }); diff --git a/actions/setup/js/generate_git_bundle.test.cjs b/actions/setup/js/generate_git_bundle.test.cjs index 3af663a4a38..e61b374ecb1 100644 --- a/actions/setup/js/generate_git_bundle.test.cjs +++ b/actions/setup/js/generate_git_bundle.test.cjs @@ -137,6 +137,89 @@ describe("generateGitBundle (incremental)", () => { expect(generatedBundleHeads).toBe(naiveBundleHeads); }); + it("includes refs/heads/ in bundle when agent is on the target branch (non-main dispatch scenario)", async () => { + // Simulates: scanner dispatches worker from a feature branch (non-main ref). + // The worker checks out the feature branch, creates a new fix branch, commits on + // it, then calls create_pull_request. In this scenario, HEAD is on fix-branch + // when generateGitBundle is called. Strategy 1 (merge-base) fails in a shallow + // clone because the common ancestor of main and the fix branch is beyond the + // shallow boundary. Strategy 2 must then produce a bundle that includes + // refs/heads/fix-branch (not just HEAD) so applyBundleToBranch can locate the ref. + const remoteDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-bundle-nonmain-remote-")); + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-bundle-nonmain-work-")); + tempDirs.push(remoteDir, workDir); + + // Build origin: main and feature-branch diverge from a common ancestor + execGit(["init", "--bare"], { cwd: remoteDir }); + const seedDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-bundle-nonmain-seed-")); + tempDirs.push(seedDir); + execGit(["clone", remoteDir, seedDir]); + execGit(["config", "user.name", "Test User"], { cwd: seedDir }); + execGit(["config", "user.email", "test@example.com"], { cwd: seedDir }); + + // Common ancestor commit A + fs.writeFileSync(path.join(seedDir, "base.txt"), "base\n"); + execGit(["add", "base.txt"], { cwd: seedDir }); + execGit(["commit", "-m", "common ancestor"], { cwd: seedDir }); + execGit(["branch", "-M", "main"], { cwd: seedDir }); + execGit(["push", "-u", "origin", "main"], { cwd: seedDir }); + + // Advance main with extra commits so its tip diverges from feature-branch + fs.writeFileSync(path.join(seedDir, "main-extra.txt"), "main-extra\n"); + execGit(["add", "main-extra.txt"], { cwd: seedDir }); + execGit(["commit", "-m", "main advance"], { cwd: seedDir }); + execGit(["push", "origin", "main"], { cwd: seedDir }); + + // Create feature-branch from common ancestor (before main diverged) + execGit(["checkout", "-b", "feature-branch", "HEAD~1"], { cwd: seedDir }); + fs.writeFileSync(path.join(seedDir, "feature.txt"), "feature\n"); + execGit(["add", "feature.txt"], { cwd: seedDir }); + execGit(["commit", "-m", "feature commit"], { cwd: seedDir }); + const featureBranchTip = execGit(["rev-parse", "HEAD"], { cwd: seedDir }).stdout.trim(); + execGit(["push", "-u", "origin", "feature-branch"], { cwd: seedDir }); + + // Simulate Actions checkout: shallow clone of feature-branch (depth=1) + execGit(["clone", "--depth=1", "--branch=feature-branch", remoteDir, workDir]); + execGit(["config", "user.name", "Test User"], { cwd: workDir }); + execGit(["config", "user.email", "test@example.com"], { cwd: workDir }); + + // Worker agent creates and checks out a new fix branch + execGit(["checkout", "-b", "fix-branch"], { cwd: workDir }); + + // Agent makes new commits on fix-branch + fs.writeFileSync(path.join(workDir, "fix1.txt"), "fix 1\n"); + execGit(["add", "fix1.txt"], { cwd: workDir }); + execGit(["commit", "-m", "fix commit 1"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "fix2.txt"), "fix 2\n"); + execGit(["add", "fix2.txt"], { cwd: workDir }); + execGit(["commit", "-m", "fix commit 2"], { cwd: workDir }); + + const fixBranchHead = execGit(["rev-parse", "HEAD"], { cwd: workDir }).stdout.trim(); + + const savedGithubSha = process.env.GITHUB_SHA; + try { + // GITHUB_SHA is the feature-branch tip at workflow trigger time + process.env.GITHUB_SHA = featureBranchTip; + + const { generateGitBundle } = require("./generate_git_bundle.cjs"); + const result = await generateGitBundle("fix-branch", "main", { mode: "full", cwd: workDir }); + expect(result.success).toBe(true); + expect(result.bundlePath).toBeTruthy(); + bundlePaths.push(result.bundlePath); + + // The bundle MUST contain refs/heads/fix-branch so applyBundleToBranch can locate the ref + const bundleHeads = execGit(["bundle", "list-heads", result.bundlePath], { cwd: workDir }).stdout.trim(); + expect(bundleHeads).toContain(fixBranchHead); + expect(bundleHeads).toContain("refs/heads/fix-branch"); + } finally { + if (savedGithubSha === undefined) { + delete process.env.GITHUB_SHA; + } else { + process.env.GITHUB_SHA = savedGithubSha; + } + } + }); + it("returns actionable guidance when branch is missing in incremental mode", async () => { const { generateGitBundle } = require("./generate_git_bundle.cjs"); From beebe9dbfd022c0c0065d98b5e533c0876b49fd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:37:48 +0000 Subject: [PATCH 3/4] fix(bundle): harden shallow clone test and HEAD retry error context Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 12 ++-- actions/setup/js/create_pull_request.test.cjs | 72 ++++++++++++++++++- actions/setup/js/generate_git_bundle.test.cjs | 2 +- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 0c4a08df281..93fcbb10abc 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -274,15 +274,19 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran core.warning(`HEAD bundle fetch failed due to ${headPrereqCommits.length} missing prerequisite commit(s); fetching prerequisites from origin and retrying`); core.info(`Prerequisite commits: ${summarizeListForLog(headPrereqCommits)}`); const useBlobFilter = await isShallowOrSparseCheckout(execApi); - const headPrereqFetchArgs = useBlobFilter - ? ["fetch", "--filter=blob:none", "origin", ...headPrereqCommits] - : ["fetch", "origin", ...headPrereqCommits]; + const headPrereqFetchArgs = useBlobFilter ? ["fetch", "--filter=blob:none", "origin", ...headPrereqCommits] : ["fetch", "origin", ...headPrereqCommits]; if (useBlobFilter) { core.info("Using --filter=blob:none for prerequisite fetch (shallow or sparse checkout detected)"); } await execApi.exec("git", headPrereqFetchArgs); core.info("Fetched HEAD bundle prerequisite commits from origin successfully"); - await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]); + try { + core.info(`Retrying HEAD bundle fetch into ${bundleTempRef} after prerequisite recovery`); + await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]); + core.info("HEAD bundle fetch retry succeeded after prerequisite recovery"); + } catch (retryError) { + throw new Error(`HEAD bundle fetch failed after fetching ${headPrereqCommits.length} prerequisite commit(s): ${retryError instanceof Error ? retryError.message : String(retryError)}`, { cause: retryError }); + } } else { throw new Error(`Failed to apply HEAD-only bundle: ${headFetchErrorOutput}`); } diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 4fda25db21e..c9ee905a6ff 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -526,7 +526,9 @@ index 0000000..abc1234 expect(global.exec.getExecOutput).toHaveBeenCalledWith("git", ["bundle", "list-heads", bundlePath]); // HEAD-based bundle fetch is now performed via getExecOutput (ignoreReturnCode: true) // so the code can distinguish prerequisite errors from other failures. - const headFetchCall = global.exec.getExecOutput.mock.calls.find(([, args, opts]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && typeof args[2] === "string" && args[2].startsWith("HEAD:") && opts && opts.ignoreReturnCode); + const headFetchCall = global.exec.getExecOutput.mock.calls.find( + ([, args, opts]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && typeof args[2] === "string" && args[2].startsWith("HEAD:") && opts && opts.ignoreReturnCode + ); if (!headFetchCall) { throw new Error("expected HEAD-based bundle fetch call via getExecOutput"); } @@ -605,6 +607,74 @@ index 0000000..abc1234 expect(bundleRetryFetchCalls.length).toBe(1); }); + it("should include retry context when HEAD-only bundle fetch still fails after prerequisite recovery", async () => { + const branchName = "docs/update-migration-version-2026-05-19-4fe3b9f7f99fc1d6"; + const patchPath = canonicalPatchPath(branchName); + fs.writeFileSync( + patchPath, + `From abc123 Mon Sep 17 00:00:00 2001 +From: Test Author +Date: Mon, 1 Jan 2024 00:00:00 +0000 +Subject: [PATCH] Test commit + +diff --git a/test.txt b/test.txt +new file mode 100644 +index 0000000..abc1234 +--- /dev/null ++++ b/test.txt +@@ -0,0 +1 @@ ++Hello World +-- +2.34.1 +` + ); + const bundlePath = canonicalBundlePath(branchName); + fs.writeFileSync(bundlePath, "bundle content"); + + const featureBranchTip = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; + + global.exec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "true\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-list") { + return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode && typeof args[2] === "string" && args[2].startsWith("refs/heads/")) { + return Promise.resolve({ exitCode: 1, stderr: `fatal: couldn't find remote ref refs/heads/${branchName}`, stdout: "" }); + } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode && typeof args[2] === "string" && args[2].startsWith("HEAD:")) { + return Promise.resolve({ exitCode: 1, stderr: `error: Repository lacks these prerequisite commits:\nerror: ${featureBranchTip}`, stdout: "" }); + } + if (cmd === "git" && args[0] === "bundle" && args[1] === "list-heads" && args[2] === bundlePath) { + return Promise.resolve({ + exitCode: 0, + stdout: `ac85f4047717ec43c931d750575f5251c45dc705 HEAD\n`, + stderr: "", + }); + } + if (cmd === "git" && args && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }); + + global.exec.exec.mockImplementation((cmd, args) => { + if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && typeof args[2] === "string" && args[2].startsWith("HEAD:")) { + throw new Error("fatal: failed to read HEAD-only bundle"); + } + return Promise.resolve(0); + }); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ base_branch: "main", preserve_branch_name: true }); + const result = await handler({ title: "Test PR", body: "Test body", branch: branchName }, {}); + + expect(result.success).toBe(false); + expect(result.error).toBe("Failed to apply bundle"); + expect(global.core.error).toHaveBeenCalledWith(expect.stringContaining("HEAD bundle fetch failed after fetching 1 prerequisite commit(s): fatal: failed to read HEAD-only bundle")); + }); + it("should fetch prerequisite commits and retry bundle fetch when prerequisites are missing", async () => { const patchPath = canonicalPatchPath("feature/test"); fs.writeFileSync( diff --git a/actions/setup/js/generate_git_bundle.test.cjs b/actions/setup/js/generate_git_bundle.test.cjs index e61b374ecb1..f36f139c446 100644 --- a/actions/setup/js/generate_git_bundle.test.cjs +++ b/actions/setup/js/generate_git_bundle.test.cjs @@ -179,7 +179,7 @@ describe("generateGitBundle (incremental)", () => { execGit(["push", "-u", "origin", "feature-branch"], { cwd: seedDir }); // Simulate Actions checkout: shallow clone of feature-branch (depth=1) - execGit(["clone", "--depth=1", "--branch=feature-branch", remoteDir, workDir]); + execGit(["clone", "--depth=1", "--no-local", "--branch=feature-branch", remoteDir, workDir]); execGit(["config", "user.name", "Test User"], { cwd: workDir }); execGit(["config", "user.email", "test@example.com"], { cwd: workDir }); From 0bdb1d3c74efd3d432085de5cbfc9e4723524495 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:38:56 +0000 Subject: [PATCH 4/4] refactor(bundle): clarify HEAD retry error construction Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 93fcbb10abc..ee994a0e888 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -285,7 +285,10 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]); core.info("HEAD bundle fetch retry succeeded after prerequisite recovery"); } catch (retryError) { - throw new Error(`HEAD bundle fetch failed after fetching ${headPrereqCommits.length} prerequisite commit(s): ${retryError instanceof Error ? retryError.message : String(retryError)}`, { cause: retryError }); + const retryErrorMessage = retryError instanceof Error ? retryError.message : String(retryError); + throw new Error(`HEAD bundle fetch failed after fetching ${headPrereqCommits.length} prerequisite commit(s): ${retryErrorMessage}`, { + cause: retryError, + }); } } else { throw new Error(`Failed to apply HEAD-only bundle: ${headFetchErrorOutput}`);