From bd203156c54ed7c1c59101f203d98ae0457d7e7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 06:20:40 +0000 Subject: [PATCH 01/10] Initial plan From fbcb7aa69651bebbab16d8b71be40e445d7a312f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 06:27:36 +0000 Subject: [PATCH 02/10] Fix safe output bundle fetch for checked-out branches Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 19 +++++++-- actions/setup/js/create_pull_request.test.cjs | 41 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index d8acf98de4a..732f28617a7 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1266,13 +1266,17 @@ async function main(config = {}) { // unlike git format-patch which flattens history and drops merge resolution content. core.info(`Applying changes from bundle: ${bundleFilePath}`); let bundleBranchRef = `refs/heads/${originalAgentBranch || branchName}`; + const bundleTargetRef = `refs/heads/${branchName}`; + const bundleTempRef = `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}`; try { await ensureFullHistoryForBundle(exec); - // Fetch from bundle: creates a local branch pointing to the bundle's tip commit. + // Fetch from bundle into a temporary ref, then update the target branch. + // Fetching directly into refs/heads/ fails when that branch is + // currently checked out in the safe_outputs job. // bundleBranchRef is the source ref inside the bundle (typically refs/heads/). try { - await exec.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:refs/heads/${branchName}`]); + await exec.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`]); } 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. @@ -1287,17 +1291,26 @@ async function main(config = {}) { 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}`]); + await exec.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`]); } 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(`Fetched bundle to ${bundleTempRef}`); + await exec.exec("git", ["update-ref", bundleTargetRef, bundleTempRef]); core.info(`Created local branch ${branchName} from bundle`); await exec.exec("git", ["checkout", branchName]); + await exec.exec("git", ["reset", "--hard"]); core.info(`Checked out branch ${branchName} from bundle`); } catch (bundleError) { core.error(`Failed to apply bundle: ${bundleError instanceof Error ? bundleError.message : String(bundleError)}`); return { success: false, error: "Failed to apply bundle" }; + } finally { + try { + await exec.exec("git", ["update-ref", "-d", bundleTempRef]); + } catch { + // Non-fatal cleanup + } } // Push the commits from the bundle to the remote branch diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index ca3ce36bdca..36e244dea75 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -251,7 +251,9 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", "--unshallow", "origin"], expect.any(Object)); - expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/feature/test:refs/heads/feature/test"]); + expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/feature/test:refs/bundles/create-pr-feature-test"]); + expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/feature/test", "refs/bundles/create-pr-feature-test"]); + expect(global.exec.exec).toHaveBeenCalledWith("git", ["reset", "--hard"]); const unshallowCallIndex = global.exec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === "--unshallow"); const bundleFetchCallIndex = global.exec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); expect(unshallowCallIndex).toBeGreaterThanOrEqual(0); @@ -282,7 +284,7 @@ index 0000000..abc1234 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") { + if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && args[2] === "refs/heads/ops-review-may09-2026:refs/bundles/create-pr-ops-review-may09-2026") { throw new Error("fatal: couldn't find remote ref refs/heads/ops-review-may09-2026"); } return Promise.resolve(0); @@ -314,7 +316,40 @@ index 0000000..abc1234 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"]); + expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/main:refs/bundles/create-pr-ops-review-may09-2026"]); + }); + + it("should not fetch a bundle directly into the target branch", 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"); + + 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: "autoloop/perf-comparison", patch_path: patchPath, bundle_path: bundlePath }, {}); + + expect(result.success).toBe(true); + expect(global.exec.exec).not.toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/heads/autoloop/perf-comparison"]); + expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/bundles/create-pr-autoloop-perf-comparison"]); + expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/autoloop/perf-comparison", "refs/bundles/create-pr-autoloop-perf-comparison"]); }); }); From 3810fa7e7b72b166bd9d24d453d5432389a824d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 11:38:24 +0000 Subject: [PATCH 03/10] Add bundle fetch integration test Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 104 +++++++++------- ...e_pull_request_bundle_integration.test.cjs | 115 ++++++++++++++++++ 2 files changed, 175 insertions(+), 44 deletions(-) create mode 100644 actions/setup/js/create_pull_request_bundle_integration.test.cjs diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 732f28617a7..ad8d9044505 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -69,6 +69,62 @@ const HANDLER_TYPE = "create_pull_request"; /** @type {string} Label always added to fallback issues so the triage system can find them */ const MANAGED_FALLBACK_ISSUE_LABEL = "agentic-workflows"; +/** + * Apply a git bundle to a local branch without fetching directly into the branch ref. + * Fetching directly into refs/heads/ fails when that branch is currently checked out. + * + * @param {string} bundleFilePath - Path to the bundle file + * @param {string} branchName - Target branch name + * @param {string} originalAgentBranch - Original source branch name from the agent, if different + * @param {{ exec: Function, getExecOutput: Function }} execApi - GitHub Actions exec API + * @returns {Promise} + */ +async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBranch, execApi) { + let bundleBranchRef = `refs/heads/${originalAgentBranch || branchName}`; + const bundleTargetRef = `refs/heads/${branchName}`; + const bundleTempRef = `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}`; + + try { + await ensureFullHistoryForBundle(execApi); + + // Fetch from bundle into a temporary ref, then update the target branch. + // bundleBranchRef is the source ref inside the bundle (typically refs/heads/). + try { + await execApi.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`]); + } 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 execApi.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 execApi.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`]); + } 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(`Fetched bundle to ${bundleTempRef}`); + await execApi.exec("git", ["update-ref", bundleTargetRef, bundleTempRef]); + core.info(`Created local branch ${branchName} from bundle`); + await execApi.exec("git", ["checkout", branchName]); + await execApi.exec("git", ["reset", "--hard"]); + core.info(`Checked out branch ${branchName} from bundle`); + } finally { + try { + await execApi.exec("git", ["update-ref", "-d", bundleTempRef]); + } catch { + // Non-fatal cleanup + } + } +} + /** * Determines if a label API error is transient and worth retrying. * Returns true for: @@ -1265,52 +1321,12 @@ 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}`); - let bundleBranchRef = `refs/heads/${originalAgentBranch || branchName}`; - const bundleTargetRef = `refs/heads/${branchName}`; - const bundleTempRef = `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}`; + const bundleSourceRef = `refs/heads/${originalAgentBranch || branchName}`; try { - await ensureFullHistoryForBundle(exec); - - // Fetch from bundle into a temporary ref, then update the target branch. - // Fetching directly into refs/heads/ fails when that branch is - // currently checked out in the safe_outputs job. - // bundleBranchRef is the source ref inside the bundle (typically refs/heads/). - try { - await exec.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`]); - } 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}:${bundleTempRef}`]); - } 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(`Fetched bundle to ${bundleTempRef}`); - await exec.exec("git", ["update-ref", bundleTargetRef, bundleTempRef]); - core.info(`Created local branch ${branchName} from bundle`); - await exec.exec("git", ["checkout", branchName]); - await exec.exec("git", ["reset", "--hard"]); - core.info(`Checked out branch ${branchName} from bundle`); + await applyBundleToBranch(bundleFilePath, branchName, originalAgentBranch, exec); } catch (bundleError) { core.error(`Failed to apply bundle: ${bundleError instanceof Error ? bundleError.message : String(bundleError)}`); return { success: false, error: "Failed to apply bundle" }; - } finally { - try { - await exec.exec("git", ["update-ref", "-d", bundleTempRef]); - } catch { - // Non-fatal cleanup - } } // Push the commits from the bundle to the remote branch @@ -1367,7 +1383,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} ${bundleBranchRef}:refs/heads/${branchName} +git fetch /tmp/agent-${runId}/${artifactFileName} ${bundleSourceRef}:refs/heads/${branchName} git checkout ${branchName} # Push the branch to origin @@ -2072,4 +2088,4 @@ ${patchPreview}`; }; // End of handleCreatePullRequest } // End of main -module.exports = { main, enforcePullRequestLimits, countUniquePatchFiles, parseDiffGitHeader }; +module.exports = { main, enforcePullRequestLimits, countUniquePatchFiles, parseDiffGitHeader, applyBundleToBranch }; diff --git a/actions/setup/js/create_pull_request_bundle_integration.test.cjs b/actions/setup/js/create_pull_request_bundle_integration.test.cjs new file mode 100644 index 00000000000..393c99a79fa --- /dev/null +++ b/actions/setup/js/create_pull_request_bundle_integration.test.cjs @@ -0,0 +1,115 @@ +/** + * Integration tests for create_pull_request bundle application. + * + * These tests run real git commands against temporary repositories to verify + * bundle handling for checked-out target branches. + */ + +import { describe, it, expect, afterEach, vi } from "vitest"; +import { createRequire } from "module"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { spawnSync } from "child_process"; + +const require = createRequire(import.meta.url); + +global.core = { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warning: vi.fn(), +}; + +function execGit(args, options = {}) { + const result = spawnSync("git", args, { + encoding: "utf8", + ...options, + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0 && !options.allowFailure) { + throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); + } + return result; +} + +function createRepo(prefix) { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + execGit(["init"], { cwd: repoDir }); + execGit(["config", "user.name", "Test User"], { cwd: repoDir }); + execGit(["config", "user.email", "test@example.com"], { cwd: repoDir }); + return repoDir; +} + +function createExecApi(cwd) { + return { + async exec(command, args = []) { + if (command !== "git") { + throw new Error(`unexpected command: ${command}`); + } + const result = execGit(args, { cwd, allowFailure: true }); + if (result.status !== 0) { + throw new Error(result.stderr || result.stdout); + } + return result.status; + }, + async getExecOutput(command, args = []) { + if (command !== "git") { + throw new Error(`unexpected command: ${command}`); + } + const result = execGit(args, { cwd, allowFailure: true }); + if (result.status !== 0) { + throw new Error(result.stderr || result.stdout); + } + return { exitCode: result.status, stdout: result.stdout, stderr: result.stderr }; + }, + }; +} + +describe("create_pull_request bundle integration", () => { + const tempDirs = []; + + afterEach(() => { + for (const tempDir of tempDirs.splice(0)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + vi.clearAllMocks(); + }); + + it("applies a bundle when the target branch is currently checked out", async () => { + const branchName = "autoloop/perf-comparison"; + const sourceRepo = createRepo("create-pr-bundle-source-"); + const targetRepo = createRepo("create-pr-bundle-target-"); + tempDirs.push(sourceRepo, targetRepo); + + fs.writeFileSync(path.join(sourceRepo, "file.txt"), "base\n"); + execGit(["add", "file.txt"], { cwd: sourceRepo }); + execGit(["commit", "-m", "base"], { cwd: sourceRepo }); + execGit(["branch", "-M", "main"], { cwd: sourceRepo }); + execGit(["checkout", "-b", branchName], { cwd: sourceRepo }); + fs.writeFileSync(path.join(sourceRepo, "file.txt"), "bundle tip\n"); + execGit(["commit", "-am", "bundle tip"], { cwd: sourceRepo }); + const expectedHead = execGit(["rev-parse", "HEAD"], { cwd: sourceRepo }).stdout.trim(); + const bundlePath = path.join(sourceRepo, "change.bundle"); + execGit(["bundle", "create", bundlePath, `refs/heads/${branchName}`], { cwd: sourceRepo }); + + fs.writeFileSync(path.join(targetRepo, "file.txt"), "checked out branch before bundle\n"); + execGit(["add", "file.txt"], { cwd: targetRepo }); + execGit(["commit", "-m", "old branch state"], { cwd: targetRepo }); + execGit(["checkout", "-b", branchName], { cwd: targetRepo }); + + const directFetchResult = execGit(["fetch", bundlePath, `refs/heads/${branchName}:refs/heads/${branchName}`], { cwd: targetRepo, allowFailure: true }); + expect(directFetchResult.status).not.toBe(0); + expect(directFetchResult.stderr).toContain("refusing to fetch into branch"); + + const { applyBundleToBranch } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, "", createExecApi(targetRepo)); + + const actualHead = execGit(["rev-parse", "HEAD"], { cwd: targetRepo }).stdout.trim(); + expect(actualHead).toBe(expectedHead); + expect(fs.readFileSync(path.join(targetRepo, "file.txt"), "utf8")).toBe("bundle tip\n"); + expect(execGit(["show-ref", "--verify", "refs/bundles/create-pr-autoloop-perf-comparison"], { cwd: targetRepo, allowFailure: true }).status).not.toBe(0); + }); +}); From 083ed99ce825d6644dadad19b7500238d7dfe542 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 11:39:28 +0000 Subject: [PATCH 04/10] Address integration validation feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 3 +-- .../js/create_pull_request_bundle_integration.test.cjs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index ad8d9044505..fdff946ce17 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1321,7 +1321,6 @@ 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 bundleSourceRef = `refs/heads/${originalAgentBranch || branchName}`; try { await applyBundleToBranch(bundleFilePath, branchName, originalAgentBranch, exec); } catch (bundleError) { @@ -1383,7 +1382,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} ${bundleSourceRef}:refs/heads/${branchName} +git fetch /tmp/agent-${runId}/${artifactFileName} refs/heads/${originalAgentBranch || branchName}:refs/heads/${branchName} git checkout ${branchName} # Push the branch to origin diff --git a/actions/setup/js/create_pull_request_bundle_integration.test.cjs b/actions/setup/js/create_pull_request_bundle_integration.test.cjs index 393c99a79fa..17ed5fb0a95 100644 --- a/actions/setup/js/create_pull_request_bundle_integration.test.cjs +++ b/actions/setup/js/create_pull_request_bundle_integration.test.cjs @@ -100,9 +100,9 @@ describe("create_pull_request bundle integration", () => { execGit(["commit", "-m", "old branch state"], { cwd: targetRepo }); execGit(["checkout", "-b", branchName], { cwd: targetRepo }); - const directFetchResult = execGit(["fetch", bundlePath, `refs/heads/${branchName}:refs/heads/${branchName}`], { cwd: targetRepo, allowFailure: true }); - expect(directFetchResult.status).not.toBe(0); - expect(directFetchResult.stderr).toContain("refusing to fetch into branch"); + const checkedOutBranchFetchResult = execGit(["fetch", bundlePath, `refs/heads/${branchName}:refs/heads/${branchName}`], { cwd: targetRepo, allowFailure: true }); + expect(checkedOutBranchFetchResult.status).not.toBe(0); + expect(checkedOutBranchFetchResult.stderr).toContain("refusing to fetch into branch"); const { applyBundleToBranch } = require("./create_pull_request.cjs"); await applyBundleToBranch(bundlePath, branchName, "", createExecApi(targetRepo)); From 5513624ad4f6e42ae7058784379f0d4893b8faf3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 12:53:31 +0000 Subject: [PATCH 05/10] Update bundle fallback instructions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 9 ++++- actions/setup/js/create_pull_request.test.cjs | 39 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index fdff946ce17..6eb780534fb 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1364,6 +1364,8 @@ async function main(config = {}) { const runId = context.runId; const artifactFileName = bundleFilePath ? bundleFilePath.replace("/tmp/gh-aw/", "") : "aw-unknown.bundle"; + const fallbackBundleSourceRef = `refs/heads/${originalAgentBranch || branchName}`; + const fallbackBundleTempRef = `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}`; const fallbackBody = `${body} --- @@ -1381,9 +1383,12 @@ To create a pull request with the changes: # Download the artifact from the workflow run 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/${originalAgentBranch || branchName}:refs/heads/${branchName} +# Fetch the bundle into a temporary ref, then update the local branch +git fetch /tmp/agent-${runId}/${artifactFileName} ${fallbackBundleSourceRef}:${fallbackBundleTempRef} +git update-ref refs/heads/${branchName} ${fallbackBundleTempRef} git checkout ${branchName} +git reset --hard +git update-ref -d ${fallbackBundleTempRef} # Push the branch to origin git push origin ${branchName} diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 36e244dea75..e02b540c0c1 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -168,6 +168,7 @@ describe("create_pull_request - bundle transport shallow checkout", () => { get: vi.fn().mockResolvedValue({ data: { default_branch: "main" } }), }, issues: { + create: vi.fn().mockResolvedValue({ data: { number: 99, html_url: "https://github.com/test-owner/test-repo/issues/99" } }), addLabels: vi.fn().mockResolvedValue({}), }, }, @@ -351,6 +352,44 @@ index 0000000..abc1234 expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/bundles/create-pr-autoloop-perf-comparison"]); expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/autoloop/perf-comparison", "refs/bundles/create-pr-autoloop-perf-comparison"]); }); + + it("should give fallback issue bundle instructions that avoid direct branch fetches", 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, "aw-test.bundle"); + fs.writeFileSync(bundlePath, "bundle content"); + pushSignedSpy.mockRejectedValueOnce(new Error("push rejected")); + + 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: "autoloop/perf-comparison", patch_path: patchPath, bundle_path: bundlePath }, {}); + + expect(result.success).toBe(true); + expect(result.fallback_used).toBe(true); + + const fallbackIssueBody = global.github.rest.issues.create.mock.calls[0][0].body; + expect(fallbackIssueBody).toContain("refs/heads/autoloop/perf-comparison:refs/bundles/create-pr-autoloop-perf-comparison"); + expect(fallbackIssueBody).toContain("git update-ref refs/heads/autoloop/perf-comparison refs/bundles/create-pr-autoloop-perf-comparison"); + expect(fallbackIssueBody).toContain("git reset --hard"); + expect(fallbackIssueBody).not.toContain("refs/heads/autoloop/perf-comparison:refs/heads/autoloop/perf-comparison"); + }); }); describe("create_pull_request - fallback-as-issue configuration", () => { From 3f18c9f4a02518ea88e72811359cfdb8a0735841 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 12:56:20 +0000 Subject: [PATCH 06/10] Use unique bundle temp refs Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 14 +++++++++-- actions/setup/js/create_pull_request.test.cjs | 24 ++++++++++++------- ...e_pull_request_bundle_integration.test.cjs | 2 +- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 6eb780534fb..ddbb739d366 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -69,6 +69,14 @@ const HANDLER_TYPE = "create_pull_request"; /** @type {string} Label always added to fallback issues so the triage system can find them */ const MANAGED_FALLBACK_ISSUE_LABEL = "agentic-workflows"; +/** + * @param {string} branchName + * @returns {string} + */ +function createBundleTempRef(branchName) { + return `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}-${crypto.randomBytes(4).toString("hex")}`; +} + /** * Apply a git bundle to a local branch without fetching directly into the branch ref. * Fetching directly into refs/heads/ fails when that branch is currently checked out. @@ -82,7 +90,7 @@ const MANAGED_FALLBACK_ISSUE_LABEL = "agentic-workflows"; async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBranch, execApi) { let bundleBranchRef = `refs/heads/${originalAgentBranch || branchName}`; const bundleTargetRef = `refs/heads/${branchName}`; - const bundleTempRef = `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}`; + const bundleTempRef = createBundleTempRef(branchName); try { await ensureFullHistoryForBundle(execApi); @@ -1365,7 +1373,7 @@ async function main(config = {}) { const artifactFileName = bundleFilePath ? bundleFilePath.replace("/tmp/gh-aw/", "") : "aw-unknown.bundle"; const fallbackBundleSourceRef = `refs/heads/${originalAgentBranch || branchName}`; - const fallbackBundleTempRef = `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}`; + const fallbackBundleTempRef = createBundleTempRef(branchName); const fallbackBody = `${body} --- @@ -1387,7 +1395,9 @@ gh run download ${runId} -n agent -D /tmp/agent-${runId} git fetch /tmp/agent-${runId}/${artifactFileName} ${fallbackBundleSourceRef}:${fallbackBundleTempRef} git update-ref refs/heads/${branchName} ${fallbackBundleTempRef} git checkout ${branchName} +# Ensure the working tree matches the updated branch git reset --hard +# Remove the temporary bundle ref git update-ref -d ${fallbackBundleTempRef} # 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 e02b540c0c1..52f75deec11 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -252,8 +252,10 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", "--unshallow", "origin"], expect.any(Object)); - expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/feature/test:refs/bundles/create-pr-feature-test"]); - expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/feature/test", "refs/bundles/create-pr-feature-test"]); + const bundleFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + expect(bundleFetchCall[1][2]).toMatch(/^refs\/heads\/feature\/test:refs\/bundles\/create-pr-feature-test-[a-f0-9]{8}$/); + const bundleTempRef = bundleFetchCall[1][2].split(":")[1]; + expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/feature/test", bundleTempRef]); expect(global.exec.exec).toHaveBeenCalledWith("git", ["reset", "--hard"]); const unshallowCallIndex = global.exec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === "--unshallow"); const bundleFetchCallIndex = global.exec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); @@ -285,7 +287,7 @@ index 0000000..abc1234 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/bundles/create-pr-ops-review-may09-2026") { + if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && /^refs\/heads\/ops-review-may09-2026:refs\/bundles\/create-pr-ops-review-may09-2026-[a-f0-9]{8}$/.test(args[2])) { throw new Error("fatal: couldn't find remote ref refs/heads/ops-review-may09-2026"); } return Promise.resolve(0); @@ -317,7 +319,8 @@ index 0000000..abc1234 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/bundles/create-pr-ops-review-may09-2026"]); + const resolvedFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && args[2].startsWith("refs/heads/main:")); + expect(resolvedFetchCall[1][2]).toMatch(/^refs\/heads\/main:refs\/bundles\/create-pr-ops-review-may09-2026-[a-f0-9]{8}$/); }); it("should not fetch a bundle directly into the target branch", async () => { @@ -349,8 +352,10 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.exec).not.toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/heads/autoloop/perf-comparison"]); - expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/bundles/create-pr-autoloop-perf-comparison"]); - expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/autoloop/perf-comparison", "refs/bundles/create-pr-autoloop-perf-comparison"]); + const bundleFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + expect(bundleFetchCall[1][2]).toMatch(/^refs\/heads\/autoloop\/perf-comparison:refs\/bundles\/create-pr-autoloop-perf-comparison-[a-f0-9]{8}$/); + const bundleTempRef = bundleFetchCall[1][2].split(":")[1]; + expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/autoloop/perf-comparison", bundleTempRef]); }); it("should give fallback issue bundle instructions that avoid direct branch fetches", async () => { @@ -385,9 +390,12 @@ index 0000000..abc1234 expect(result.fallback_used).toBe(true); const fallbackIssueBody = global.github.rest.issues.create.mock.calls[0][0].body; - expect(fallbackIssueBody).toContain("refs/heads/autoloop/perf-comparison:refs/bundles/create-pr-autoloop-perf-comparison"); - expect(fallbackIssueBody).toContain("git update-ref refs/heads/autoloop/perf-comparison refs/bundles/create-pr-autoloop-perf-comparison"); + const tempRefMatch = fallbackIssueBody.match(/refs\/heads\/autoloop\/perf-comparison:(refs\/bundles\/create-pr-autoloop-perf-comparison-[a-f0-9]{8})/); + expect(tempRefMatch).not.toBeNull(); + const fallbackBundleTempRef = tempRefMatch[1]; + expect(fallbackIssueBody).toContain(`git update-ref refs/heads/autoloop/perf-comparison ${fallbackBundleTempRef}`); expect(fallbackIssueBody).toContain("git reset --hard"); + expect(fallbackIssueBody).toContain(`git update-ref -d ${fallbackBundleTempRef}`); expect(fallbackIssueBody).not.toContain("refs/heads/autoloop/perf-comparison:refs/heads/autoloop/perf-comparison"); }); }); diff --git a/actions/setup/js/create_pull_request_bundle_integration.test.cjs b/actions/setup/js/create_pull_request_bundle_integration.test.cjs index 17ed5fb0a95..839f77a5d02 100644 --- a/actions/setup/js/create_pull_request_bundle_integration.test.cjs +++ b/actions/setup/js/create_pull_request_bundle_integration.test.cjs @@ -110,6 +110,6 @@ describe("create_pull_request bundle integration", () => { const actualHead = execGit(["rev-parse", "HEAD"], { cwd: targetRepo }).stdout.trim(); expect(actualHead).toBe(expectedHead); expect(fs.readFileSync(path.join(targetRepo, "file.txt"), "utf8")).toBe("bundle tip\n"); - expect(execGit(["show-ref", "--verify", "refs/bundles/create-pr-autoloop-perf-comparison"], { cwd: targetRepo, allowFailure: true }).status).not.toBe(0); + expect(execGit(["for-each-ref", "--format=%(refname)", "refs/bundles/create-pr-autoloop-perf-comparison-"], { cwd: targetRepo }).stdout).toBe(""); }); }); From 5fea75206d79f2c4a211965e6802881bcbf3aa9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 12:57:50 +0000 Subject: [PATCH 07/10] Address bundle temp ref review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 4 +++- actions/setup/js/create_pull_request.test.cjs | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index ddbb739d366..13c1c3cf937 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -74,7 +74,9 @@ const MANAGED_FALLBACK_ISSUE_LABEL = "agentic-workflows"; * @returns {string} */ function createBundleTempRef(branchName) { - return `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}-${crypto.randomBytes(4).toString("hex")}`; + // Avoid collisions between sanitized branch names such as foo/bar and foo-bar. + const suffix = crypto.randomBytes(4).toString("hex"); + return `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}-${suffix}`; } /** diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 52f75deec11..43bf942063a 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -253,6 +253,9 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", "--unshallow", "origin"], expect.any(Object)); const bundleFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + if (!bundleFetchCall) { + throw new Error("expected bundle fetch call"); + } expect(bundleFetchCall[1][2]).toMatch(/^refs\/heads\/feature\/test:refs\/bundles\/create-pr-feature-test-[a-f0-9]{8}$/); const bundleTempRef = bundleFetchCall[1][2].split(":")[1]; expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/feature/test", bundleTempRef]); @@ -320,6 +323,9 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.getExecOutput).toHaveBeenCalledWith("git", ["bundle", "list-heads", bundlePath]); const resolvedFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && args[2].startsWith("refs/heads/main:")); + if (!resolvedFetchCall) { + throw new Error("expected resolved bundle fetch call"); + } expect(resolvedFetchCall[1][2]).toMatch(/^refs\/heads\/main:refs\/bundles\/create-pr-ops-review-may09-2026-[a-f0-9]{8}$/); }); @@ -353,6 +359,9 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.exec).not.toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/heads/autoloop/perf-comparison"]); const bundleFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + if (!bundleFetchCall) { + throw new Error("expected bundle fetch call"); + } expect(bundleFetchCall[1][2]).toMatch(/^refs\/heads\/autoloop\/perf-comparison:refs\/bundles\/create-pr-autoloop-perf-comparison-[a-f0-9]{8}$/); const bundleTempRef = bundleFetchCall[1][2].split(":")[1]; expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/autoloop/perf-comparison", bundleTempRef]); @@ -391,7 +400,9 @@ index 0000000..abc1234 const fallbackIssueBody = global.github.rest.issues.create.mock.calls[0][0].body; const tempRefMatch = fallbackIssueBody.match(/refs\/heads\/autoloop\/perf-comparison:(refs\/bundles\/create-pr-autoloop-perf-comparison-[a-f0-9]{8})/); - expect(tempRefMatch).not.toBeNull(); + if (!tempRefMatch?.[1]) { + throw new Error("expected fallback bundle temp ref"); + } const fallbackBundleTempRef = tempRefMatch[1]; expect(fallbackIssueBody).toContain(`git update-ref refs/heads/autoloop/perf-comparison ${fallbackBundleTempRef}`); expect(fallbackIssueBody).toContain("git reset --hard"); From fe85167a3d553ca703aec0ba8ab3950fcb001d9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 12:59:02 +0000 Subject: [PATCH 08/10] Document bundle temp ref cleanup Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 9 +++++--- ...e_pull_request_bundle_integration.test.cjs | 21 ++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 13c1c3cf937..0fff6aa65a4 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -70,11 +70,14 @@ const HANDLER_TYPE = "create_pull_request"; const MANAGED_FALLBACK_ISSUE_LABEL = "agentic-workflows"; /** - * @param {string} branchName - * @returns {string} + * Creates a temporary refs/bundles ref for applying create_pull_request bundles. + * Branch names are sanitized for ref compatibility, and a short crypto-random + * suffix avoids collisions between branches that sanitize to the same value. + * + * @param {string} branchName - Target branch name + * @returns {string} Temporary bundle ref name */ function createBundleTempRef(branchName) { - // Avoid collisions between sanitized branch names such as foo/bar and foo-bar. const suffix = crypto.randomBytes(4).toString("hex"); return `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}-${suffix}`; } diff --git a/actions/setup/js/create_pull_request_bundle_integration.test.cjs b/actions/setup/js/create_pull_request_bundle_integration.test.cjs index 839f77a5d02..50303997d91 100644 --- a/actions/setup/js/create_pull_request_bundle_integration.test.cjs +++ b/actions/setup/js/create_pull_request_bundle_integration.test.cjs @@ -43,7 +43,7 @@ function createRepo(prefix) { return repoDir; } -function createExecApi(cwd) { +function createExecApi(cwd, onExec) { return { async exec(command, args = []) { if (command !== "git") { @@ -53,6 +53,9 @@ function createExecApi(cwd) { if (result.status !== 0) { throw new Error(result.stderr || result.stdout); } + if (onExec) { + onExec(args); + } return result.status; }, async getExecOutput(command, args = []) { @@ -104,12 +107,24 @@ describe("create_pull_request bundle integration", () => { expect(checkedOutBranchFetchResult.status).not.toBe(0); expect(checkedOutBranchFetchResult.stderr).toContain("refusing to fetch into branch"); + let bundleTempRef = ""; const { applyBundleToBranch } = require("./create_pull_request.cjs"); - await applyBundleToBranch(bundlePath, branchName, "", createExecApi(targetRepo)); + await applyBundleToBranch( + bundlePath, + branchName, + "", + createExecApi(targetRepo, args => { + if (args[0] === "fetch" && args[1] === bundlePath) { + bundleTempRef = args[2].split(":")[1]; + expect(execGit(["show-ref", "--verify", bundleTempRef], { cwd: targetRepo }).status).toBe(0); + } + }) + ); const actualHead = execGit(["rev-parse", "HEAD"], { cwd: targetRepo }).stdout.trim(); expect(actualHead).toBe(expectedHead); expect(fs.readFileSync(path.join(targetRepo, "file.txt"), "utf8")).toBe("bundle tip\n"); - expect(execGit(["for-each-ref", "--format=%(refname)", "refs/bundles/create-pr-autoloop-perf-comparison-"], { cwd: targetRepo }).stdout).toBe(""); + expect(bundleTempRef).toMatch(/^refs\/bundles\/create-pr-autoloop-perf-comparison-[a-f0-9]{8}$/); + expect(execGit(["show-ref", "--verify", bundleTempRef], { cwd: targetRepo, allowFailure: true }).status).not.toBe(0); }); }); From afadfdd836af663be1d8756927ef9879592b93e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 13:07:15 +0000 Subject: [PATCH 09/10] Address bundle review comments Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 1 + ...e_pull_request_bundle_integration.test.cjs | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 0fff6aa65a4..96a34c6d323 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -127,6 +127,7 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran await execApi.exec("git", ["update-ref", bundleTargetRef, bundleTempRef]); core.info(`Created local branch ${branchName} from bundle`); await execApi.exec("git", ["checkout", branchName]); + // Ensure the working tree matches the new HEAD in case checkout left any index/worktree drift. await execApi.exec("git", ["reset", "--hard"]); core.info(`Checked out branch ${branchName} from bundle`); } finally { diff --git a/actions/setup/js/create_pull_request_bundle_integration.test.cjs b/actions/setup/js/create_pull_request_bundle_integration.test.cjs index 50303997d91..9c37f784c2f 100644 --- a/actions/setup/js/create_pull_request_bundle_integration.test.cjs +++ b/actions/setup/js/create_pull_request_bundle_integration.test.cjs @@ -127,4 +127,51 @@ describe("create_pull_request bundle integration", () => { expect(bundleTempRef).toMatch(/^refs\/bundles\/create-pr-autoloop-perf-comparison-[a-f0-9]{8}$/); expect(execGit(["show-ref", "--verify", bundleTempRef], { cwd: targetRepo, allowFailure: true }).status).not.toBe(0); }); + + it("cleans up the temp ref when updating the target branch fails", async () => { + const branchName = "autoloop/perf-comparison"; + const sourceRepo = createRepo("create-pr-bundle-source-"); + const targetRepo = createRepo("create-pr-bundle-target-"); + tempDirs.push(sourceRepo, targetRepo); + + fs.writeFileSync(path.join(sourceRepo, "file.txt"), "base\n"); + execGit(["add", "file.txt"], { cwd: sourceRepo }); + execGit(["commit", "-m", "base"], { cwd: sourceRepo }); + execGit(["branch", "-M", "main"], { cwd: sourceRepo }); + execGit(["checkout", "-b", branchName], { cwd: sourceRepo }); + fs.writeFileSync(path.join(sourceRepo, "file.txt"), "bundle tip\n"); + execGit(["commit", "-am", "bundle tip"], { cwd: sourceRepo }); + const bundlePath = path.join(sourceRepo, "change.bundle"); + execGit(["bundle", "create", bundlePath, `refs/heads/${branchName}`], { cwd: sourceRepo }); + + fs.writeFileSync(path.join(targetRepo, "file.txt"), "old branch state\n"); + execGit(["add", "file.txt"], { cwd: targetRepo }); + execGit(["commit", "-m", "old branch state"], { cwd: targetRepo }); + execGit(["checkout", "-b", branchName], { cwd: targetRepo }); + const originalHead = execGit(["rev-parse", `refs/heads/${branchName}`], { cwd: targetRepo }).stdout.trim(); + + let bundleTempRef = ""; + const execApi = createExecApi(targetRepo, args => { + if (args[0] === "fetch" && args[1] === bundlePath) { + bundleTempRef = args[2].split(":")[1]; + } + }); + const { applyBundleToBranch } = require("./create_pull_request.cjs"); + + await expect( + applyBundleToBranch(bundlePath, branchName, "", { + ...execApi, + async exec(command, args = []) { + if (command === "git" && args[0] === "update-ref" && args[1] === `refs/heads/${branchName}`) { + throw new Error("simulated update-ref failure"); + } + return execApi.exec(command, args); + }, + }) + ).rejects.toThrow("simulated update-ref failure"); + + expect(bundleTempRef).toMatch(/^refs\/bundles\/create-pr-autoloop-perf-comparison-[a-f0-9]{8}$/); + expect(execGit(["show-ref", "--verify", bundleTempRef], { cwd: targetRepo, allowFailure: true }).status).not.toBe(0); + expect(execGit(["rev-parse", `refs/heads/${branchName}`], { cwd: targetRepo }).stdout.trim()).toBe(originalHead); + }); }); From fcd70125cbb317192bf13aef10eb557f7cd21408 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 13:07:56 +0000 Subject: [PATCH 10/10] Tweak bundle reset comment wording Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 96a34c6d323..5dc2d97a1dc 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -127,7 +127,7 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran await execApi.exec("git", ["update-ref", bundleTargetRef, bundleTempRef]); core.info(`Created local branch ${branchName} from bundle`); await execApi.exec("git", ["checkout", branchName]); - // Ensure the working tree matches the new HEAD in case checkout left any index/worktree drift. + // Ensure the working tree matches the new HEAD in case checkout left any index/working tree drift. await execApi.exec("git", ["reset", "--hard"]); core.info(`Checked out branch ${branchName} from bundle`); } finally {