Skip to content

push_to_pull_request_branch: bundle generated for current checkout branch, not the named pull_request_number — apply fails with "couldn't find remote ref" in multi-PR batch workflows #41643

Description

@dsyme

Summary

push_to_pull_request_branch silently generates a git bundle for the wrong branch in multi-PR batch workflows, because the bundle's source branch is derived from the working-tree's current HEAD at MCP-call time, while the apply step independently derives the destination branch from the pull_request_number argument. When those two disagree, the apply step fails with:

Error: Failed to apply bundle: Failed to fetch bundle: fatal: couldn't find remote ref refs/heads/<pr-head-branch>

and the safe output never lands. The PR instead receives a warning comment:

Warning

The push_to_pull_request_branch operation failed: Failed to apply bundle. The code changes were not applied.

Where it happens

This is reproducible with batch workflows that loop over many PRs, git checkout each PR branch, and call push_to_pull_request_branch(pull_request_number=X). Observed in production runs of the github/github-automation Deep Clean workflow operating cross-repo against github/github (sparse, shallow checkout, PAT-authenticated):

Root cause

The source branch (what goes into the bundle) and the destination branch (where the apply step pushes) are computed from two independent, unreconciled sources:

  1. MCP collection timepushToPullRequestBranchHandler in actions/setup/js/safe_outputs_handlers.cjs strips any agent-supplied branch and sets:

    const detectedBranch = getCurrentBranch(repoCwd);
    entry.branch = detectedBranch;

    The accompanying comment encodes the (unenforced) assumption:

    Derive it from the current checkout: the working tree must be on the PR head ref because that's what the agent committed onto. The apply-time push job independently re-derives the destination from pulls.get(pull_number)...

    The bundle filename and its internal ref are then built from this entry.branchgenerate_git_bundle.cjs runs git bundle create <file> <baseRef>..<branchName>, so the bundle contains refs/heads/<currentBranch>.

  2. Apply timeactions/setup/js/push_to_pull_request_branch.cjs resolves the destination independently from the pull_request_number:

    const response = await githubClient.rest.pulls.get({ ... pull_number });
    branchName = pullRequest.head.ref;            // <- destination branch
    ...
    const bundleFetchRef = `refs/heads/${branchName}:${bundleRef}`;
    await exec.getExecOutput("git", ["fetch", bundleFilePath, bundleFetchRef], ...);

    while the bundle file itself is located via message.branch (the current-checkout branch) in resolve_transport_paths.cjs.

There is no validation that getCurrentBranch() corresponds to the PR identified by pull_request_number. When the agent's working tree is checked out on a different branch than the PR it names — which happens whenever tool calls interleave with git checkout, run in parallel, or execute out of order in a batch loop — the bundle is named after, and contains a ref for, branch A, but the apply step tries to git fetch refs/heads/B (B = the named PR's head). The bundle has no refs/heads/B, so git fetch fails with couldn't find remote ref refs/heads/B.

The agent in run 28172063942 actually observed the mismatch itself:

# Switch to 437916 branch for its push (the bundle ended up on 438016 so
# 437916 still needs a push)

Concrete evidence

Run 28196257479, message 1/12 (from the safe_outputs log):

Bundle file path: /tmp/gh-aw/aw-github-github-dsyme-deep-clean-style-mechanical-authorization-platform-reviewers-01-...bundle
Target branch:    dsyme/deep-clean/style-mechanical-lifecycle-reviewers-01-...
git fetch <...authorization-platform...bundle> refs/heads/dsyme/deep-clean/style-mechanical-lifecycle-reviewers-01-...:refs/bundles/...
fatal: couldn't find remote ref refs/heads/dsyme/deep-clean/style-mechanical-lifecycle-reviewers-01-...
Error: Failed to apply bundle

The bundle is …authorization-platform… but the target branch is …lifecycle…. The bundle only contains refs/heads/…authorization-platform…, so fetching refs/heads/…lifecycle… cannot succeed.

Run 28172063942, message 1/11 shows the same pattern: bundle …minitest-assertions-marketplace… vs target branch …style-mechanical-hosted-compute….

Why the existing recovery paths don't catch it

The apply step has recovery logic for missing prerequisite commits (shallow/fetch-depth:1 races, cf. #32310/#32467) but that path only triggers when the bundle ref exists and is missing ancestors. Here the bundle ref is entirely absent (wrong branch name), so extractBundlePrerequisiteCommits() returns empty and the handler goes straight to throw new Error("Failed to fetch bundle: ..."). This is a different failure mode than the shallow-clone bundle issues (#31600, #32310, #32467) and from the create_pull_request ref-name mismatch (#31918, #32069), all of which are closed.

Suggested fixes (any one closes the gap)

  1. Bind source to the named PR at MCP time. When pull_request_number / target is supplied, resolve the PR head ref via the API in pushToPullRequestBranchHandler and assert the current checkout matches it. If not, fail fast with a clear, actionable error (or check out the correct ref before generating the bundle) instead of silently bundling the wrong branch.

  2. Make the apply step fetch the ref the bundle actually contains. Record the bundle's real source branch in the safe-output message and, at apply time, fetch that ref from the bundle, then update-ref the API-resolved destination head to the bundle tip. This decouples "ref inside bundle" from "destination branch name".

  3. At minimum, add a guard + diagnostic in the apply handler: if message.branch (the bundle's source) differs from the resolved pullRequest.head.ref, emit an explicit error like "bundle was generated for branch A but PR #N head is branch B; the working tree was not on the PR head ref when push_to_pull_request_branch was called" rather than the opaque couldn't find remote ref.

Environment

  • gh-aw safe-outputs runtime (MCP collection + apply jobs), cross-repo push with target-repo + per-handler github-token (PAT), sparse + shallow (fetch-depth: 1) checkout of the target repo.
  • Engine: copilot (claude-sonnet model), batch workflow processing 10+ PRs per run.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions