From b0abd7c043fd267aa6e55050ac12faef6d45024c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:31:03 +0000 Subject: [PATCH 1/4] Initial plan From 32d462e832588353830b03f0b8b45cd2fc4397e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:43:57 +0000 Subject: [PATCH 2/4] Plan repo-memory precheck diff-size fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/skills/agentic-workflows/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index 615a51e551e..3fc711d4035 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -49,6 +49,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/optimize-agentic-workflow.md` - `.github/aw/patterns.md` - `.github/aw/pr-reviewer.md` +- `.github/aw/release-workflow.md` - `.github/aw/report.md` - `.github/aw/reuse.md` - `.github/aw/safe-outputs-automation.md` From 0c2184541017a4137b79202e903333a6f4fe5087 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:49:59 +0000 Subject: [PATCH 3/4] Use staged diff size for repo-memory max-patch-size pre-check Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/push_repo_memory.cjs | 9 +--- actions/setup/js/repo_memory_patch_size.cjs | 34 +++++++++++++++ actions/setup/js/safe_outputs_handlers.cjs | 42 +++++++++++++------ .../setup/js/safe_outputs_handlers.test.cjs | 35 ++++++++++------ 4 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 actions/setup/js/repo_memory_patch_size.cjs diff --git a/actions/setup/js/push_repo_memory.cjs b/actions/setup/js/push_repo_memory.cjs index 44349024b9b..45ca12758fd 100644 --- a/actions/setup/js/push_repo_memory.cjs +++ b/actions/setup/js/push_repo_memory.cjs @@ -7,6 +7,7 @@ const path = require("path"); const { getErrorMessage } = require("./error_helpers.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); const { execGitSync, getGitAuthEnv } = require("./git_helpers.cjs"); +const { getStagedPatchAdditionsSizeBytes } = require("./repo_memory_patch_size.cjs"); const { parseAllowedRepos, validateRepo } = require("./repo_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); @@ -544,13 +545,7 @@ async function main() { // Deletions are ignored since removing content is acceptable and does not // contribute to the size of the content being pushed. try { - const patchContent = execGitSync(["diff", "--cached"], { stdio: "pipe" }); - // Count only added lines (starting with '+', excluding '+++' file-header lines) - const addedSizeBytes = patchContent - .split("\n") - .filter(line => line.startsWith("+") && !line.startsWith("+++")) - .reduce((sum, line) => sum + Buffer.byteLength(line + "\n", "utf8"), 0); - const patchSizeBytes = addedSizeBytes; + const patchSizeBytes = getStagedPatchAdditionsSizeBytes({ execGitSyncFn: execGitSync }); const patchSizeKb = Math.ceil(patchSizeBytes / 1024); const maxPatchSizeKb = Math.floor(maxPatchSize / 1024); // Allow 20% overhead to account for git diff format (headers, context lines, etc.) diff --git a/actions/setup/js/repo_memory_patch_size.cjs b/actions/setup/js/repo_memory_patch_size.cjs new file mode 100644 index 00000000000..aa1959efd89 --- /dev/null +++ b/actions/setup/js/repo_memory_patch_size.cjs @@ -0,0 +1,34 @@ +// @ts-check +/// + +/** + * Count total UTF-8 bytes for added lines in a unified diff. + * Added lines start with "+" and exclude file header lines ("+++"). + * + * @param {string} patchContent + * @returns {number} + */ +function getAddedPatchSizeBytesFromDiff(patchContent) { + return patchContent + .split("\n") + .filter(line => line.startsWith("+") && !line.startsWith("+++")) + .reduce((sum, line) => sum + Buffer.byteLength(line + "\n", "utf8"), 0); +} + +/** + * Compute staged patch additions size from git diff --cached. + * + * @param {Object} options + * @param {(args: string[], opts?: Record) => string} options.execGitSyncFn + * @param {string} [options.cwd] + * @returns {number} + */ +function getStagedPatchAdditionsSizeBytes({ execGitSyncFn, cwd }) { + const patchContent = execGitSyncFn(["diff", "--cached"], { stdio: "pipe", cwd }); + return getAddedPatchSizeBytesFromDiff(patchContent); +} + +module.exports = { + getAddedPatchSizeBytesFromDiff, + getStagedPatchAdditionsSizeBytes, +}; diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index b9a4fc8b84b..0af94ff315e 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -21,6 +21,7 @@ const { findRepoCheckout } = require("./find_repo_checkout.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { getOrGenerateTemporaryId } = require("./temporary_id.cjs"); const { parseAllowedExtensionsEnv } = require("./allowed_extensions_helpers.cjs"); +const { getStagedPatchAdditionsSizeBytes } = require("./repo_memory_patch_size.cjs"); const { sanitizeTitle, applyTitlePrefix } = require("./sanitize_title.cjs"); const { parseDeduplicateByTitle, normalizeTitleForDedup, findDuplicateByTitle } = require("./issue_title_dedup.cjs"); const { validateCreatePullRequestIntent, validatePushToPullRequestBranchIntent, validateCreateIssueIntent, validateAddCommentIntent } = require("./intent_probe.cjs"); @@ -1645,17 +1646,35 @@ function createHandlers(server, appendSafeOutput, config = {}) { }; } - // Check total content size. totalSize is the raw sum of all file sizes in the memory - // directory (excluding .git). It is compared against max_patch_size × 1.2 so that - // a full rewrite of all memory content would remain within the push gate limit in - // push_repo_memory.cjs, which applies the same 1.2× factor to diff additions. const totalSize = files.reduce((sum, f) => sum + f.size, 0); const totalSizeKb = Math.ceil(totalSize / 1024); const effectiveMaxKb = Math.floor(effectiveMaxPatchSize / 1024); + const maxPatchSizeKb = Math.floor(maxPatchSize / 1024); - core.debug(`push_repo_memory validation: ${files.length} files, total ${totalSize} bytes, effective limit ${effectiveMaxPatchSize} bytes`); + let patchSizeBytes; + try { + ensureSafeDirectoryTrust(memoryDir, server); + execGitSync(["add", "--sparse", "."], { cwd: memoryDir, stdio: "pipe" }); + patchSizeBytes = getStagedPatchAdditionsSizeBytes({ execGitSyncFn: execGitSync, cwd: memoryDir }); + } catch (/** @type {any} */ error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + result: "error", + error: `Failed to compute staged patch additions size for '${memoryDir}': ${getErrorMessage(error)}`, + }), + }, + ], + isError: true, + }; + } + const patchSizeKb = Math.ceil(patchSizeBytes / 1024); + + core.debug(`push_repo_memory validation: ${files.length} files, total ${totalSize} bytes, patch additions ${patchSizeBytes} bytes, effective limit ${effectiveMaxPatchSize} bytes`); - if (totalSize > effectiveMaxPatchSize) { + if (patchSizeBytes > effectiveMaxPatchSize) { return { content: [ { @@ -1663,11 +1682,10 @@ function createHandlers(server, appendSafeOutput, config = {}) { text: JSON.stringify({ result: "error", error: - `Total memory size (${totalSizeKb} KB) exceeds the allowed limit of ${effectiveMaxKb} KB ` + - `(configured max-patch-size: ${Math.floor(maxPatchSize / 1024)} KB).\n\n` + - `Please reduce the total size of files in '${memoryDir}' before the workflow completes. ` + - `Consider: summarizing notes instead of keeping full history, removing outdated entries, or compressing data. ` + - `Then call push_repo_memory again to verify the size is within limits.`, + `Patch additions size (${patchSizeKb} KB, ${patchSizeBytes} bytes) exceeds the allowed limit of ${effectiveMaxKb} KB ` + + `(${effectiveMaxPatchSize} bytes, configured max-patch-size: ${maxPatchSizeKb} KB / ${maxPatchSize} bytes with 20% overhead).\n\n` + + `Please reduce the size of staged changes in '${memoryDir}' before the workflow completes. ` + + `Then call push_repo_memory again to verify the patch size is within limits.`, }), }, ], @@ -1681,7 +1699,7 @@ function createHandlers(server, appendSafeOutput, config = {}) { type: "text", text: JSON.stringify({ result: "success", - message: `Memory validation passed: ${files.length} file(s), ${totalSizeKb} KB total (limit: ${effectiveMaxKb} KB).`, + message: `Memory validation passed: ${files.length} file(s), ${totalSizeKb} KB total content, ` + `${patchSizeKb} KB patch additions (${patchSizeBytes} bytes) (limit: ${effectiveMaxKb} KB / ${effectiveMaxPatchSize} bytes).`, }), }, ], diff --git a/actions/setup/js/safe_outputs_handlers.test.cjs b/actions/setup/js/safe_outputs_handlers.test.cjs index d26430e65ae..313a1757b96 100644 --- a/actions/setup/js/safe_outputs_handlers.test.cjs +++ b/actions/setup/js/safe_outputs_handlers.test.cjs @@ -2512,6 +2512,12 @@ describe("safe_outputs_handlers", () => { }); } + function initGitRepo(repoDir) { + execSync("git init", { cwd: repoDir, stdio: "pipe" }); + execSync('git config user.email "test@example.com"', { cwd: repoDir, stdio: "pipe" }); + execSync('git config user.name "Test User"', { cwd: repoDir, stdio: "pipe" }); + } + it("should return success when no repo-memory is configured", () => { const h = createHandlers(mockServer, mockAppendSafeOutput, {}); const result = h.pushRepoMemoryHandler({}); @@ -2543,6 +2549,7 @@ describe("safe_outputs_handlers", () => { it("should return success for valid files within limits", () => { const h = makeHandlersWithMemory(); fs.mkdirSync(memoryDir, { recursive: true }); + initGitRepo(memoryDir); fs.writeFileSync(path.join(memoryDir, "state.json"), "x".repeat(100)); const result = h.pushRepoMemoryHandler({ memory_id: "default" }); const data = JSON.parse(result.content[0].text); @@ -2576,42 +2583,43 @@ describe("safe_outputs_handlers", () => { expect(data.error).toContain("3 files"); }); - it("should return error when total size exceeds effective max_patch_size", () => { - // max_patch_size = 500 bytes, effective limit = floor(500 * 1.2) = 600 bytes - const h = makeHandlersWithMemory({ max_patch_size: 500, max_file_size: 1024 * 1024 }); + it("should pass when total folder size is large but staged diff is tiny", () => { + const h = makeHandlersWithMemory({ max_patch_size: 50, max_file_size: 1024 * 1024 }); fs.mkdirSync(memoryDir, { recursive: true }); - // Write two files totaling 650 bytes (above the 600 byte effective limit) - fs.writeFileSync(path.join(memoryDir, "a.json"), "x".repeat(350)); - fs.writeFileSync(path.join(memoryDir, "b.json"), "x".repeat(300)); + initGitRepo(memoryDir); + fs.writeFileSync(path.join(memoryDir, "large.json"), `${"x\n".repeat(3000)}`); + execSync("git add . && git commit -m 'seed'", { cwd: memoryDir, stdio: "pipe" }); + fs.appendFileSync(path.join(memoryDir, "large.json"), "small-diff\n"); const result = h.pushRepoMemoryHandler({ memory_id: "default" }); - expect(result.isError).toBe(true); + expect(result.isError).toBeUndefined(); const data = JSON.parse(result.content[0].text); - expect(data.result).toBe("error"); - expect(data.error).toContain("exceeds the allowed limit"); - expect(data.error).toContain("push_repo_memory again"); + expect(data.result).toBe("success"); + expect(data.message).toContain("patch additions"); }); it("should use 'default' memory_id when memory_id is not specified", () => { const h = makeHandlersWithMemory(); fs.mkdirSync(memoryDir, { recursive: true }); + initGitRepo(memoryDir); fs.writeFileSync(path.join(memoryDir, "notes.md"), "hello"); const result = h.pushRepoMemoryHandler({}); // no memory_id const data = JSON.parse(result.content[0].text); expect(data.result).toBe("success"); }); - it("should scan files recursively in subdirectories", () => { + it("should fail when staged patch additions exceed effective max_patch_size", () => { // max_patch_size = 500 bytes, effective limit = 600 bytes const h = makeHandlersWithMemory({ max_patch_size: 500, max_file_size: 1024 * 1024 }); + fs.mkdirSync(memoryDir, { recursive: true }); + initGitRepo(memoryDir); const subDir = path.join(memoryDir, "history"); fs.mkdirSync(subDir, { recursive: true }); - // Write a nested file that pushes total above effective limit fs.writeFileSync(path.join(subDir, "log.jsonl"), "x".repeat(700)); const result = h.pushRepoMemoryHandler({ memory_id: "default" }); expect(result.isError).toBe(true); const data = JSON.parse(result.content[0].text); expect(data.result).toBe("error"); - // The nested file path should appear correctly + expect(data.error).toContain("Patch additions size"); expect(data.error).toContain("exceeds the allowed limit"); }); @@ -2622,6 +2630,7 @@ describe("safe_outputs_handlers", () => { // files are small but .git directory content is large — must not count toward limit. const h = makeHandlersWithMemory({ max_patch_size: 500, max_file_size: 1024 * 1024 }); fs.mkdirSync(memoryDir, { recursive: true }); + initGitRepo(memoryDir); // Small memory files (well within limit) fs.writeFileSync(path.join(memoryDir, "memory.json"), "x".repeat(100)); fs.writeFileSync(path.join(memoryDir, "state.json"), "x".repeat(100)); From 28209531474542e1618b1ef42a3accac89cf9942 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Jul 2026 05:55:16 +0000 Subject: [PATCH 4/4] fix: use hunk-state tracking in getAddedPatchSizeBytesFromDiff and add repo_memory_patch_size.cjs to SAFE_OUTPUTS_FILES - Replace filter-based +++ exclusion with a state-machine that tracks whether parsing is inside a hunk. File headers (+++ before the first @@ of each file) are correctly ignored; content lines that begin with ++ (appearing as +++ inside a hunk) are now correctly counted. - Add repo_memory_patch_size.cjs to SAFE_OUTPUTS_FILES in setup.sh so the transitive-dependency test passes (safe_outputs_handlers.cjs requires it at runtime). Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/repo_memory_patch_size.cjs | 19 +++++++++++++++---- actions/setup/setup.sh | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/repo_memory_patch_size.cjs b/actions/setup/js/repo_memory_patch_size.cjs index aa1959efd89..b2de1919f80 100644 --- a/actions/setup/js/repo_memory_patch_size.cjs +++ b/actions/setup/js/repo_memory_patch_size.cjs @@ -4,15 +4,26 @@ /** * Count total UTF-8 bytes for added lines in a unified diff. * Added lines start with "+" and exclude file header lines ("+++"). + * Uses hunk-state tracking so content lines that begin with "++" are + * counted correctly (they appear as "+++" inside a hunk but are not headers). * * @param {string} patchContent * @returns {number} */ function getAddedPatchSizeBytesFromDiff(patchContent) { - return patchContent - .split("\n") - .filter(line => line.startsWith("+") && !line.startsWith("+++")) - .reduce((sum, line) => sum + Buffer.byteLength(line + "\n", "utf8"), 0); + let inHunk = false; + let total = 0; + for (const line of patchContent.split("\n")) { + if (line.startsWith("@@")) { + inHunk = true; + } else if (line.startsWith("diff ")) { + inHunk = false; + } + if (inHunk && line.startsWith("+")) { + total += Buffer.byteLength(line + "\n", "utf8"); + } + } + return total; } /** diff --git a/actions/setup/setup.sh b/actions/setup/setup.sh index 92968eb518e..e9addfff9bb 100755 --- a/actions/setup/setup.sh +++ b/actions/setup/setup.sh @@ -364,6 +364,7 @@ SAFE_OUTPUTS_FILES=( "markdown_code_region_balancer.cjs" "temporary_id.cjs" "invocation_context_helpers.cjs" + "repo_memory_patch_size.cjs" ) SAFE_OUTPUTS_COUNT=0