diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 5454a7e4bb..6aecf628af 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -603,9 +603,17 @@ async function main(config = {}) { await exec.exec("git", ["fetch", bundleFilePath, `refs/heads/${message.branch}:${bundleRef}`], baseGitOpts); core.info(`Fetched bundle to ${bundleRef}`); - // Fast-forward the current branch to the bundle tip - await exec.exec("git", ["merge", "--ff-only", bundleRef], baseGitOpts); - core.info("Fast-forwarded branch to bundle tip"); + // Point the checked-out branch at the bundle tip directly. In shallow + // checkouts, merge --ff-only can fail to discover the ancestry even + // when the bundle tip is based on the current branch tip and the + // prerequisite exists locally. + const updateRefArgs = ["update-ref", `refs/heads/${branchName}`, bundleRef]; + if (remoteHeadBeforePatch) { + updateRefArgs.push(remoteHeadBeforePatch); + } + await exec.exec("git", updateRefArgs, baseGitOpts); + await exec.exec("git", ["reset", "--hard"], baseGitOpts); + core.info("Updated branch to bundle tip"); // Clean up the temporary ref try { diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs index 486573ddd1..075a6f7edc 100644 --- a/actions/setup/js/push_to_pull_request_branch.test.cjs +++ b/actions/setup/js/push_to_pull_request_branch.test.cjs @@ -1199,6 +1199,40 @@ index 0000000..abc1234 }); }); + // ────────────────────────────────────────────────────── + // Bundle Transport Application + // ────────────────────────────────────────────────────── + + describe("bundle transport application", () => { + it("should apply bundle transport by updating the branch ref instead of merging", async () => { + const bundlePath = path.join(tempDir, "test.bundle"); + const patchPath = createPatchFile("small patch content"); + fs.writeFileSync(bundlePath, "bundle content"); + + const pushSignedCommitsModule = require("./push_signed_commits.cjs"); + const pushSignedSpy = vi.spyOn(pushSignedCommitsModule, "pushSignedCommits").mockResolvedValue("bundle-tip"); + + try { + mockExec.getExecOutput + .mockResolvedValueOnce({ exitCode: 0, stdout: "remote-head\trefs/heads/feature-branch\n", stderr: "" }) // preflight ls-remote + .mockResolvedValueOnce({ exitCode: 0, stdout: "remote-head\n", stderr: "" }) // rev-parse HEAD before bundle + .mockResolvedValueOnce({ exitCode: 0, stdout: "2\n", stderr: "" }); // rev-list --count + + const module = await loadModule(); + const handler = await module.main({}); + const result = await handler({ branch: "feature-branch", patch_path: patchPath, bundle_path: bundlePath, diff_size: 5 * 1024 }, {}); + + expect(result.success).toBe(true); + expect(mockExec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/feature-branch:refs/bundles/push-feature-branch"], expect.any(Object)); + expect(mockExec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/feature-branch", "refs/bundles/push-feature-branch", "remote-head"], expect.any(Object)); + expect(mockExec.exec).toHaveBeenCalledWith("git", ["reset", "--hard"], expect.any(Object)); + expect(mockExec.exec).not.toHaveBeenCalledWith("git", ["merge", "--ff-only", "refs/bundles/push-feature-branch"], expect.any(Object)); + } finally { + pushSignedSpy.mockRestore(); + } + }); + }); + // ────────────────────────────────────────────────────── // Title Prefix and Labels Validation // ──────────────────────────────────────────────────────