diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index a5004902b69..d8acf98de4a 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1265,13 +1265,33 @@ async function main(config = {}) { // This preserves merge commit topology and per-commit metadata (messages, authorship) // unlike git format-patch which flattens history and drops merge resolution content. core.info(`Applying changes from bundle: ${bundleFilePath}`); - const bundleBranchRef = originalAgentBranch || branchName; + let bundleBranchRef = `refs/heads/${originalAgentBranch || branchName}`; try { await ensureFullHistoryForBundle(exec); // Fetch from bundle: creates a local branch pointing to the bundle's tip commit. - // The bundle contains refs/heads/ which was the agent's working branch. - await exec.exec("git", ["fetch", bundleFilePath, `refs/heads/${bundleBranchRef}:refs/heads/${branchName}`]); + // bundleBranchRef is the source ref inside the bundle (typically refs/heads/). + try { + await exec.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:refs/heads/${branchName}`]); + } catch (initialFetchError) { + // Fallback: resolve the source ref directly from the bundle contents. + // Some agents may emit a JSONL branch name that differs from the ref embedded in the bundle. + const initialFetchErrorMessage = initialFetchError instanceof Error ? initialFetchError.message : String(initialFetchError); + core.warning(`Bundle fetch with ${bundleBranchRef} failed: ${initialFetchErrorMessage}; resolving branch ref from bundle heads`); + const { stdout: bundleHeadsOutput } = await exec.getExecOutput("git", ["bundle", "list-heads", bundleFilePath]); + const branchRefs = bundleHeadsOutput + .split("\n") + .map(line => line.trim().split(/\s+/)[1] || "") + .filter(ref => /^refs\/heads\/[A-Za-z0-9._][A-Za-z0-9._/-]*$/.test(ref)); + + if (branchRefs.length === 1) { + bundleBranchRef = branchRefs[0]; + core.info(`Resolved bundle source ref from list-heads: ${bundleBranchRef}`); + await exec.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:refs/heads/${branchName}`]); + } else { + throw new Error(`Failed to resolve bundle branch ref from list-heads: expected exactly 1 refs/heads entry, found ${branchRefs.length}`, { cause: initialFetchError }); + } + } core.info(`Created local branch ${branchName} from bundle`); await exec.exec("git", ["checkout", branchName]); core.info(`Checked out branch ${branchName} from bundle`); @@ -1334,7 +1354,7 @@ To create a pull request with the changes: gh run download ${runId} -n agent -D /tmp/agent-${runId} # Fetch the bundle into a local branch -git fetch /tmp/agent-${runId}/${artifactFileName} refs/heads/${bundleBranchRef}:refs/heads/${branchName} +git fetch /tmp/agent-${runId}/${artifactFileName} ${bundleBranchRef}:refs/heads/${branchName} git checkout ${branchName} # Push the branch to origin diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 5d4db78f067..ca8b449d81b 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -257,6 +257,71 @@ index 0000000..abc1234 expect(unshallowCallIndex).toBeGreaterThanOrEqual(0); expect(bundleFetchCallIndex).toBeGreaterThan(unshallowCallIndex); }); + + it("should resolve bundle source ref from list-heads when JSONL branch ref is missing in bundle", async () => { + const patchPath = path.join(tempDir, "test.patch"); + 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 = path.join(tempDir, "test.bundle"); + fs.writeFileSync(bundlePath, "bundle content"); + + global.exec.exec.mockImplementation((cmd, args) => { + if ( + cmd === "git" && + Array.isArray(args) && + args[0] === "fetch" && + args[1] === bundlePath && + args[2] === "refs/heads/ops-review-may09-2026:refs/heads/ops-review-may09-2026" + ) { + throw new Error("fatal: couldn't find remote ref refs/heads/ops-review-may09-2026"); + } + return Promise.resolve(0); + }); + + global.exec.getExecOutput.mockImplementation((cmd, args) => { + 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] === "bundle" && args[1] === "list-heads" && args[2] === bundlePath) { + return Promise.resolve({ + exitCode: 0, + stdout: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 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: "ops-review-may09-2026", patch_path: patchPath, bundle_path: bundlePath }, {}); + + expect(result.success).toBe(true); + expect(global.exec.getExecOutput).toHaveBeenCalledWith("git", ["bundle", "list-heads", bundlePath]); + expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/main:refs/heads/ops-review-may09-2026"]); + }); }); describe("create_pull_request - fallback-as-issue configuration", () => {