From 013c18f68d1598f3dcf82a405caa84a69479fb9b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 23:28:03 +0000
Subject: [PATCH 1/3] Initial plan
From a582dbe2837a6db304635bc36ffabe26a2ed8420 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 20 Apr 2026 23:37:07 +0000
Subject: [PATCH 2/3] Fix preserve-branch-name silent bypass on remote branch
collision
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e96fa9ed-55e3-405c-9607-653a6964420d
Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
---
actions/setup/js/create_pull_request.cjs | 21 +++++++
actions/setup/js/create_pull_request.test.cjs | 57 +++++++++++++++++++
.../reference/safe-outputs-pull-requests.md | 2 +
3 files changed, 80 insertions(+)
diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs
index 2acf9e735ce..b66d2e45d4b 100644
--- a/actions/setup/js/create_pull_request.cjs
+++ b/actions/setup/js/create_pull_request.cjs
@@ -983,6 +983,13 @@ async function main(config = {}) {
}
if (remoteBranchExists) {
+ if (preserveBranchName) {
+ throw new Error(
+ `Remote branch "${branchName}" already exists and preserve-branch-name is enabled. ` +
+ `Refusing to silently rename the branch. Either delete the remote branch, choose a different ` +
+ `branch name, or disable preserve-branch-name to allow a random suffix to be appended.`
+ );
+ }
core.warning(`Remote branch ${branchName} already exists - appending random suffix`);
const extraHex = crypto.randomBytes(4).toString("hex");
const oldBranch = branchName;
@@ -1211,6 +1218,13 @@ gh pr create --title '${title}' --base ${baseBranch} --head ${branchName} --repo
}
if (remoteBranchExists) {
+ if (preserveBranchName) {
+ throw new Error(
+ `Remote branch "${branchName}" already exists and preserve-branch-name is enabled. ` +
+ `Refusing to silently rename the branch. Either delete the remote branch, choose a different ` +
+ `branch name, or disable preserve-branch-name to allow a random suffix to be appended.`
+ );
+ }
core.warning(`Remote branch ${branchName} already exists - appending random suffix`);
const extraHex = crypto.randomBytes(4).toString("hex");
const oldBranch = branchName;
@@ -1374,6 +1388,13 @@ ${patchPreview}`;
}
if (remoteBranchExists) {
+ if (preserveBranchName) {
+ throw new Error(
+ `Remote branch "${branchName}" already exists and preserve-branch-name is enabled. ` +
+ `Refusing to silently rename the branch. Either delete the remote branch, choose a different ` +
+ `branch name, or disable preserve-branch-name to allow a random suffix to be appended.`
+ );
+ }
core.warning(`Remote branch ${branchName} already exists - appending random suffix`);
const extraHex = crypto.randomBytes(4).toString("hex");
const oldBranch = branchName;
diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs
index 97868b46a57..ad1da7f9b81 100644
--- a/actions/setup/js/create_pull_request.test.cjs
+++ b/actions/setup/js/create_pull_request.test.cjs
@@ -1574,6 +1574,63 @@ describe("create_pull_request - patch apply fallback to original base commit", (
expect(result.error).toBe("Failed to apply patch");
expect(global.core.warning).toHaveBeenCalledWith("No base_commit recorded in safe output entry - fallback not possible");
});
+
+ it("should fail loudly when preserve-branch-name is true and remote branch already exists", async () => {
+ // Simulate the remote branch existing (ls-remote returns content)
+ global.exec = {
+ exec: vi.fn().mockResolvedValue(0),
+ getExecOutput: vi.fn().mockImplementation((cmd, args) => {
+ const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
+ if (cmdStr.includes("ls-remote --heads origin")) {
+ return Promise.resolve({ exitCode: 0, stdout: "abc123\trefs/heads/preserve-me\n", stderr: "" });
+ }
+ return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" });
+ }),
+ };
+
+ const { main } = require("./create_pull_request.cjs");
+ const handler = await main({ preserve_branch_name: true, fallback_as_issue: false });
+
+ const result = await handler({ title: "Test PR", body: "Test body", patch_path: patchFilePath, branch: "preserve-me", base_commit: MOCK_BASE_COMMIT_SHA }, {});
+
+ expect(result.success).toBe(false);
+ expect(result.error_type).toBe("push_failed");
+ expect(result.error).toContain('Remote branch "preserve-me" already exists');
+ expect(result.error).toContain("preserve-branch-name is enabled");
+ // Critical: should NOT have warned about appending random suffix (silent bypass)
+ const warningCalls = global.core.warning.mock.calls.map(call => String(call[0]));
+ expect(warningCalls.some(msg => msg.includes("appending random suffix"))).toBe(false);
+ });
+
+ it("should append random suffix when preserve-branch-name is false and remote branch already exists", async () => {
+ let renameCalled = false;
+ global.exec = {
+ exec: vi.fn().mockImplementation((cmd, args) => {
+ const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
+ if (cmdStr.includes("git branch -m")) {
+ renameCalled = true;
+ }
+ return Promise.resolve(0);
+ }),
+ getExecOutput: vi.fn().mockImplementation((cmd, args) => {
+ const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
+ if (cmdStr.includes("ls-remote --heads origin")) {
+ return Promise.resolve({ exitCode: 0, stdout: "abc123\trefs/heads/some-branch\n", stderr: "" });
+ }
+ return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" });
+ }),
+ };
+
+ const { main } = require("./create_pull_request.cjs");
+ const handler = await main({});
+
+ const result = await handler({ title: "Test PR", body: "Test body", patch_path: patchFilePath, branch: "some-branch", base_commit: MOCK_BASE_COMMIT_SHA }, {});
+
+ expect(result.success).toBe(true);
+ expect(renameCalled).toBe(true);
+ const warningCalls = global.core.warning.mock.calls.map(call => String(call[0]));
+ expect(warningCalls.some(msg => msg.includes("appending random suffix"))).toBe(true);
+ });
});
describe("create_pull_request - copilot assignee on fallback issues", () => {
diff --git a/docs/src/content/docs/reference/safe-outputs-pull-requests.md b/docs/src/content/docs/reference/safe-outputs-pull-requests.md
index 4583974d3d7..af777a6be7c 100644
--- a/docs/src/content/docs/reference/safe-outputs-pull-requests.md
+++ b/docs/src/content/docs/reference/safe-outputs-pull-requests.md
@@ -83,6 +83,8 @@ The `excluded-files` field accepts a list of glob patterns. Each matching file i
The `preserve-branch-name` field, when set to `true`, omits the random hex salt suffix that is normally appended to the agent-specified branch name. This is useful when the target repository enforces branch naming conventions such as Jira keys in uppercase (e.g., `bugfix/BR-329-red` instead of `bugfix/br-329-red-cde2a954`). Invalid characters are always replaced for security, and casing is always preserved regardless of this setting. Defaults to `false`.
+When `preserve-branch-name: true` and the agent-supplied branch name already exists on the remote, the workflow fails with an explicit error rather than silently appending a random suffix. To resolve, delete the existing remote branch, choose a different branch name, or disable `preserve-branch-name` to allow collision-avoidance via a random suffix.
+
The `draft` field is a **configuration policy**, not a default. Whatever value is set in the workflow frontmatter is always used — the agent cannot override it at runtime.
By default, when a workflow is triggered from an issue, the `create-pull-request` handler automatically appends `- Fixes #N` to the PR description if no closing keyword is already present. This causes GitHub to auto-close the triggering issue when the PR is merged. Set `auto-close-issue: false` to opt out of this behavior — useful for partial-work PRs, multi-PR workflows, or any case where the PR should reference but not close the issue.
From 65bfa286afb1771d4369a6eadc251eed48ca75fb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Apr 2026 00:44:50 +0000
Subject: [PATCH 3/3] Extract remote-branch collision handler; set error_type
on allow-empty push failure
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/fab5372a-c934-4afd-bef1-c41d44e169ff
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
actions/setup/js/create_pull_request.cjs | 127 ++++++++---------------
1 file changed, 46 insertions(+), 81 deletions(-)
diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs
index b66d2e45d4b..48e828c81a3 100644
--- a/actions/setup/js/create_pull_request.cjs
+++ b/actions/setup/js/create_pull_request.cjs
@@ -273,6 +273,48 @@ function generatePatchPreview(patchContent) {
return `\n\n${summary}
\n\n\`\`\`diff\n${preview}${truncated ? "\n... (truncated)" : ""}\n\`\`\`\n\n `;
}
+/**
+ * Check whether the remote branch already exists and, if so, either fail loudly
+ * (when preserve-branch-name is enabled) or rename the local branch by appending
+ * a random hex suffix.
+ * @param {string} branchName - Current local branch name.
+ * @param {boolean} preserveBranchName - Whether preserve-branch-name is enabled.
+ * @returns {Promise} The (possibly renamed) branch name to use going forward.
+ * @throws {Error} If the remote branch exists and preserve-branch-name is true.
+ */
+async function handleRemoteBranchCollision(branchName, preserveBranchName) {
+ let remoteBranchExists = false;
+ try {
+ const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`);
+ if (stdout.trim()) {
+ remoteBranchExists = true;
+ }
+ } catch (checkError) {
+ core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`);
+ }
+
+ if (!remoteBranchExists) {
+ return branchName;
+ }
+
+ if (preserveBranchName) {
+ throw new Error(
+ `Remote branch "${branchName}" already exists and preserve-branch-name is enabled. ` +
+ `Refusing to silently rename the branch. Either delete the remote branch, choose a different ` +
+ `branch name, or disable preserve-branch-name to allow a random suffix to be appended.`
+ );
+ }
+
+ core.warning(`Remote branch ${branchName} already exists - appending random suffix`);
+ const extraHex = crypto.randomBytes(4).toString("hex");
+ const oldBranch = branchName;
+ const renamedBranch = `${branchName}-${extraHex}`;
+ // Rename local branch
+ await exec.exec(`git branch -m ${oldBranch} ${renamedBranch}`);
+ core.info(`Renamed branch to ${renamedBranch}`);
+ return renamedBranch;
+}
+
/**
* Main handler factory for create_pull_request
* Returns a message handler function that processes individual create_pull_request messages
@@ -971,33 +1013,7 @@ async function main(config = {}) {
// Push the commits from the bundle to the remote branch
try {
- // Check if remote branch already exists (optional precheck)
- let remoteBranchExists = false;
- try {
- const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`);
- if (stdout.trim()) {
- remoteBranchExists = true;
- }
- } catch (checkError) {
- core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`);
- }
-
- if (remoteBranchExists) {
- if (preserveBranchName) {
- throw new Error(
- `Remote branch "${branchName}" already exists and preserve-branch-name is enabled. ` +
- `Refusing to silently rename the branch. Either delete the remote branch, choose a different ` +
- `branch name, or disable preserve-branch-name to allow a random suffix to be appended.`
- );
- }
- core.warning(`Remote branch ${branchName} already exists - appending random suffix`);
- const extraHex = crypto.randomBytes(4).toString("hex");
- const oldBranch = branchName;
- branchName = `${branchName}-${extraHex}`;
- // Rename local branch
- await exec.exec(`git branch -m ${oldBranch} ${branchName}`);
- core.info(`Renamed branch to ${branchName}`);
- }
+ branchName = await handleRemoteBranchCollision(branchName, preserveBranchName);
await pushSignedCommits({
githubClient,
@@ -1206,33 +1222,7 @@ gh pr create --title '${title}' --base ${baseBranch} --head ${branchName} --repo
// Push the applied commits to the branch (with fallback to issue creation on failure)
try {
- // Check if remote branch already exists (optional precheck)
- let remoteBranchExists = false;
- try {
- const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`);
- if (stdout.trim()) {
- remoteBranchExists = true;
- }
- } catch (checkError) {
- core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`);
- }
-
- if (remoteBranchExists) {
- if (preserveBranchName) {
- throw new Error(
- `Remote branch "${branchName}" already exists and preserve-branch-name is enabled. ` +
- `Refusing to silently rename the branch. Either delete the remote branch, choose a different ` +
- `branch name, or disable preserve-branch-name to allow a random suffix to be appended.`
- );
- }
- core.warning(`Remote branch ${branchName} already exists - appending random suffix`);
- const extraHex = crypto.randomBytes(4).toString("hex");
- const oldBranch = branchName;
- branchName = `${branchName}-${extraHex}`;
- // Rename local branch
- await exec.exec(`git branch -m ${oldBranch} ${branchName}`);
- core.info(`Renamed branch to ${branchName}`);
- }
+ branchName = await handleRemoteBranchCollision(branchName, preserveBranchName);
await pushSignedCommits({
githubClient,
@@ -1376,33 +1366,7 @@ ${patchPreview}`;
await exec.exec(`git commit --allow-empty -m "Initialize"`);
core.info("Created empty commit");
- // Check if remote branch already exists (optional precheck)
- let remoteBranchExists = false;
- try {
- const { stdout } = await exec.getExecOutput(`git ls-remote --heads origin ${branchName}`);
- if (stdout.trim()) {
- remoteBranchExists = true;
- }
- } catch (checkError) {
- core.info(`Remote branch check failed (non-fatal): ${checkError instanceof Error ? checkError.message : String(checkError)}`);
- }
-
- if (remoteBranchExists) {
- if (preserveBranchName) {
- throw new Error(
- `Remote branch "${branchName}" already exists and preserve-branch-name is enabled. ` +
- `Refusing to silently rename the branch. Either delete the remote branch, choose a different ` +
- `branch name, or disable preserve-branch-name to allow a random suffix to be appended.`
- );
- }
- core.warning(`Remote branch ${branchName} already exists - appending random suffix`);
- const extraHex = crypto.randomBytes(4).toString("hex");
- const oldBranch = branchName;
- branchName = `${branchName}-${extraHex}`;
- // Rename local branch
- await exec.exec(`git branch -m ${oldBranch} ${branchName}`);
- core.info(`Renamed branch to ${branchName}`);
- }
+ branchName = await handleRemoteBranchCollision(branchName, preserveBranchName);
await pushSignedCommits({
githubClient,
@@ -1429,6 +1393,7 @@ ${patchPreview}`;
return {
success: false,
error,
+ error_type: "push_failed",
};
}
} else {