Skip to content

Fix push_to_pull_request_branch patch size to use incremental net diff#28198

Merged
pelikhan merged 4 commits intomainfrom
copilot/fix-patch-size-calculation
Apr 24, 2026
Merged

Fix push_to_pull_request_branch patch size to use incremental net diff#28198
pelikhan merged 4 commits intomainfrom
copilot/fix-patch-size-calculation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

On long-running branches (e.g. autoloop iteration branches), push_to_pull_request_branch's max_patch_size check measured the cumulative diff from the default branch instead of the incremental change against the existing PR branch head. As accepted iterations accumulated, every push eventually exceeded the limit even when each iteration only changed a few KB.

Changes

  • git_patch_utils.cjs (new module): extracts reusable git patch helpers out of generate_git_patch.cjs:
    • sanitizeForFilename, sanitizeBranchNameForPatch, sanitizeRepoSlugForPatch, getPatchPath, getPatchPathForRepo
    • buildExcludePathspecs — builds ["--", ":(exclude)<pat>", ...] from an excludedFiles list.
    • computeIncrementalDiffSize — measures the net diff between two refs in a git repo using git diff --binary --output=<tmpfile> + fs.statSync (O(1) memory, with finally cleanup of the temp file). Returns null on missing args or git failure.
  • generate_git_patch.cjs (incremental mode):
    • Delegates to git_patch_utils.cjs for filename sanitization, path construction, pathspec building, and incremental diff-size measurement. Re-exports the helpers for backward compatibility with existing callers.
    • Returns diffSize = the net git diff <baseRef>..<branch> size alongside the existing patchSize. The format-patch file accumulates per-commit metadata + per-commit diffs and is not a faithful proxy for the net change.
    • Refuses to fall through to Strategies 2/3 (GITHUB_SHA..HEAD, merge-base with default branch) when Strategy 1 fails to write a patch — those paths produce checkout-base diffs and silently violate the incremental contract. Error message includes commitCount and the baseRef..branch range for diagnosis.
  • safe_outputs_handlers.cjs: forward diffSize onto the safe-output entry as diff_size.
  • push_to_pull_request_branch.cjs:
    • Prefer message.diff_size for the max_patch_size check; fall back to the transport file size when absent (backward compatible with older MCP outputs).
    • Enforce max_patch_size on bundle transport as well, using the on-disk bundle file size (or message.diff_size when provided). Bundles no longer silently bypass the limit.
    • Rejection error message distinguishes "Incremental diff size" vs "Patch file size" vs "Bundle size" and includes both the incremental and transport values when diff_size is authoritative.
  • actions/setup/setup.sh: register the new git_patch_utils.cjs in SAFE_OUTPUTS_FILES so it is copied to the runtime actions directory.
// push_to_pull_request_branch.cjs — size check (simplified)
const haveDiffSize = typeof message.diff_size === "number" && message.diff_size >= 0;
let sizeForCheckBytes;
if (haveDiffSize)        sizeForCheckBytes = message.diff_size;
else if (hasBundleFile)  sizeForCheckBytes = fs.statSync(bundleFilePath).size;
else                     sizeForCheckBytes = patchSizeBytes;
const sizeForCheckKb = Math.ceil(sizeForCheckBytes / 1024);
if (sizeForCheckKb > maxSizeKb) {
  return { success: false, error: /* tailored message including both incremental and transport sizes */ };
}

Tests

  • 18 new tests in git_patch_utils.test.cjs — covers the pure helpers and exercises computeIncrementalDiffSize against a real local git repository (temp dir + git init + real commits + real git diff --output): size matches independently-computed git diff bytes, temp file is always cleaned up, zero size for identical refs, excludedFiles pathspecs actually shrink the measured diff, null for invalid refs, null for missing args.
  • 5 new unit tests in push_to_pull_request_branch.test.cjs: prefers diff_size, rejects when diff_size exceeds, falls back to transport file size when absent, enforces limit on bundle file size, and prefers diff_size over bundle file size.
  • 1 new integration test in git_patch_integration.test.cjs reproducing the long-running branch scenario (~50 KB accumulated on origin/<branch>, tiny new commit on top) and asserting diffSize reflects only the new iteration.

Copilot AI changed the title [WIP] Fix patch size calculation in push_to_pull_request_branch Fix push_to_pull_request_branch patch size to use incremental net diff Apr 24, 2026
Copilot finished work on behalf of mrjf April 24, 2026 04:51
Copilot AI requested a review from mrjf April 24, 2026 04:51
@pelikhan pelikhan marked this pull request as ready for review April 24, 2026 04:52
Copilot AI review requested due to automatic review settings April 24, 2026 04:52
@github-actions github-actions Bot mentioned this pull request Apr 24, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes push_to_pull_request_branch max_patch_size enforcement for long-running PR branches by checking the incremental net diff against the current PR branch head, instead of relying on cumulative/transport patch size.

Changes:

  • generate_git_patch.cjs: computes and returns diffSize (net git diff size) in incremental mode and blocks falling back to non-incremental strategies.
  • safe_outputs_handlers.cjs: forwards diffSize as diff_size in the safe-output entry.
  • push_to_pull_request_branch.cjs + tests: prefer message.diff_size for size validation, with fallback behavior covered by new unit/integration tests.
Show a summary per file
File Description
actions/setup/js/generate_git_patch.cjs Adds incremental net diff sizing (diffSize) and prevents non-incremental fallback strategies.
actions/setup/js/safe_outputs_handlers.cjs Persists diffSize into safe outputs as diff_size.
actions/setup/js/push_to_pull_request_branch.cjs Uses diff_size (when present) for max_patch_size checks; logs both file size and checked size.
actions/setup/js/push_to_pull_request_branch.test.cjs Adds unit tests for preferring diff_size and fallback behavior.
actions/setup/js/git_patch_integration.test.cjs Adds integration test reproducing long-running branch scenario and asserting diffSize is small.
.changeset/patch-push-pr-incremental-size-check.md Documents the behavior change in a changeset entry.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 4

Comment thread actions/setup/js/generate_git_patch.cjs Outdated
debugLog(`Strategy 1 (incremental): No patch generated from ${baseRef}..${branchName}, refusing to fall through to checkout-base strategies`);
return {
success: false,
error: `Cannot generate incremental patch: no incremental commits found between ${baseRef} and ${branchName}.`,
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In incremental mode, this error path triggers when commitCount > 0 but git format-patch produced empty output (since commitCount === 0 returns earlier). The message "no incremental commits found" is misleading in that case and will send users down the wrong debugging path. Consider wording that reflects the actual failure (e.g., format-patch produced no output / could not generate patch content) and include commitCount/range in the error to aid diagnosis.

Suggested change
error: `Cannot generate incremental patch: no incremental commits found between ${baseRef} and ${branchName}.`,
error: `Cannot generate incremental patch: git format-patch produced no output for ${baseRef}..${branchName} despite ${commitCount} incremental commit(s).`,

Copilot uses AI. Check for mistakes.
Comment thread actions/setup/js/generate_git_patch.cjs Outdated
Comment on lines +470 to +486
// In incremental mode, also compute the net diff size between baseRef and the
// branch tip. The format-patch file size (patchSize) is the sum of every
// commit's individual diff plus per-commit metadata headers, which can be
// significantly larger than the actual net change. Consumers (e.g.
// push_to_pull_request_branch) should validate `max_patch_size` against the
// incremental net diff so the limit reflects how much the branch will
// actually change, not the cumulative size of the commit history. See:
// https://github.com/github/gh-aw/issues for the long-running branch case.
let diffSize = null;
if (mode === "incremental" && baseCommitSha && branchName) {
try {
const diffOutput = execGitSync(["diff", "--binary", `${baseCommitSha}..${branchName}`, ...excludeArgs()], { cwd });
diffSize = Buffer.byteLength(diffOutput, "utf8");
debugLog(`Final: Computed incremental net diffSize=${diffSize} bytes (baseRef=${baseCommitSha}..${branchName})`);
} catch (diffErr) {
debugLog(`Final: Failed to compute incremental net diffSize - ${getErrorMessage(diffErr)} (will fall back to patchSize)`);
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execGitSync(["diff", "--binary", ...]) captures the entire diff output into memory as a UTF-8 string just to measure its byte length. For larger incremental diffs this can be slow and memory-heavy (and may hit the 100MB maxBuffer, making diffSize null and falling back). Consider measuring diff size without buffering the full output (e.g., write diff to a temp file via git diff --output <file> and stat it, or stream to a byte counter) so size measurement is O(1) memory.

Copilot uses AI. Check for mistakes.
core.info(`${sizeLabel}: ${sizeForCheckKb} KB (maximum allowed: ${maxSizeKb} KB)`);

if (sizeForCheckKb > maxSizeKb) {
const msg = `Patch size (${sizeForCheckKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`;
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When message.diff_size is used, the rejection error still says "Patch size (...)". This can be confusing because the value being enforced is the incremental net diff, not the transport file size. Consider tailoring the error message to indicate whether the limit was exceeded by the incremental diff size vs. the patch file size (and possibly include both values, since you already log them).

Suggested change
const msg = `Patch size (${sizeForCheckKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`;
const msg = haveDiffSize
? `Incremental diff size (${sizeForCheckKb} KB) exceeds maximum allowed size (${maxSizeKb} KB). Patch file size: ${patchSizeKb} KB.`
: `Patch file size (${sizeForCheckKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`;

Copilot uses AI. Check for mistakes.
Comment on lines +178 to +199
@@ -179,10 +179,26 @@ async function main(config = {}) {
const patchSizeBytes = Buffer.byteLength(patchContent, "utf8");
const patchSizeKb = Math.ceil(patchSizeBytes / 1024);

core.info(`Patch size: ${patchSizeKb} KB (maximum allowed: ${maxSizeKb} KB)`);

if (patchSizeKb > maxSizeKb) {
const msg = `Patch size (${patchSizeKb} KB) exceeds maximum allowed size (${maxSizeKb} KB)`;
// Prefer the incremental net diff size (computed by the MCP server when
// the patch was generated in incremental mode) over the format-patch file
// size for `max_patch_size` validation. The format-patch file accumulates
// per-commit metadata and per-commit diffs, which can be much larger than
// the actual net change relative to the existing PR branch head — and on
// a long-running branch (e.g. autoloop iteration branches) this drift
// grows monotonically even when each iteration only changes a few KB.
// The diff size, in contrast, is the size of `git diff origin/<branch>..HEAD`
// and is what the user actually expects `max-patch-size` to cap.
const diffSizeBytesRaw = message.diff_size;
const haveDiffSize = typeof diffSizeBytesRaw === "number" && diffSizeBytesRaw >= 0;
const sizeForCheckBytes = haveDiffSize ? diffSizeBytesRaw : patchSizeBytes;
const sizeForCheckKb = Math.ceil(sizeForCheckBytes / 1024);
const sizeLabel = haveDiffSize ? "Incremental patch size" : "Patch size";

core.info(`Patch file size: ${patchSizeKb} KB`);
core.info(`${sizeLabel}: ${sizeForCheckKb} KB (maximum allowed: ${maxSizeKb} KB)`);

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max_patch_size validation still only runs for patch transport (!hasBundleFile). If bundle transport is configured, large bundles can bypass the size limit entirely. If max_patch_size is intended to cap push size regardless of transport (as implied by the PR description mentioning bundle backward-compat), consider enforcing the limit against bundle_path file size (or explicitly document that the limit is patch-transport-only).

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 73/100

⚠️ Acceptable — with minor suggestions

Metric Value
New/modified tests analyzed 4
✅ Design tests (behavioral contracts) 4 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 3 (75%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes — push_to_pull_request_branch.test.cjs (+50 lines) vs production push_to_pull_request_branch.cjs (+20 lines) = 2.5:1 ratio
🚨 Coding-guideline violations None

Test Classification Details

View All Test Classifications
Test File Classification Notes
should report diffSize as the net diff between origin/branch and HEAD in incremental mode git_patch_integration.test.cjs ✅ Design Integration test using real git; verifies the core bug fix (diffSize < 1 KB for tiny change on long-running branch)
should prefer message.diff_size (incremental net diff) over patch file size push_to_pull_request_branch.test.cjs ✅ Design Happy path — verifies diff_size takes precedence over file size when provided
should reject when message.diff_size exceeds max size even if file size is small push_to_pull_request_branch.test.cjs ✅ Design Error path — verifies diff_size is the authoritative source even when file is small
should fall back to patch file size when message.diff_size is not provided push_to_pull_request_branch.test.cjs ✅ Design Backward-compat error path — verifies file size used when diff_size absent

Minor Suggestions

⚠️ Test Inflation — push_to_pull_request_branch.test.cjs

The test file grew at a 2.5:1 ratio relative to the production file (+50 vs +20 lines). This is marginal and not a concern here — the three new unit tests are each legitimately distinct scenarios covering three different behavioral cases (prefer diff_size, reject on diff_size, fallback to file size). The extra lines are well-justified.

i️ Happy-path-only test

"should prefer message.diff_size..." has no error assertions — it only verifies the success case. The complementary test "should reject when diff_size exceeds max..." covers the failure case, so together these provide full behavioral coverage. No change needed.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests (no Go test files modified)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 4 tests (vitest)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All 4 new tests verify behavioral contracts directly tied to the bug fix. The integration test in git_patch_integration.test.cjs is particularly valuable — it exercises real git operations and proves the fix end-to-end.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References:

🧪 Test quality analysis by Test Quality Sentinel · ● 603.3K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 73/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). All 4 new tests verify behavioral contracts for the patch size fix.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

@github-actions
Copy link
Copy Markdown
Contributor

Hey @Copilot 🎉 — great work on this fix! The incremental net diff approach for max_patch_size validation is exactly the right solution for the long-running autoloop branch scenario, and the backward-compatible fallback is a thoughtful touch.

The PR includes thorough test coverage (3 new unit tests + 1 integration test reproducing the exact failure scenario), a clear and detailed description, a changeset entry, and stays tightly focused on the single bug. This looks ready for maintainer review!

Generated by Contribution Check · ● 1.5M ·

…ile, enforce limit on bundles

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9d54ee1e-974b-45f3-a4d8-d78cd2281713

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot review all comments

Addressed all four review comments in abf826f:

  1. Error wording when format-patch yields no output — the message now reflects the actual failure and includes commitCount + the range: "git format-patch produced no output for <baseRef>..<branch> despite N incremental commit(s)."
  2. Memory-heavy diff measurement — switched to git diff --binary --output=<tmpfile> + fs.statSync, so measurement is O(1) memory; temp file is cleaned up in finally.
  3. Rejection error message — now distinguishes incremental diff size vs. transport file size and includes both values when diff_size is authoritative.
  4. Bundle transport bypassing the size limitmax_patch_size is now also enforced on bundle transport (via the on-disk bundle file size, or message.diff_size when available). Added 2 tests covering bundle size enforcement and bundle diff_size preference.

All 6954 JS tests pass.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • invalid.example.invalid
    • Triggering command: /usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git git conf�� user.email test@example.com ache/uv/0.11.7/x86_64/git /tmp/bare-incremgit . bin/git git clon�� /tmp/bare-incremental-EeDE96 . it README.md git t git (dns block)
    • Triggering command: /usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git git conf�� user.email lure test commit tions/setup/js/node_modules/.bin/git ing-branch..longgit Accumulated workcheckout /git git clon�� /tmp/bare-incremental-u5jIaI . k/gh-aw/gh-aw/actions/setup/js/node_modules/.bin/git tiny.txt it k/gh-aw/node_modagent-change.txt git (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI requested a review from pelikhan April 24, 2026 05:53
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot move git patch utilities to separate file, add tests using local git repo

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot move git patch utilities to separate file, add tests using local git repo

Done in 960e574.

Moved git patch utilities into a new actions/setup/js/git_patch_utils.cjs:

  • sanitizeForFilename, sanitizeBranchNameForPatch, sanitizeRepoSlugForPatch
  • getPatchPath, getPatchPathForRepo
  • buildExcludePathspecs (extracted from the inline excludeArgs builder)
  • computeIncrementalDiffSize (extracted from the in-function tmpfile+stat+cleanup logic)

generate_git_patch.cjs now imports and delegates to these helpers, and re-exports them so existing callers (e.g. find_repo_checkout.test.cjs) keep working unchanged. Registered the new file in actions/setup/setup.sh SAFE_OUTPUTS_FILES.

Added git_patch_utils.test.cjs with 18 tests, including 6 that run against a real local git repo (git init in a temp dir, real commits, real git diff --output). Coverage: size matches independently-computed git diff bytes, temp file always cleaned up, zero size for identical refs, excludedFiles pathspecs actually shrink the measured diff, null for invalid refs, null for missing args.

All 6972 JS tests pass.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • invalid.example.invalid
    • Triggering command: /usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git git conf�� user.name Test User ache/uv/0.11.7/x86_64/git --bare --initial-branchcheckout ache/uv/0.11.7/x-b git init�� --bare --initial-branch=main it README.md git it git (dns block)
    • Triggering command: /usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git git fetc�� origin est-change..feature/test-change cal/bin/git -r --raw 86_64/git git conf�� user.name Test User tions/setup/js/node_modules/.bin/git -1 --format=%B tions/node_modulagent-change.txt git (dns block)
    • Triggering command: /usr/lib/git-core/git-remote-https /usr/lib/git-core/git-remote-https origin https://invalid.example.invalid/nonexistent-repo.git git conf�� user.email test@example.com ache/node/24.14.1/x64/lib/node_modules/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin/git /tmp/bare-incremgit . ules/.bin/git git clon�� /tmp/bare-incremental-MIP74i . it README.md /usr/lib/git-coradd t git (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan pelikhan merged commit 7defc03 into main Apr 24, 2026
39 checks passed
@pelikhan pelikhan deleted the copilot/fix-patch-size-calculation branch April 24, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

push_to_pull_request_branch should compute patch size relative to PR branch head, not checkout base

4 participants