Skip to content

Pin safe-output patch application to recorded base commit SHA#34876

Merged
pelikhan merged 11 commits into
mainfrom
copilot/fix-patch-application-issue
May 26, 2026
Merged

Pin safe-output patch application to recorded base commit SHA#34876
pelikhan merged 11 commits into
mainfrom
copilot/fix-patch-application-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 26, 2026

In concurrent pipelines, patches are generated from one commit graph but were applied against a newer remote branch tip, causing frequent git am --3way failures after unrelated merges. This change aligns apply-time checkout/reset with the same base_commit captured at patch-generation time.

  • Create PR flow: apply from recorded generation base

    • create_pull_request.cjs now prefers pullRequestItem.base_commit when creating the working branch for patch transport.
    • If that SHA is unavailable locally, behavior degrades safely to existing base-branch behavior.
  • Push-to-PR-branch flow: re-anchor before git am

    • push_to_pull_request_branch.cjs now resets to message.base_commit before patch apply (patch transport only).
    • If the commit cannot be resolved/fetched, it logs and continues from current HEAD (existing fallback semantics).
  • Targeted regression coverage

    • Added focused tests proving both handlers use base_commit when present, preventing apply-time drift from generation-time base.
// create_pull_request: branch is anchored to generation-time base
if (pullRequestItem.base_commit) {
  await exec.exec("git", ["cat-file", "-e", pullRequestItem.base_commit]);
  await exec.exec("git", ["checkout", "-b", branchName, pullRequestItem.base_commit]);
}

// push_to_pull_request_branch: patch apply is re-anchored first
if (!hasBundleFile && message.base_commit) {
  await exec.exec("git", ["cat-file", "-e", message.base_commit], baseGitOpts);
  await exec.exec("git", ["reset", "--hard", message.base_commit], baseGitOpts);
  await exec.exec("git", ["am", "--3way", patchFilePath], [], baseGitOpts);
}

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix patch application using latest base instead of starting SHA Pin safe-output patch application to recorded base commit SHA May 26, 2026
Copilot AI requested a review from pelikhan May 26, 2026 05:52
@pelikhan pelikhan marked this pull request as ready for review May 26, 2026 05:53
Copilot AI review requested due to automatic review settings May 26, 2026 05:53
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

This PR aims to reduce git am --3way failures in concurrent pipelines by ensuring patch-transport flows apply patches against the same recorded base_commit that was captured during patch generation, rather than whatever the remote branch tip happens to be at apply time.

Changes:

  • create_pull_request: create the working branch from pullRequestItem.base_commit when available (with fallback to base branch behavior).
  • push_to_pull_request_branch: for patch transport, attempt to reset the working tree to message.base_commit prior to git am.
  • Add targeted tests covering both handlers’ use of base_commit.
Show a summary per file
File Description
actions/setup/js/push_to_pull_request_branch.cjs Adds pre-git am re-anchoring to recorded base_commit (patch transport only).
actions/setup/js/push_to_pull_request_branch.test.cjs Adds a regression test asserting reset/cat-file calls for base_commit.
actions/setup/js/create_pull_request.cjs Anchors new PR branch creation to base_commit when present.
actions/setup/js/create_pull_request.test.cjs Adds a regression test asserting branch creation from base_commit.

Copilot's findings

Tip

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

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

if (remoteHeadBeforePatch && remoteHeadBeforePatch !== recordedBaseCommit) {
core.warning(`Remote PR branch advanced since patch generation (remote HEAD ${remoteHeadBeforePatch}, patch base ${recordedBaseCommit}); applying patch from recorded base commit`);
}
await exec.exec("git", ["reset", "--hard", recordedBaseCommit], baseGitOpts);
Comment on lines +665 to +677
if (!hasBundleFile && message.base_commit) {
const recordedBaseCommit = String(message.base_commit).trim();
if (recordedBaseCommit) {
try {
try {
await exec.exec("git", ["fetch", "origin", recordedBaseCommit, "--depth=1"], {
env: { ...process.env, ...gitAuthEnv },
...baseGitOpts,
});
} catch (fetchError) {
core.info(`Note: could not fetch base_commit ${recordedBaseCommit} explicitly (${getErrorMessage(fetchError)}); will verify local availability next`);
}
await exec.exec("git", ["cat-file", "-e", recordedBaseCommit], baseGitOpts);
Comment on lines +1505 to +1516
const recordedBaseCommit = pullRequestItem.base_commit;
if (recordedBaseCommit) {
core.info(`Using base_commit from safe output entry for patch apply: ${recordedBaseCommit}`);
try {
try {
await exec.exec("git", ["fetch", "origin", recordedBaseCommit, "--depth=1"]);
} catch (fetchError) {
core.info(`Note: could not fetch base commit ${recordedBaseCommit} explicitly (${fetchError instanceof Error ? fetchError.message : String(fetchError)}); will verify local availability next`);
}
await exec.exec("git", ["cat-file", "-e", recordedBaseCommit]);
branchBaseRef = recordedBaseCommit;
} catch (baseCommitError) {
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #34876 does not have the 'implementation' label and has 0 new lines of code in default business logic directories (≤100 threshold).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions github-actions Bot mentioned this pull request May 26, 2026
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.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 1.2M

env: { ...process.env, ...gitAuthEnv },
...baseGitOpts,
});
} catch (fetchError) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale-object hazard: a failed git fetch is silently swallowed, then cat-file -e succeeds against a locally-cached object that may belong to a completely different history (e.g. after a force-push on origin), causing reset --hard to land on the wrong commit with no error surfaced.

💡 Suggested fix

After a fetch failure, do not fall through to cat-file. Either skip the reset path entirely or surface a warning and abort:

try {
  await exec.exec("git", ["fetch", "origin", recordedBaseCommit, "--depth=1"], { ... });
} catch (fetchError) {
  core.warning(`Cannot fetch base_commit ${recordedBaseCommit}; skipping base-pinning to avoid stale-object reset: ${getErrorMessage(fetchError)}`);
  // do NOT fall through — exit the outer try here
  throw fetchError;  // or just: return / continue with HEAD
}
await exec.exec("git", ["cat-file", "-e", recordedBaseCommit], baseGitOpts);

If the fetch fails and you still want to try a local-only path, at minimum log a prominent warning explaining that the object's provenance is unverified.

Comment thread actions/setup/js/create_pull_request.cjs
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 70/100 — Acceptable

Analyzed 2 test(s): 2 design, 0 implementation, 0 guideline violation(s). Both tests cover the new base_commit pinning behavior but test only the happy path — no error/edge cases.

📊 Metrics & Test Classification (2 tests analyzed)
Metric Value
New/modified tests analyzed 2
✅ Design tests (behavioral contracts) 2 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 0 (0%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
should create the PR branch from base_commit before applying the patch when available actions/setup/js/create_pull_request.test.cjs ✅ Design Happy-path only; no error-case coverage
should reset to message.base_commit before applying patch transport actions/setup/js/push_to_pull_request_branch.test.cjs ✅ Design Happy-path only; no error-case coverage

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests
  • 🟨 JavaScript (*.test.cjs, *.test.js): 2 tests (vitest)
⚠️ Flagged Tests — Requires Review (2 issue(s))

⚠️ should create the PR branch from base_commit before applying the patch when available (actions/setup/js/create_pull_request.test.cjs)

Classification: Design test (behavioral contract present)
Issue: Only tests the happy path — no coverage of failure scenarios such as base_commit not existing in the repo (git cat-file -e fails) or git checkout -b returning a non-zero exit code.
What design invariant does this test enforce? When base_commit is provided, the PR branch is created from that specific commit SHA (security-critical pinning behavior).
What would break if deleted? The regression that motivated this PR (branch created from wrong base) could silently re-appear.
Suggested improvement: Add a test case where git cat-file -e returns a non-zero exit code and assert the handler returns an appropriate error (e.g., result.success === false or a thrown error), confirming the guard against an unknown commit SHA.


⚠️ should reset to message.base_commit before applying patch transport (actions/setup/js/push_to_pull_request_branch.test.cjs)

Classification: Design test (behavioral contract present)
Issue: Only tests the happy path — no coverage of what happens when git reset --hard fails or when base_commit is absent from the repository.
What design invariant does this test enforce? When base_commit is provided, the working tree is reset to that SHA before the patch is applied, ensuring deterministic patch application.
What would break if deleted? The reset --hard step could be silently dropped or reordered without a test catching the regression.
Suggested improvement: Add a case where git cat-file -e or git reset --hard fails (mock returning non-zero exit code) and assert the result reflects the failure, ensuring the guard is actually enforced.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). Score: 70/100 — both tests assert observable success output and verify the security-critical commit-pinning behavior. The main gap is missing error-path coverage for both new tests.

📖 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.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 2.3M ·

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: 70/100. Test quality is acceptable - 0% of new tests are implementation tests (threshold: 30%). Both tests verify the security-critical base_commit pinning behavior with observable success assertions. Consider adding error-path coverage for the commit-not-found case in a follow-up.

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.

Skills-Based Review 🧠

Applied /diagnose and /tdd — commenting with suggestions; no blocking issues that should prevent merge.

📋 Key Themes & Highlights

Key Themes

  • reset --hard semantics deserve documentation: The push flow silently discards any post-generation commits on the PR branch. This is likely intentional (agent work replaces the branch from its exact base), but it would benefit from an explicit comment or warning so operators know what to expect when the branch has drifted for non-trivial reasons.
  • git fetch origin <SHA> will almost always fail: GitHub doesn't serve arbitrary SHA refspecs. The inner catch swallows this predictably, making it log noise on every invocation. Either remove the fetch or replace it with a branch-name fetch.
  • Missing fallback tests in both test files: Happy-path coverage is good; the fallback paths (commit unavailable → revert to current HEAD / base branch) are untested. This is where future regressions are most likely to hide.

Positive Highlights

  • ✅ Root cause correctly identified and addressed at both apply sites
  • ✅ Fallback semantics are safe — degradation to existing behaviour on unavailability
  • ✅ Consistent approach across create_pull_request and push_to_pull_request_branch
  • ✅ Good use of cat-file -e as the availability gate before attempting destructive operations
  • ✅ Regression tests added alongside the fix

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.2M

core.warning(`Unable to use recorded base_commit ${recordedBaseCommit}; applying patch on current branch HEAD: ${getErrorMessage(baseCommitError)}`);
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnose] git reset --hard silently discards any commits that were pushed to the PR branch after patch generation (e.g. manual edits, review-driven commits). A warning is logged when remoteHeadBeforePatch !== recordedBaseCommit, but only in that case — and the warning doesn't distinguish "unrelated merge drift" from "intentional work on the branch".

💡 Suggestion

Consider surfacing a more prominent warning (or even a core.error) when the reset would throw away commits that are ancestors of remoteHeadBeforePatch but not of recordedBaseCommit. This would alert operators before work is silently lost:

// before reset --hard, check how many commits would be discarded
const { stdout: commitsBetween } = await exec.getExecOutput(
  "git", ["log", "--oneline", `${recordedBaseCommit}..HEAD`], baseGitOpts
);
if (commitsBetween.trim()) {
  core.warning(`Discarding ${commitsBetween.trim().split('\n').length} commit(s) on PR branch to re-anchor to base_commit`);
}

Alternatively, document the intentional semantics ("agent work always replaces the branch from base_commit") so reviewers understand the blast radius.

});
} catch (fetchError) {
core.info(`Note: could not fetch base_commit ${recordedBaseCommit} explicitly (${getErrorMessage(fetchError)}); will verify local availability next`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnose] git fetch origin <SHA> --depth=1 is unreliable on GitHub and most Git hosts — servers don't advertise arbitrary commit SHAs as fetchable refs. This will almost always throw, landing in the inner catch. The outer cat-file then determines real availability, which is the right gate — but the fetch attempt adds noise in logs on every invocation.

💡 Suggestion

If the intent is to ensure the commit is present before cat-file, try fetching the branch ref instead of the raw SHA:

// Fetch the branch to ensure recent commits are available, then verify SHA
await exec.exec("git", ["fetch", "origin", baseBranch, "--depth=20"], fetchOpts);

Or simply omit the fetch entirely and rely on cat-file as the gate, adding a comment explaining that shallow clones in Actions may not have the commit, and the fallback handles that case.

const result = await handler({ patch_path: patchPath, base_commit: "base-sha-123" }, {});

expect(result.success).toBe(true);
expect(mockExec.exec).toHaveBeenCalledWith("git", ["cat-file", "-e", "base-sha-123"], expect.any(Object));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The new test covers the happy path but not the fallback: when cat-file fails (commit unavailable), the code should continue applying the patch on current HEAD without reset --hard. Without a test, a future refactor could accidentally break the fallback and silently always fail in cross-repo or missing-object scenarios.

💡 Suggested additional test
it("should fall back to current HEAD when base_commit is not available locally", async () => {
  const patchPath = createPatchFile();
  mockExec.getExecOutput.mockResolvedValue({ exitCode: 0, stdout: "abc123\n", stderr: "" });
  mockExec.exec.mockImplementation(async (cmd, args) => {
    if (args[0] === "cat-file") throw new Error("not in object store");
    return 0;
  });

  const module = await loadModule();
  const handler = await module.main({});
  const result = await handler({ patch_path: patchPath, base_commit: "unknown-sha" }, {});

  expect(result.success).toBe(true);
  // reset --hard should NOT have been called
  const resetCall = mockExec.exec.mock.calls.find(([c, a]) => c === "git" && a[0] === "reset");
  expect(resetCall).toBeUndefined();
});


expect(result.success).toBe(true);
expect(global.exec.exec).toHaveBeenCalledWith("git", ["cat-file", "-e", MOCK_BASE_COMMIT_SHA]);
const checkoutWithBaseCommit = global.exec.exec.mock.calls.find(([cmd, args]) => cmd === "git" && Array.isArray(args) && args[0] === "checkout" && args[1] === "-b" && args[3] === MOCK_BASE_COMMIT_SHA);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The test only exercises the happy path (cat-file succeeds → checkout uses base_commit). The fallback path — where cat-file throws — should also be tested to confirm branchBaseRef reverts to baseBranch. Without it, a typo in the catch block (e.g., accidentally using branchBaseRef = recordedBaseCommit after the error) would go undetected.

💡 Suggested additional test
it("should fall back to base branch when base_commit is unavailable", async () => {
  global.exec = {
    exec: vi.fn().mockImplementation(async (cmd, args) => {
      if (cmd === "git" && args[0] === "cat-file") throw new Error("not in object store");
      return 0;
    }),
    getExecOutput: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }),
  };

  const { main } = require("./create_pull_request.cjs");
  const handler = await main({});
  const result = await handler({ title: "Test", body: "B", patch_path: patchFilePath, branch: "test-branch", base_commit: MOCK_BASE_COMMIT_SHA }, {});

  expect(result.success).toBe(true);
  // checkout -b should use baseBranch (index 3), NOT the base_commit SHA
  const checkoutCall = global.exec.exec.mock.calls.find(([c, a]) => c === "git" && a[0] === "checkout" && a[1] === "-b");
  expect(checkoutCall[1][3]).not.toBe(MOCK_BASE_COMMIT_SHA);
});

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments. Make sure patch and bundle routes are supported.

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

Copilot AI commented May 26, 2026

@copilot review all comments. Make sure patch and bundle routes are supported.

Addressed in c47e451.

  • Added base_commit normalization/validation in both create and push handlers.
  • Patch route now re-anchors signed linearization/push range to the recorded base_commit when reset succeeds.
  • Bundle route remains supported and unchanged (base reset logic still applies to patch transport only).
  • Added regression tests for normalized/invalid base_commit handling and signed-push baseRef anchoring.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review and integration tests

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 26, 2026

@copilot review and integration tests

Reviewed. I ran the relevant patch/bundle integration + targeted tests on c47e451:

  • create_pull_request.test.cjs
  • push_to_pull_request_branch.test.cjs
  • create_pull_request_bundle_integration.test.cjs
  • git_patch_integration.test.cjs
  • patch_path_helpers.integration.test.cjs

All passed (229/229). No additional code changes were needed.

@github-actions github-actions Bot mentioned this pull request May 26, 2026
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all review comments from GitHub-action actor

Copilot AI and others added 4 commits May 26, 2026 13:45
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 26, 2026

@copilot review all review comments from GitHub-action actor

Done. I reviewed all github-actions[bot] review comments and addressed the actionable ones:

  • 18b4d9d: added ancestry safety checks for recorded base_commit in both patch/create flows + fallback tests for unavailable base_commit
  • a4d2dea: tightened fallback test mocks/assertions
  • dd6d1c1, e40451d: improved ancestry rejection/fallback diagnostics

Validation and targeted integration tests pass:

  • create_pull_request.test.cjs
  • push_to_pull_request_branch.test.cjs
  • create_pull_request_bundle_integration.test.cjs
  • git_patch_integration.test.cjs
  • patch_path_helpers.integration.test.cjs

Remaining automated notes are test-style suggestions only (non-blocking).

Copy link
Copy Markdown
Collaborator

@pelikhan pelikhan left a comment

Choose a reason for hiding this comment

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

@copilot refactor duplicate helpers, add integration git tests

Copilot AI and others added 2 commits May 26, 2026 14:06
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 26, 2026

@copilot refactor duplicate helpers, add integration git tests

Done in 932386e and f865ef3.

  • Refactored duplicated normalizeCommitSHA logic into shared actions/setup/js/commit_sha_helpers.cjs and updated both create_pull_request and push_to_pull_request_branch to use it.
  • Added git integration coverage for both routes:
    • Patch route: verifies re-anchoring to recorded base commit enables successful apply after branch drift (git_patch_integration.test.cjs).
    • Bundle route: verifies bundle application preserves merge history (create_pull_request_bundle_integration.test.cjs).

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot add enough logging to debug issues in logs

Copilot AI and others added 2 commits May 26, 2026 14:43
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 26, 2026

@copilot add enough logging to debug issues in logs

Implemented in c560de5.

Added debug logging in both create/push handlers for patch and bundle routes, including apply transport selection, explicit bundle-missing fallback to patch, resolved patch base_commit logging, remote/apply base ref context, and bundle ref update context to make workflow log triage easier.

@github-actions
Copy link
Copy Markdown
Contributor

@copilot review all comments and address the unresolved review threads.
Please summarize the remaining blockers and post the next concrete step.

Generated by 👨‍🍳 PR Sous Chef · gpt54 6.8M ·

@pelikhan pelikhan merged commit a2694c9 into main May 26, 2026
12 checks passed
@pelikhan pelikhan deleted the copilot/fix-patch-application-issue branch May 26, 2026 15:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

create_pull_request / push_to_pull_request_branch: patch applied against latest base, not the SHA it was generated from

3 participants