Summary
For a stacked PR — one whose base branch is the head branch of another open PR — merge-strategy builds fail 100% of the time, permanently, with:
ERROR: Checkout failed
hudson.plugins.git.GitException: Command "/usr/bin/git merge 735f70a5a6bd3d80d9cb773c0ac969226fb88df7" returned status code 1:
stdout:
stderr: merge: 735f70a5a6bd3d80d9cb773c0ac969226fb88df7 - not something we can merge
at ...MergeWithGitSCMExtension.decorateRevisionToBuild(MergeWithGitSCMExtension.java:120)
at ...GitSCM.determineRevisionToBuild(GitSCM.java:1162)
The baseHash the plugin picks is a commit that exists only under refs/pull/<parent>/merge and is reachable from no branch, so the fetch refspec can never bring it into the workspace.
This is the same code path as #1534, but a different trigger, and unlike #1534 it never self-heals — no rescan, close/reopen, or plugin upgrade helps.
Environment
|
|
| github-branch-source |
1917.v9ee8a_39b_3d0d (also present on master today) |
| git / git-client |
5.7.0 / 6.5.0 |
| branch-api |
2.1244.vf95c81f1641c |
| Discovery trait |
OriginPullRequestDiscoveryTrait strategyId=1 (Merging the pull request with the current target branch revision) |
Root cause
Step 1 — GitHub stacks the merge preview. For a PR whose base branch is another open PR's head, GitHub computes the child's merge ref against the parent PR's merge ref, not against the base branch tip:
refs/pull/15618/merge = 7334a316 "Merge e6f95c52 into 56f891d6"
parents = [56f891d6, e6f95c52]
^^^^^^^^ == refs/pull/15617/merge (synthetic!)
refs/heads/feature/minicloud-integration = 6e317e6a <-- the actual base tip
56f891d6 is reachable from no branch — it only exists as refs/pull/15617/merge.
Step 2 — the plugin trusts merge_commit_sha. GitHubSCMSource.createPullRequestSCMRevision():
// We found a merge_commit_sha with 2 parents and one matches the prHeadHash
// Use the other parent hash as the base. ...
mergeHash = proposedMergeHash;
baseHash = prHeadHash.equals(parents.get(0)) ? parents.get(1) : parents.get(0);
The guard above it only checks parents.size() == 2 and parents.contains(prHeadHash) — both true here. Nothing checks that the other parent is on the base branch. The comment "Merge commits always merge against the most recent base commit they can detect" is false for stacks.
Step 3 — the fetch can never satisfy it. GitHubSCMBuilder only ever emits:
+refs/pull/15618/head:refs/remotes/origin/PR-15618
+refs/heads/feature/minicloud-integration:refs/remotes/origin/feature/minicloud-integration
Never refs/pull/*/merge. So baseHash is absent locally, and MergeWithGitSCMExtension runs git merge 56f891d6 → not something we can merge.
Reproducer
- PR A →
master, head branch feature/a.
- PR B →
feature/a, head branch feature/b. Both open, both mergeable.
- Discover origin PRs with strategy "Merging the pull request with the current target branch revision".
- Every build of B fails in checkout. A builds fine.
Live proof it is deterministic, not stale state
Calling the plugin's own resolver from the script console (SCMSource.fetch(head, listener)) reproduces it on demand:
PR-15617 target=master strategy=MERGE
-> 6e317e6a+4698ab66 (56f891d6) base = real master tip ✅ builds green
PR-15618 target=feature/minicloud-integration strategy=MERGE
-> e6f95c52+56f891d6 (7334a316) base = refs/pull/15617/merge ❌ always fails
Recorded SCMRevisionAction for every build of the child PR — the base always equals the parent PR's merge_commit_sha at that moment:
| child build |
baseHash used |
equals |
| #21–24 |
948d822c |
parent's merge ref, rev 1 |
| #25 |
18f54ec4 |
parent's merge ref, rev 2 |
| #26–27 |
735f70a5 |
parent's merge ref, rev 3 |
| #28 |
56f891d6 |
parent's merge ref, rev 4 |
8/8 FAILURE. It also propagates down a 3-deep stack: refs/pull/15668/merge = "Merge <head> into 7334a316", i.e. the grandchild's base becomes the child's merge ref. 10/10 FAILURE there.
Control: a PR based on a long-lived branch with no open PR of its own resolves to a real reachable commit and builds green — confirming the trigger is precisely "base branch has an open PR".
Why this is worse than #1534
#1534 describes the same baseHash-from-merge-parents inference going stale after a retarget — transient, self-heals once GitHub recomputes. Here the derived base is unreachable by construction: it is a synthetic merge ref that will never be reachable from any branch for as long as the parent PR stays open. Rescan, close/reopen, and upgrading the plugin all have no effect. Stacked PRs simply cannot be built with merge strategy.
Proposed fix
This is #1534's proposed fix (a), and it resolves both issues:
After deriving baseHash from the merge parents, verify it is reachable from heads/<base.ref>; if not, fall back to the existing branch-tip lookup.
The fallback already exists a few lines below, for the mergeHash == null path:
baseHash = ghRepository.getRef("heads/" + pr.getBase().getRef()).getObject().getSha();
So the change is to extend the validity check at line ~1736 — e.g. also require that the non-head parent is reachable from the base ref (one compare call, or simply prefer the branch-tip lookup and let MergeWithGitSCMExtension merge against the real tip).
A cheaper heuristic that covers this case without an extra API call: if the non-head parent is itself a 2-parent commit whose parents include the base branch's tip, it is a synthetic PR merge ref and must not be used as baseHash.
Workaround
Switch origin PR discovery from strategyId 1 (merge with target) to strategyId 2 (the current pull request revision). MergeWithGitSCMExtension and the base-branch refspec are both inside if (head.isMerge()) in GitHubSCMBuilder, so HEAD strategy sidesteps the failure — at the cost of no longer testing PRs against their target branch. Flattening the stack (retargeting children to the default branch) also works.
Summary
For a stacked PR — one whose base branch is the head branch of another open PR — merge-strategy builds fail 100% of the time, permanently, with:
The
baseHashthe plugin picks is a commit that exists only underrefs/pull/<parent>/mergeand is reachable from no branch, so the fetch refspec can never bring it into the workspace.This is the same code path as #1534, but a different trigger, and unlike #1534 it never self-heals — no rescan, close/reopen, or plugin upgrade helps.
Environment
mastertoday)OriginPullRequestDiscoveryTraitstrategyId=1 (Merging the pull request with the current target branch revision)Root cause
Step 1 — GitHub stacks the merge preview. For a PR whose base branch is another open PR's head, GitHub computes the child's merge ref against the parent PR's merge ref, not against the base branch tip:
56f891d6is reachable from no branch — it only exists asrefs/pull/15617/merge.Step 2 — the plugin trusts
merge_commit_sha.GitHubSCMSource.createPullRequestSCMRevision():The guard above it only checks
parents.size() == 2andparents.contains(prHeadHash)— both true here. Nothing checks that the other parent is on the base branch. The comment "Merge commits always merge against the most recent base commit they can detect" is false for stacks.Step 3 — the fetch can never satisfy it.
GitHubSCMBuilderonly ever emits:Never
refs/pull/*/merge. SobaseHashis absent locally, andMergeWithGitSCMExtensionrunsgit merge 56f891d6→ not something we can merge.Reproducer
master, head branchfeature/a.feature/a, head branchfeature/b. Both open, both mergeable.Live proof it is deterministic, not stale state
Calling the plugin's own resolver from the script console (
SCMSource.fetch(head, listener)) reproduces it on demand:Recorded
SCMRevisionActionfor every build of the child PR — the base always equals the parent PR'smerge_commit_shaat that moment:948d822c18f54ec4735f70a556f891d68/8 FAILURE. It also propagates down a 3-deep stack:
refs/pull/15668/merge="Merge <head> into 7334a316", i.e. the grandchild's base becomes the child's merge ref. 10/10 FAILURE there.Control: a PR based on a long-lived branch with no open PR of its own resolves to a real reachable commit and builds green — confirming the trigger is precisely "base branch has an open PR".
Why this is worse than #1534
#1534 describes the same
baseHash-from-merge-parents inference going stale after a retarget — transient, self-heals once GitHub recomputes. Here the derived base is unreachable by construction: it is a synthetic merge ref that will never be reachable from any branch for as long as the parent PR stays open. Rescan, close/reopen, and upgrading the plugin all have no effect. Stacked PRs simply cannot be built with merge strategy.Proposed fix
This is #1534's proposed fix (a), and it resolves both issues:
The fallback already exists a few lines below, for the
mergeHash == nullpath:So the change is to extend the validity check at line ~1736 — e.g. also require that the non-head parent is reachable from the base ref (one
comparecall, or simply prefer the branch-tip lookup and letMergeWithGitSCMExtensionmerge against the real tip).A cheaper heuristic that covers this case without an extra API call: if the non-head parent is itself a 2-parent commit whose parents include the base branch's tip, it is a synthetic PR merge ref and must not be used as
baseHash.Workaround
Switch origin PR discovery from strategyId 1 (merge with target) to strategyId 2 (the current pull request revision).
MergeWithGitSCMExtensionand the base-branch refspec are both insideif (head.isMerge())inGitHubSCMBuilder, so HEAD strategy sidesteps the failure — at the cost of no longer testing PRs against their target branch. Flattening the stack (retargeting children to the default branch) also works.