Skip to content

Fix bundle transport in shallow checkouts#31346

Merged
pelikhan merged 3 commits intomainfrom
copilot/fix-bundle-transport-issue
May 10, 2026
Merged

Fix bundle transport in shallow checkouts#31346
pelikhan merged 3 commits intomainfrom
copilot/fix-bundle-transport-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 10, 2026

Bug Fix

push_to_pull_request_branch bundle transport failed in safe_outputs shallow checkouts because git merge --ff-only could not prove ancestry and reported unrelated histories. The bundle ref already represents the intended branch tip, so applying it should not depend on merge-base discovery.

  • What changed

    • Replaced bundle application via git merge --ff-only with a guarded ref update.
    • Reset the worktree after moving the checked-out branch to the bundle tip.
    • Added regression coverage to ensure bundle transport no longer invokes merge --ff-only.
  • Core flow

await exec.exec("git", ["update-ref", `refs/heads/${branchName}`, bundleRef, remoteHeadBeforePatch], baseGitOpts);
await exec.exec("git", ["reset", "--hard"], baseGitOpts);
  • Effect
    • Preserves bundle transport behavior for merge-topology commits.
    • Avoids shallow-clone ancestry failures without requiring unshallowing or changing checkout depth.

Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix bundle transport failure in shallow checkout Fix bundle transport in shallow checkouts May 10, 2026
Copilot finished work on behalf of mrjf May 10, 2026 13:59
Copilot AI requested a review from mrjf May 10, 2026 13:59
@pelikhan pelikhan marked this pull request as ready for review May 10, 2026 17:09
Copilot AI review requested due to automatic review settings May 10, 2026 17:09
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 bundle transport in push_to_pull_request_branch for shallow checkouts by avoiding git merge --ff-only (which can fail ancestry verification) and instead moving the branch ref to the bundle tip, then hard-resetting the worktree.

Changes:

  • Replace bundle application from git merge --ff-only to a guarded git update-ref on the checked-out branch.
  • Hard reset the worktree (git reset --hard) after updating the branch ref to reflect the new tip.
  • Add a regression test asserting bundle transport uses update-ref/reset and does not invoke merge --ff-only.
Show a summary per file
File Description
actions/setup/js/push_to_pull_request_branch.cjs Switch bundle application from merge to direct ref update + worktree reset for shallow checkout robustness.
actions/setup/js/push_to_pull_request_branch.test.cjs Add regression coverage ensuring bundle transport no longer calls git merge --ff-only.

Copilot's findings

Tip

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

  • Files reviewed: 2/2 changed files
  • Comments generated: 2

Comment on lines +607 to +609
// checkouts, merge --ff-only can fail to discover the ancestry even
// when the bundle was created from origin/<branch>..branch and the
// prerequisite exists locally.
Comment on lines +1201 to +1207
it("should apply bundle transport by updating the branch ref instead of merging", async () => {
const bundlePath = path.join(tempDir, "test.bundle");
const patchPath = createPatchFile("small patch content");
fs.writeFileSync(bundlePath, "bundle content");

const pushSignedCommitsModule = require("./push_signed_commits.cjs");
const pushSignedSpy = vi.spyOn(pushSignedCommitsModule, "pushSignedCommits").mockResolvedValue("bundle-tip");
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 — this is a bug fix with a regression test, the canonical use case for both skills.

Key Themes

  • Fix is correct and well-reasoned: replacing merge --ff-only with update-ref + reset --hard directly addresses the root cause (shallow-clone ancestry gaps) rather than masking it. The guarded old-value argument preserves safety by making the ref update atomic against concurrent remote advances.
  • Regression test covers the primary path: the new test verifies that merge --ff-only is no longer called and that update-ref/reset are invoked. One coverage gap flagged inline: the !remoteHeadBeforePatch branch (update-ref without old-value guard) is untested.
  • Existing review comments (misleading inline comment wording, test placed inside the wrong describe block) are accurate — worth addressing before merge for long-term maintainability.

Positive Highlights

  • ✅ Regression test is present and makes a clear negative assertion (expect(mockExec.exec).not.toHaveBeenCalledWith(..."merge"...))
  • ✅ The conditional old-value guard is a thoughtful safety mechanism — keeps update-ref transactional where possible
  • ✅ PR description clearly explains the root cause and the chosen approach

Verdict

Approving — the fix is sound. The inline comment and test location issues noted by prior reviewers, and the uncovered branch I flagged, are polish items that can be addressed here or in follow-up.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 3.7M

mockExec.getExecOutput
.mockResolvedValueOnce({ exitCode: 0, stdout: "remote-head\trefs/heads/feature-branch\n", stderr: "" }) // preflight ls-remote
.mockResolvedValueOnce({ exitCode: 0, stdout: "remote-head\n", stderr: "" }) // rev-parse HEAD before bundle
.mockResolvedValueOnce({ exitCode: 0, stdout: "2\n", stderr: "" }); // rev-list --count
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 if (remoteHeadBeforePatch) guard means update-ref can be called without the old-value safety argument, but this code path is untested — the new test always exercises the non-empty case.

Consider adding a complementary case:

it("should update branch ref without old-value guard when remoteHeadBeforePatch is absent", async () => {
  mockExec.getExecOutput
    .mockResolvedValueOnce({ exitCode: 0, stdout: "\trefs/heads/feature-branch\n", stderr: "" }) // ls-remote (no SHA)
    .mockResolvedValueOnce({ exitCode: 0, stdout: "", stderr: "" })                              // rev-parse: empty
    .mockResolvedValueOnce({ exitCode: 0, stdout: "2\n", stderr: "" });                          // rev-list count

  // ... exercise handler ...

  expect(mockExec.exec).toHaveBeenCalledWith(
    "git",
    ["update-ref", "refs/heads/feature-branch", "refs/bundles/push-feature-branch"],
    expect.any(Object)
  );
  // No 4th arg: intentional, but documents the safety trade-off
});

Without this, the branch that skips the old-value guard is invisible to the test suite, and the intentional trade-off (ref can be force-updated if remote has advanced concurrently) goes undocumented.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 60/100

⚠️ Acceptable — with suggestions

Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 0 (0%)
Duplicate test clusters 0
Test inflation detected ✅ Yes (28 lines added to test vs 11 to production — ratio ≈ 2.5:1)
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
should apply bundle transport by updating the branch ref instead of merging actions/setup/js/push_to_pull_request_branch.test.cjs ✅ Design Happy-path only; no error case for update-ref failure

Flagged Tests — Requires Review

⚠️ should apply bundle transport by updating the branch ref instead of merging

Classification: Design test (behavioral contract) — but happy-path only
Issue: The test correctly verifies that git update-ref is used instead of git merge --ff-only (the core behavioral contract of this fix). However, it only exercises the success path. The production code now takes two paths depending on whether remoteHeadBeforePatch is available: one that passes the old SHA as a safety lock, and one that omits it. Only the first path is tested. No test covers update-ref failure or reset --hard failure.

What design invariant does this test enforce? That bundle transport uses ref-update semantics, not merge semantics — preventing shallow-checkout ancestry failures.
What would break if deleted? A regression back to git merge --ff-only would go undetected — genuinely high value.
Suggested improvement: Add a second table-driven case where remoteHeadBeforePatch is undefined/empty to exercise the no-lock-SHA path, and one case where git update-ref returns a non-zero exit code to verify the error surface.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests — no Go test files changed
  • 🟨 JavaScript (*.test.cjs): 1 test (vitest)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The single new test is a genuine behavioral contract test that would catch a regression to the old merge --ff-only approach. Score is reduced from a potential 70 due to test inflation (28 test lines / 11 production lines ≈ 2.5:1 ratio) and the absence of an error-path assertion.


📐 Score Breakdown
Component Max Earned Reason
Behavioral Coverage 40 40 1/1 tests are design tests
Error/Edge Case Coverage 30 0 0/1 tests include an error or edge case
Low Duplication 20 20 No duplicate clusters detected
Proportional Growth 10 0 Inflation ratio 2.5:1 exceeds 2:1 threshold
Total 100 60
📖 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: §25634715413

🧪 Test quality analysis by Test Quality Sentinel · ● 9.6M ·

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: 60/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). The new bundle-transport test enforces a genuine behavioral contract. See detailed analysis in the PR comment.

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

Copilot AI commented May 10, 2026

@copilot review all comments

Addressed the review comments in 8d44ce7: clarified the bundle transport inline comment and moved the regression test into a dedicated bundle transport describe block.

Copilot AI requested a review from pelikhan May 10, 2026 17:33
@pelikhan pelikhan merged commit 6e47752 into main May 10, 2026
90 checks passed
@pelikhan pelikhan deleted the copilot/fix-bundle-transport-issue branch May 10, 2026 18:08
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.

Bundle transport fails with 'refusing to merge unrelated histories' in shallow checkout

4 participants