Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions actions/setup/js/create_pull_request.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<bundleBranchRef> 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/<agent-branch>).
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`);
Expand Down Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions actions/setup/js/create_pull_request.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <test@example.com>
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", () => {
Expand Down
Loading