Skip to content

fix: handle HEAD-only bundles in create_pull_request safe-output fallback#35989

Merged
pelikhan merged 4 commits into
mainfrom
copilot/create-pull-request-fix
May 30, 2026
Merged

fix: handle HEAD-only bundles in create_pull_request safe-output fallback#35989
pelikhan merged 4 commits into
mainfrom
copilot/create-pull-request-fix

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 30, 2026

When git bundle list-heads returns only a HEAD entry (no refs/heads/*), the fallback path in applyBundleToBranch aborted with expected exactly 1 refs/heads entry, found 0, breaking every create_pull_request run whose agent produced a HEAD-only bundle.

Changes

  • create_pull_request.cjs — extends the branchRefs.length === 0 case in the list-heads fallback to fetch using HEAD:<tempRef> instead of throwing:

    } else if (branchRefs.length === 0) {
      // Bundle has only HEAD — no refs/heads/* entry
      const headLine = bundleHeadsOutput
        .split("\n")
        .map(line => line.trim())
        .find(line => /^[0-9a-f]{40}\s+HEAD$/.test(line));
      if (headLine) {
        await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]);
      } else {
        throw new Error(`...bundle contains no refs/heads entries and no HEAD ref`);
      }
    }
  • create_pull_request.test.cjs — unit test covering the HEAD-only mock path, asserting exec is called with a HEAD:<tempRef> refspec.

  • create_pull_request_bundle_integration.test.cjs — integration test using a real git bundle create <f> HEAD (no refs/heads/*), verifying the target branch lands at the expected SHA.

Copilot AI and others added 2 commits May 30, 2026 20:49
When git bundle list-heads returns only a HEAD entry (no refs/heads/*),
the previous fallback threw "expected exactly 1 refs/heads entry, found 0".

Extend the else-if ladder so a zero-refs/heads result tries fetching with
`HEAD:<temp-ref>` instead of aborting. The HEAD SHA is validated against
the 40-hex pattern before the fetch is attempted; if even HEAD is absent the
error message now clearly says so.

Also add:
- Unit test in create_pull_request.test.cjs for the HEAD-only mock path
- Integration test in create_pull_request_bundle_integration.test.cjs using
  a real HEAD-only bundle (git bundle create <f> HEAD)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix create_pull_request safe-output handler for missing refs fix: handle HEAD-only bundles in create_pull_request safe-output fallback May 30, 2026
Copilot AI requested a review from pelikhan May 30, 2026 20:51
@pelikhan pelikhan marked this pull request as ready for review May 30, 2026 20:54
Copilot AI review requested due to automatic review settings May 30, 2026 20:54
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot add git integration test

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

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

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 fixes create_pull_request bundle application when bundle fallback inspection finds only HEAD and no refs/heads/* entry.

Changes:

  • Adds a HEAD-only fallback path in applyBundleToBranch.
  • Adds unit coverage for the mocked HEAD-only fallback.
  • Adds integration coverage using a real HEAD-only git bundle.
Show a summary per file
File Description
actions/setup/js/create_pull_request.cjs Adds fallback fetching from HEAD:<tempRef> when no branch refs are listed.
actions/setup/js/create_pull_request.test.cjs Verifies the fallback issues a HEAD-based bundle fetch.
actions/setup/js/create_pull_request_bundle_integration.test.cjs Validates applying a real HEAD-only bundle end-to-end.

Copilot's findings

Tip

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

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

const headLine = bundleHeadsOutput
.split("\n")
.map(line => line.trim())
.find(line => /^[0-9a-f]{40}\s+HEAD$/.test(line));
@github-actions github-actions Bot mentioned this pull request May 30, 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.

Correct, well-scoped fix. The HEAD-only bundle fallback is properly guarded, and both unit and integration tests validate the real git behavior.

### Review summary

Grumpy-coder findings adjudicated:

  • git fetch <bundle> HEAD:<dst> will failDROPPED. The integration test runs a real git bundle create ... HEAD + fetch cycle and proves this works. The claim is incorrect.
  • SHA-256 regex fragility ({40} vs {64})* — Valid future concern, non-blocking. GitHub infra still uses SHA-1 object storage.
  • headLine SHA unused in log — Left as a minor inline suggestion (non-blocking).
  • case-sensitive hex regexDROPPED. Git consistently outputs lowercase hex SHAs.

Code is correct. The fix handles the edge case cleanly.

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

await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]);
} else {
throw new Error(`Failed to resolve bundle branch ref from list-heads: bundle contains no refs/heads entries and no HEAD ref`);
}
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.

Non-blocking: headLine SHA is checked for existence but not surfaced in the log message, missing a useful debugging signal when this fallback fires in production.

💡 Suggestion

Extract the SHA from headLine and include it in the core.info call:

const headSha = headLine.split(/\s+/)[0];
core.info(`Bundle has no refs/heads entries; fetching HEAD (${headSha}) directly into ${bundleTempRef}`);

This makes it trivial to correlate the fetched commit against what was expected when reading GitHub Actions logs. Non-blocking — the current code is functionally correct.

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 — approving with minor suggestions.

📋 Key Themes & Highlights

Key Themes

  • Test assertion style: one test uses throw new Error(...) instead of a proper expect() assertion (minor)
  • Uncovered error branch: the branchRefs.length === 0 + no-HEAD-line error path has no unit test
  • Diagnostic logging: the resolved HEAD SHA is parsed but not surfaced in logs

Positive Highlights

  • ✅ Root cause correctly identified and surgically fixed (else if (branchRefs.length === 0) guard)
  • ✅ Both a mocked unit test and a real-git integration test are included — good dual coverage
  • ✅ Integration test explicitly asserts the bundle shape that reproduces the bug (no refs/heads/ in list-heads)
  • ✅ Strict SHA regex (/^[0-9a-f]{40}\s+HEAD$/) avoids false positives
  • ✅ Zero deletions — purely additive, low regression risk

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

// Should have fetched using HEAD:<temp-ref> as the refspec
const headFetchCall = global.exec.exec.mock.calls.find(
([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && typeof args[2] === "string" && args[2].startsWith("HEAD:")
);
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] Using throw new Error(...) for assertion failure is unusual in a test — prefer expect(headFetchCall).toBeTruthy(...) so Jest/Vitest can format the failure consistently and count it as a proper assertion.

💡 Suggested replacement
// Instead of:
if (!headFetchCall) {
  throw new Error("expected HEAD-based bundle fetch call");
}
expect(headFetchCall[1][2]).toMatch(...);

// Prefer:
expect(headFetchCall).toBeTruthy("expected HEAD-based bundle fetch call using HEAD:<tempRef> refspec");
expect(headFetchCall[1][2]).toMatch(...);

This also removes the manual null-guard before the .toMatch assertion.

.split("\n")
.map(line => line.trim())
.find(line => /^[0-9a-f]{40}\s+HEAD$/.test(line));
if (headLine) {
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] headLine is parsed to extract the SHA but the SHA value itself is never used or logged — only the line's truthiness matters. Consider adding a core.info with the resolved SHA to aid future diagnostics.

💡 Suggestion
if (headLine) {
  const headSha = headLine.split(/\s+/)[0];
  core.info(`Bundle has no refs/heads entries; fetching HEAD (${headSha}) directly into ${bundleTempRef}`);
  await execApi.exec("git", ["fetch", bundleFilePath, `HEAD:${bundleTempRef}`]);
}

Logging the SHA makes it easy to verify the correct commit was fetched when reviewing workflow logs.

@@ -423,6 +423,73 @@ index 0000000..abc1234
expect(resolvedFetchCall[1][2]).toMatch(/^refs\/heads\/main:refs\/bundles\/create-pr-ops-review-may09-2026-[a-f0-9]{8}$/);
});

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 error branch — branchRefs.length === 0 with no HEAD line in the output — has no test coverage. A bundle producing empty list-heads output would silently hit this path.

💡 Suggested test skeleton
it("should throw when bundle has no refs/heads entries and no HEAD ref", async () => {
  // mock git bundle list-heads to return empty output
  global.exec.getExecOutput.mockImplementation((cmd, args) => {
    if (cmd === "git" && args[0] === "bundle" && args[1] === "list-heads") {
      return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" });
    }
    // ... other mocks ...
  });

  await expect(handler(...)).rejects.toThrow(
    "bundle contains no refs/heads entries and no HEAD ref"
  );
});

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100 — Excellent

Analyzed 2 test(s): 2 design, 0 implementation, 0 guideline violations.

📊 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 2 (100%)
Duplicate test clusters 0
Test inflation detected Yes (110 test lines / 15 production lines ≈ 7.3:1)
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
should fall back to HEAD refspec when bundle contains only HEAD actions/setup/js/create_pull_request.test.cjs ✅ Design Some assertions lack descriptive messages; asserts result.success and the HEAD:<ref> fetch refspec
applies a HEAD-only bundle (no refs/heads/* entry) using HEAD refspec fallback actions/setup/js/create_pull_request_bundle_integration.test.cjs ✅ Design Full end-to-end git integration; asserts exact HEAD commit hash and file contents

Language Support

Tests analyzed:

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

⚠️ should fall back to HEAD refspec when bundle contains only HEAD (actions/setup/js/create_pull_request.test.cjs)

Classification: Design test — behavioral contract (minor style note)
Issue: Two vitest assertions use bare .toBe(true) and .toHaveBeenCalledWith(...) without a descriptive message argument, making failure output less informative. Additionally, the test verifies global.exec.getExecOutput was called with specific args (internal call pattern) alongside the observable result.success assertion — the former is an implementation detail, but it is justified here because the refspec chosen is the core behavioral guarantee being tested.
Suggested improvement: Add descriptive message strings to assertions, e.g. expect(result.success).toBe(true, "expected handler to succeed with HEAD-only bundle") and consider commenting why verifying the HEAD:<ref> fetch call is the key contract.

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The 7.3:1 test-to-production line ratio is expected here: the integration test requires real git repository setup (multiple execGit calls for init, commit, branch, bundle) to reproduce the bug scenario end-to-end, which is far more valuable than the line count suggests.

📖 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 1.1M ·

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: 80/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). Both new tests exercise the HEAD-only bundle fallback as behavioral contracts, including a real end-to-end integration test.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 30, 2026

@copilot add git integration test

Addressed in fc718a5: actions/setup/js/create_pull_request_bundle_integration.test.cjs now includes a real git integration test that creates a HEAD-only bundle with git bundle create <file> HEAD and verifies applyBundleToBranch lands the target branch at the expected SHA. I also re-ran the targeted vitest files locally and they pass.

@github-actions
Copy link
Copy Markdown
Contributor

@copilot please fix the lint-js failure and rerun checks, then summarize any remaining blockers.

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

@pelikhan
Copy link
Copy Markdown
Collaborator

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

Copilot AI commented May 30, 2026

@copilot lint js https://github.com/github/gh-aw/actions/runs/26694675925/job/78677038120

Addressed in 6056e69: formatted the two new JS test files that lint-js flagged (create_pull_request.test.cjs and create_pull_request_bundle_integration.test.cjs). npm run lint:cjs and the targeted vitest run both pass locally.

@pelikhan pelikhan merged commit 51378f6 into main May 30, 2026
@pelikhan pelikhan deleted the copilot/create-pull-request-fix branch May 30, 2026 23:42
@github-actions
Copy link
Copy Markdown
Contributor

@copilot refresh the branch and rerun checks, then post any remaining blockers.

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

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 safe-output: bundle contains only HEAD, no refs/heads/* entry — fallback in #31955 cannot recover (v0.76.0)

3 participants