From e09c9316cf43b7f0de92fe2fa1670c68352ca4b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 03:51:47 +0000 Subject: [PATCH 1/8] Initial plan From b2ee21a680259cc09ebce1053d909ea80f076468 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 04:01:47 +0000 Subject: [PATCH 2/8] feat: improve max-patch-files error message and add docs - Make E003 error actionable: tell users the exact file count and the `max-patch-files` frontmatter field they can set to raise it - Document `max-patch-files` and `max-patch-size` options in safe-outputs-pull-requests.md with usage examples - Add `max-patch-files` and `max-patch-size` to the config YAML snippet - Update tests to assert the new actionable hint is present in E003 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 6 +++++- actions/setup/js/create_pull_request.test.cjs | 1 + .../reference/safe-outputs-pull-requests.md | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 601d978bd98..37c69fbdd4e 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -570,7 +570,11 @@ function enforcePullRequestLimits(patchContent, maxFiles = MAX_FILES) { // Check file count - max limit exceeded check if (fileCount > limit) { - throw new Error(`E003: Cannot create pull request with more than ${limit} files (received ${fileCount})`); + throw new Error( + `E003: Cannot create pull request with more than ${limit} files (received ${fileCount}). ` + + `To increase the limit, set \`max-patch-files: ${fileCount}\` (or higher) under ` + + `\`safe-outputs.create-pull-request\` in your workflow frontmatter.` + ); } } diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index da5bd73e2f5..a87977b0a32 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -958,6 +958,7 @@ describe("create_pull_request - max limit enforcement", () => { expect(() => enforcePullRequestLimits(patchContent)).toThrow("E003"); expect(() => enforcePullRequestLimits(patchContent)).toThrow("Cannot create pull request with more than 100 files"); expect(() => enforcePullRequestLimits(patchContent)).toThrow("received 101"); + expect(() => enforcePullRequestLimits(patchContent)).toThrow("max-patch-files"); }); it("should allow patches under the file limit", () => { 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 7c9a819bb69..761d02e4a19 100644 --- a/docs/src/content/docs/reference/safe-outputs-pull-requests.md +++ b/docs/src/content/docs/reference/safe-outputs-pull-requests.md @@ -52,6 +52,8 @@ safe-outputs: excluded-files: # files to omit from the patch entirely - "**/*.lock" - "dist/**" + max-patch-files: 300 # max unique files in the patch (default: 100) + max-patch-size: 2048 # max patch size in KB (default: 1024) github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions github-token-for-extra-empty-commit: ${{ secrets.CI_TOKEN }} # optional token to push empty commit triggering CI signed-commits: true # signed commits are required (default); set false to use git push directly @@ -84,6 +86,22 @@ safe-outputs: The `excluded-files` field accepts a list of glob patterns. Each matching file is stripped from the patch using `git format-patch`'s `:(exclude)` magic pathspec at generation time, so the file never appears in the commit. Excluded files are also exempt from `allowed-files` and `protected-files` checks. This is useful for suppressing auto-generated or lock files that the agent must not commit (e.g. `**/*.lock`, `dist/**`). Supports `*` (any characters except `/`) and `**` (any characters including `/`). +The `max-patch-files` field sets the maximum number of unique files allowed in a single PR's patch (default: `100`). Workflows that regenerate large sets of data or documentation files — for example, per-package API schemas or integration metadata — can raise this limit to accommodate their output. If the limit is exceeded, PR creation fails with an actionable error message that tells you the exact count and the field to set. Example for a workflow that routinely touches ~250 generated files: + +```yaml +safe-outputs: + create-pull-request: + max-patch-files: 300 +``` + +The `max-patch-size` field sets the maximum patch size in kilobytes (default: `1024` KB). Raise this for workflows that produce large generated files. + +```yaml +safe-outputs: + create-pull-request: + max-patch-size: 2048 # allow up to 2 MB patches +``` + 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 default behavior is to fall back (e.g. open an issue when `fallback-as-issue: true`) rather than rename the branch or overwrite the remote ref. To enable reuse of the existing remote branch, set `recreate-ref: true`: the handler will force-delete the stale remote ref and recreate it from the agent's local HEAD (force-push semantics). This is the intended behavior for long-lived reusable branches whose previous PR was merged. `recreate-ref` requires `preserve-branch-name: true` to take effect; the handler does not silently rename the branch in this case. From afc0c0e232070c805cc457a67fe0fbe70199bb01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 04:14:23 +0000 Subject: [PATCH 3/8] feat: surface E003 file-limit error into fallback issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the patch exceeds `max-patch-files` and `fallback-as-issue` is enabled (the default), create a fallback issue that surfaces the E003 error and the actionable `max-patch-files` fix — instead of silently returning a bare failure result that only appears in step logs. When `fallback-as-issue: false`, behaviour is unchanged (return error). Add three integration tests covering: - E003 + fallback_as_issue=true → fallback issue created - E003 + fallback_as_issue=false → error returned, no issue - max_patch_files raised above count → PR created normally Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 28 +++- actions/setup/js/create_pull_request.test.cjs | 133 ++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 37c69fbdd4e..7290f60fe9b 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1086,7 +1086,33 @@ async function main(config = {}) { } catch (error) { const errorMessage = getErrorMessage(error); core.warning(`Pull request limit exceeded: ${errorMessage}`); - return { success: false, error: errorMessage }; + + if (!fallbackAsIssue) { + return { success: false, error: errorMessage }; + } + + // Surface the limit error in a fallback issue so it appears in the agent failure + // issue/comment thread and the workflow operator knows exactly how to fix it. + const fallbackTitle = pullRequestItem.title?.trim() || "Agent Output"; + const fallbackLabels = mergeFallbackIssueLabels(configFallbackLabels.length > 0 ? configFallbackLabels : envLabels); + const fallbackBody = `> [!WARNING]\n> ${errorMessage}\n\nThe pull request could not be created because the patch contains more files than the configured limit.\n\nTo increase the limit, add \`max-patch-files\` to your workflow frontmatter:\n\n\`\`\`yaml\nsafe-outputs:\n create-pull-request:\n max-patch-files: ${maxFiles * 2} # adjust as needed\n\`\`\``; + + try { + const { data: issue } = await createFallbackIssue(githubClient, repoParts, fallbackTitle, fallbackBody, fallbackLabels, configAssignees); + core.info(`Created fallback issue #${issue.number}: ${issue.html_url}`); + await assignCopilotToFallbackIssueIfEnabled(repoParts.owner, repoParts.repo, issue.number); + await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); + return { + success: true, + fallback_used: true, + issue_number: issue.number, + issue_url: issue.html_url, + }; + } catch (issueError) { + const combinedError = `Pull request limit exceeded and failed to create fallback issue. Limit error: ${errorMessage}. Issue error: ${getErrorMessage(issueError)}`; + core.error(combinedError); + return { success: false, error: combinedError }; + } } // Check for actual error conditions (but allow empty patches as valid noop) diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index a87977b0a32..3cf5594f740 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3025,3 +3025,136 @@ describe("create_pull_request - branch-prefix config", () => { expect(branchArg).toMatch(/^bad-prefix/); }); }); + +describe("create_pull_request - E003 file-limit fallback-to-issue", () => { + let originalEnv; + let tempDir; + + beforeEach(() => { + originalEnv = { ...process.env }; + process.env.GH_AW_WORKFLOW_ID = "test-workflow"; + process.env.GITHUB_REPOSITORY = "test-owner/test-repo"; + process.env.GITHUB_BASE_REF = "main"; + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-e003-test-")); + + global.core = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), + startGroup: vi.fn(), + endGroup: vi.fn(), + summary: { addRaw: vi.fn().mockReturnThis(), write: vi.fn().mockResolvedValue(undefined) }, + }; + + global.github = { + rest: { + pulls: { create: vi.fn().mockResolvedValue({ data: { number: 1, html_url: "https://github.com/test/pull/1" } }) }, + repos: { get: vi.fn().mockResolvedValue({ data: { default_branch: "main" } }) }, + issues: { + create: vi.fn().mockResolvedValue({ data: { number: 55, html_url: "https://github.com/test/issues/55" } }), + addLabels: vi.fn().mockResolvedValue({}), + }, + }, + graphql: vi.fn(), + }; + + global.context = { + eventName: "workflow_dispatch", + repo: { owner: "test-owner", repo: "test-repo" }, + payload: {}, + runId: "99999", + }; + + global.exec = { + exec: vi.fn().mockResolvedValue(0), + getExecOutput: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }), + }; + + delete require.cache[require.resolve("./create_pull_request.cjs")]; + }); + + afterEach(() => { + for (const key of Object.keys(process.env)) { + if (!(key in originalEnv)) delete process.env[key]; + } + Object.assign(process.env, originalEnv); + if (tempDir && fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true }); + delete global.core; + delete global.github; + delete global.context; + delete global.exec; + vi.clearAllMocks(); + }); + + /** Build a patch string touching `n` unique files */ + function buildOversizedPatch(n) { + const lines = []; + for (let i = 0; i < n; i++) { + lines.push(`diff --git a/file${i}.txt b/file${i}.txt`); + lines.push("index 1234567..abcdefg 100644"); + lines.push(`--- a/file${i}.txt`); + lines.push(`+++ b/file${i}.txt`); + lines.push("@@ -1,1 +1,1 @@"); + lines.push("-old"); + lines.push("+new"); + } + return lines.join("\n"); + } + + it("should create a fallback issue when E003 fires and fallback_as_issue is true (default)", async () => { + const patchPath = path.join(tempDir, "aw-test.patch"); + fs.writeFileSync(patchPath, buildOversizedPatch(101)); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({}); + const result = await handler({ title: "Data refresh PR", body: "Daily update", branch: "data/refresh", patch_path: patchPath }, {}); + + expect(result.success).toBe(true); + expect(result.fallback_used).toBe(true); + expect(result.issue_number).toBe(55); + + // A fallback issue should have been created + expect(global.github.rest.issues.create).toHaveBeenCalledTimes(1); + const issueCall = global.github.rest.issues.create.mock.calls[0][0]; + + // The body should contain the E003 error message and the actionable fix + expect(issueCall.body).toContain("E003"); + expect(issueCall.body).toContain("max-patch-files"); + + // PR creation should NOT have been attempted + expect(global.github.rest.pulls.create).not.toHaveBeenCalled(); + }); + + it("should return success: false when E003 fires and fallback_as_issue is false", async () => { + const patchPath = path.join(tempDir, "aw-test.patch"); + fs.writeFileSync(patchPath, buildOversizedPatch(101)); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ fallback_as_issue: false }); + const result = await handler({ title: "Data refresh PR", body: "Daily update", branch: "data/refresh", patch_path: patchPath }, {}); + + expect(result.success).toBe(false); + expect(result.error).toContain("E003"); + + // No fallback issue should have been created + expect(global.github.rest.issues.create).not.toHaveBeenCalled(); + }); + + it("should pass when max_patch_files is raised above the file count", async () => { + const patchPath = path.join(tempDir, "aw-test.patch"); + fs.writeFileSync(patchPath, buildOversizedPatch(150)); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ max_patch_files: 200 }); + const result = await handler({ title: "Data refresh PR", body: "Daily update", branch: "data/refresh", patch_path: patchPath }, {}); + + // Should succeed — limit was raised + expect(result.success).toBe(true); + expect(result.fallback_used).toBeUndefined(); + expect(global.github.rest.pulls.create).toHaveBeenCalledTimes(1); + expect(global.github.rest.issues.create).not.toHaveBeenCalled(); + }); +}); From 0dac072b4b1e164e2555aa3eb253a5a698cf958c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 04:15:18 +0000 Subject: [PATCH 4/8] refactor: improve readability of E003 fallback body and comment style Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 15 ++++++++++++++- actions/setup/js/create_pull_request.test.cjs | 2 +- 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 7290f60fe9b..ef6680fa523 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1095,7 +1095,20 @@ async function main(config = {}) { // issue/comment thread and the workflow operator knows exactly how to fix it. const fallbackTitle = pullRequestItem.title?.trim() || "Agent Output"; const fallbackLabels = mergeFallbackIssueLabels(configFallbackLabels.length > 0 ? configFallbackLabels : envLabels); - const fallbackBody = `> [!WARNING]\n> ${errorMessage}\n\nThe pull request could not be created because the patch contains more files than the configured limit.\n\nTo increase the limit, add \`max-patch-files\` to your workflow frontmatter:\n\n\`\`\`yaml\nsafe-outputs:\n create-pull-request:\n max-patch-files: ${maxFiles * 2} # adjust as needed\n\`\`\``; + const fallbackBody = [ + `> [!WARNING]`, + `> ${errorMessage}`, + ``, + `The pull request could not be created because the patch contains more files than the configured limit.`, + ``, + `To increase the limit, add \`max-patch-files\` to your workflow frontmatter:`, + ``, + `\`\`\`yaml`, + `safe-outputs:`, + ` create-pull-request:`, + ` max-patch-files: ${maxFiles * 2} # adjust as needed`, + `\`\`\``, + ].join("\n"); try { const { data: issue } = await createFallbackIssue(githubClient, repoParts, fallbackTitle, fallbackBody, fallbackLabels, configAssignees); diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 3cf5594f740..4a33edeb076 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3089,7 +3089,7 @@ describe("create_pull_request - E003 file-limit fallback-to-issue", () => { vi.clearAllMocks(); }); - /** Build a patch string touching `n` unique files */ + // Build a patch string touching `n` unique files function buildOversizedPatch(n) { const lines = []; for (let i = 0; i < n; i++) { From 7ce1ccf91dd34410636b03b16a611ddd4e5108ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 04:42:02 +0000 Subject: [PATCH 5/8] refactor: move E003 fallback body to md template file Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 19 +++++-------------- actions/setup/js/create_pull_request.test.cjs | 7 +++++++ actions/setup/md/e003_file_limit_fallback.md | 12 ++++++++++++ 3 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 actions/setup/md/e003_file_limit_fallback.md diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index ef6680fa523..929a9a1af93 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1095,20 +1095,11 @@ async function main(config = {}) { // issue/comment thread and the workflow operator knows exactly how to fix it. const fallbackTitle = pullRequestItem.title?.trim() || "Agent Output"; const fallbackLabels = mergeFallbackIssueLabels(configFallbackLabels.length > 0 ? configFallbackLabels : envLabels); - const fallbackBody = [ - `> [!WARNING]`, - `> ${errorMessage}`, - ``, - `The pull request could not be created because the patch contains more files than the configured limit.`, - ``, - `To increase the limit, add \`max-patch-files\` to your workflow frontmatter:`, - ``, - `\`\`\`yaml`, - `safe-outputs:`, - ` create-pull-request:`, - ` max-patch-files: ${maxFiles * 2} # adjust as needed`, - `\`\`\``, - ].join("\n"); + const fallbackTemplatePath = getPromptPath("e003_file_limit_fallback.md"); + const fallbackBody = renderTemplateFromFile(fallbackTemplatePath, { + error_message: errorMessage, + suggested_limit: maxFiles * 2, + }); try { const { data: issue } = await createFallbackIssue(githubClient, repoParts, fallbackTitle, fallbackBody, fallbackLabels, configAssignees); diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 4a33edeb076..4251d71553e 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3037,6 +3037,13 @@ describe("create_pull_request - E003 file-limit fallback-to-issue", () => { process.env.GITHUB_BASE_REF = "main"; tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-e003-test-")); + // Set up prompts directory with the E003 template so getPromptPath resolves + const promptsDir = path.join(tempDir, "prompts"); + fs.mkdirSync(promptsDir, { recursive: true }); + const templateSrc = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../md/e003_file_limit_fallback.md"); + fs.copyFileSync(templateSrc, path.join(promptsDir, "e003_file_limit_fallback.md")); + process.env.GH_AW_PROMPTS_DIR = promptsDir; + global.core = { info: vi.fn(), warning: vi.fn(), diff --git a/actions/setup/md/e003_file_limit_fallback.md b/actions/setup/md/e003_file_limit_fallback.md new file mode 100644 index 00000000000..2779fea8e9d --- /dev/null +++ b/actions/setup/md/e003_file_limit_fallback.md @@ -0,0 +1,12 @@ +> [!WARNING] +> {error_message} + +The pull request could not be created because the patch contains more files than the configured limit. + +To increase the limit, add `max-patch-files` to your workflow frontmatter: + +```yaml +safe-outputs: + create-pull-request: + max-patch-files: {suggested_limit} # adjust as needed +``` From d030cc6dead3d092fe308d8b545b13dd3906254e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 04:42:40 +0000 Subject: [PATCH 6/8] fix: use fileURLToPath for cross-platform path resolution in E003 test Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.test.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 4251d71553e..d4710b3fdbe 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -1,6 +1,7 @@ // @ts-check import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { createRequire } from "module"; +import { fileURLToPath } from "url"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; @@ -3040,7 +3041,7 @@ describe("create_pull_request - E003 file-limit fallback-to-issue", () => { // Set up prompts directory with the E003 template so getPromptPath resolves const promptsDir = path.join(tempDir, "prompts"); fs.mkdirSync(promptsDir, { recursive: true }); - const templateSrc = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../md/e003_file_limit_fallback.md"); + const templateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/e003_file_limit_fallback.md"); fs.copyFileSync(templateSrc, path.join(promptsDir, "e003_file_limit_fallback.md")); process.env.GH_AW_PROMPTS_DIR = promptsDir; From 36bc63e9bb21a3dd5ddff5f043cdba2ccb628d79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 05:14:41 +0000 Subject: [PATCH 7/8] fix: staged mode, exact file count, and title sanitization in E003 fallback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 18 ++++++- actions/setup/js/create_pull_request.test.cjs | 52 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 929a9a1af93..ba37eb15846 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1087,18 +1087,32 @@ async function main(config = {}) { const errorMessage = getErrorMessage(error); core.warning(`Pull request limit exceeded: ${errorMessage}`); + // In staged mode, show a preview instead of performing API side effects + if (isStaged) { + let summaryContent = "## 🎭 Staged Mode: Create Pull Request Preview\n\n"; + summaryContent += "The following pull request would be created if staged mode was disabled:\n\n"; + summaryContent += `**Status:** ⚠️ Patch file limit exceeded\n\n`; + summaryContent += `**Message:** ${errorMessage}\n\n`; + + await core.summary.addRaw(summaryContent).write(); + core.info("📝 Pull request creation preview written to step summary (file limit exceeded)"); + return { success: true, staged: true }; + } + if (!fallbackAsIssue) { return { success: false, error: errorMessage }; } // Surface the limit error in a fallback issue so it appears in the agent failure // issue/comment thread and the workflow operator knows exactly how to fix it. - const fallbackTitle = pullRequestItem.title?.trim() || "Agent Output"; + const rawFallbackTitle = pullRequestItem.title?.trim() || "Agent Output"; + const fallbackTitle = applyTitlePrefix(sanitizeTitle(rawFallbackTitle, titlePrefix), titlePrefix); const fallbackLabels = mergeFallbackIssueLabels(configFallbackLabels.length > 0 ? configFallbackLabels : envLabels); + const receivedFileCount = countUniquePatchFiles(patchContent); const fallbackTemplatePath = getPromptPath("e003_file_limit_fallback.md"); const fallbackBody = renderTemplateFromFile(fallbackTemplatePath, { error_message: errorMessage, - suggested_limit: maxFiles * 2, + suggested_limit: receivedFileCount, }); try { diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index d4710b3fdbe..262de1ca25a 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3132,10 +3132,62 @@ describe("create_pull_request - E003 file-limit fallback-to-issue", () => { expect(issueCall.body).toContain("E003"); expect(issueCall.body).toContain("max-patch-files"); + // The suggested limit must be >= the actual file count (101), not maxFiles * 2 (200) + expect(issueCall.body).toContain("max-patch-files: 101"); + // PR creation should NOT have been attempted expect(global.github.rest.pulls.create).not.toHaveBeenCalled(); }); + it("should use the actual received file count (not maxFiles*2) as the suggested limit", async () => { + const patchPath = path.join(tempDir, "aw-test.patch"); + fs.writeFileSync(patchPath, buildOversizedPatch(220)); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({}); + const result = await handler({ title: "API regen PR", body: "Daily update", branch: "api/regen", patch_path: patchPath }, {}); + + expect(result.success).toBe(true); + expect(result.fallback_used).toBe(true); + + const issueCall = global.github.rest.issues.create.mock.calls[0][0]; + // With default limit=100 and 220 files, old code would suggest 200; correct is 220 + expect(issueCall.body).toContain("max-patch-files: 220"); + }); + + it("should sanitize and apply title prefix to fallback issue title", async () => { + const patchPath = path.join(tempDir, "aw-test.patch"); + fs.writeFileSync(patchPath, buildOversizedPatch(101)); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ title_prefix: "[bot]" }); + const result = await handler({ title: "Data refresh PR", body: "Daily update", branch: "data/refresh", patch_path: patchPath }, {}); + + expect(result.success).toBe(true); + const issueCall = global.github.rest.issues.create.mock.calls[0][0]; + // Title prefix should be applied + expect(issueCall.title).toMatch(/^\[bot\]/); + }); + + it("should return staged preview instead of creating a fallback issue when in staged mode", async () => { + process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; + + const patchPath = path.join(tempDir, "aw-test.patch"); + fs.writeFileSync(patchPath, buildOversizedPatch(101)); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({}); + const result = await handler({ title: "Data refresh PR", body: "Daily update", branch: "data/refresh", patch_path: patchPath }, {}); + + // Staged mode: no API side effects, just a preview + expect(result.success).toBe(true); + expect(result.staged).toBe(true); + expect(result.fallback_used).toBeUndefined(); + expect(global.github.rest.issues.create).not.toHaveBeenCalled(); + expect(global.github.rest.pulls.create).not.toHaveBeenCalled(); + expect(global.core.summary.addRaw).toHaveBeenCalled(); + }); + it("should return success: false when E003 fires and fallback_as_issue is false", async () => { const patchPath = path.join(tempDir, "aw-test.patch"); fs.writeFileSync(patchPath, buildOversizedPatch(101)); From 1cc6a63e61007e34fd288d119d66d8f770f07ac4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 05:16:04 +0000 Subject: [PATCH 8/8] fix: avoid double file-count parse and add regression assertions Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 7 ++++--- actions/setup/js/create_pull_request.test.cjs | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index ba37eb15846..37a41428e9a 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -1080,7 +1080,9 @@ async function main(config = {}) { isEmpty = !patchContent || !patchContent.trim(); } - // Enforce max limits on patch before processing + // Enforce max limits on patch before processing. + // Count files once here so the catch block can reuse the value without re-parsing. + const patchFileCount = countUniquePatchFiles(patchContent); try { enforcePullRequestLimits(patchContent, maxFiles); } catch (error) { @@ -1108,11 +1110,10 @@ async function main(config = {}) { const rawFallbackTitle = pullRequestItem.title?.trim() || "Agent Output"; const fallbackTitle = applyTitlePrefix(sanitizeTitle(rawFallbackTitle, titlePrefix), titlePrefix); const fallbackLabels = mergeFallbackIssueLabels(configFallbackLabels.length > 0 ? configFallbackLabels : envLabels); - const receivedFileCount = countUniquePatchFiles(patchContent); const fallbackTemplatePath = getPromptPath("e003_file_limit_fallback.md"); const fallbackBody = renderTemplateFromFile(fallbackTemplatePath, { error_message: errorMessage, - suggested_limit: receivedFileCount, + suggested_limit: patchFileCount, }); try { diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 262de1ca25a..7fccfac3e74 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -3134,6 +3134,7 @@ describe("create_pull_request - E003 file-limit fallback-to-issue", () => { // The suggested limit must be >= the actual file count (101), not maxFiles * 2 (200) expect(issueCall.body).toContain("max-patch-files: 101"); + expect(issueCall.body).not.toContain("max-patch-files: 200"); // PR creation should NOT have been attempted expect(global.github.rest.pulls.create).not.toHaveBeenCalled(); @@ -3153,6 +3154,7 @@ describe("create_pull_request - E003 file-limit fallback-to-issue", () => { const issueCall = global.github.rest.issues.create.mock.calls[0][0]; // With default limit=100 and 220 files, old code would suggest 200; correct is 220 expect(issueCall.body).toContain("max-patch-files: 220"); + expect(issueCall.body).not.toContain("max-patch-files: 200"); }); it("should sanitize and apply title prefix to fallback issue title", async () => {