fix(pr): resolve the PR base from the request and the platform, not the stored target - #917
Merged
Conversation
…ring the merge verify_merge_commit_parents collapsed two different states onto None: an unreadable repository, which is deliberately silent so a missing local checkout cannot invent an outage, and a READABLE repository whose base ref cannot be read. The caller prints nothing for None, so the second state reported 'I could not look' in the exact shape of 'I looked and it was fine' -- clearance in the one direction the check cannot fail. Only the second state changes. The unreadable-repository case keeps its silence and its existing test still passes. Witness: an_absent_base_ref_in_a_readable_repository_is_reported, which carries a control asserting the same fixture DOES report a finding for a ref that exists -- without it, a guard broken into always returning None would pass by accident.
… target run_pr_create resolves the base once at the top of the loop, honoring --base. The pre-flight existence check then re-derived it from the manifest's stored target, so the two disagreed on exactly the runs where --base was passed -- and --base is what we pass BECAUSE the stored target is stale. Measured symptom: with a retired sprint branch still stored, 'gr pr create --base dev' asked GitHub whether that retired branch existed, got 404 for a branch nobody named, and skipped the repo reporting a base the operator never asked for. The create call one block below was already passing the resolved base, so the only thing between the operator and a correct PR was a guard checking a different branch. Class swept, not just the cited instance: every remaining target_branch() in this file is either the resolution expression itself or sits inside the base_override.is_none() branch, where reading the stored target is correct. Witnesses (tests/test_pr_create_base.rs, run_pr_create end to end against wiremock, asserting the server's received-request log): - base_override_is_the_branch_that_gets_checked_and_the_pr_that_gets_opened fails on the old code with exactly the reported message, and carries a control requiring that SOME pre-flight check happened, so deleting the check could not pass it. - without_an_override_the_stored_target_is_still_what_gets_checked is the negative control; without it, 'prefer the override' and 'ignore the manifest' would be indistinguishable. run_pr_create had no test coverage before this.
… stored target PRToMerge.base was set from repo.target_branch(), the workspace's stored target. The platform's answer was already in hand and discarded: the get_pull_request call two blocks above returns a PullRequest whose .base carries the branch the PR is genuinely open against, and only .mergeable was read off it. All four adapters populate base. The two values differ exactly when the stored target is stale, which is the ordinary state after a branch is retired. That wrong base then flowed into verify_merge_commit_parents -- the post-merge assertion added for the squash incident -- so it read a ref unrelated to the merge that just happened, and read nothing at all once the stored target named a deleted branch. Paired with the previous commit, which makes an unreadable base ref say so, the assertion now both looks at the right ref and reports when it cannot. The stored target stays as the fallback for a failed API call, and an empty platform answer falls back too rather than building refs/remotes/origin/. WITNESS SCOPE, stated rather than implied: base_binding_tests pins the resolution helper, NOT the call site. Measured, not assumed -- reverting only the call-site bind while leaving the helper intact fails no test in this suite. The call site's protection is structural, that repo.target_branch() no longer appears there as a standalone expression. An end-to-end witness would need the parent-assertion warning to be observable to a test; today it only reaches stdout.
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three commits on one seam: where
grgets the base branch of a pull request. Both defects were found during a release train.What was wrong
gr pr create --base <branch>resolved the base correctly and then asked the platform about a different branch. The base is computed once, honoring--base; the pre-flight existence check re-derived it from the manifest's stored target. The two disagree only when--basewas passed, which is exactly when the stored target is stale, because passing--baseis what we do because it is stale. With a retired branch still stored, the command asked whether that retired branch existed, got a 404 for a branch nobody named, and skipped the repo reporting a base the operator never asked for. The create call one block below was already passing the resolved base, so the only thing between the operator and a correct PR was a guard checking a different branch.gr pr mergebound its notion of the base to that same stored target. The platform's answer was already in hand and discarded:get_pull_requestis called two blocks above for the mergeable flag and returns aPullRequestwhose.basecarries the branch the PR is genuinely open against. All four adapters populate it.That wrong base then flowed into
verify_merge_commit_parents, the post-merge assertion that a requested merge produced a two-parent commit. It readrefs/remotes/origin/<base>through a bare?, so a stored target naming a deleted branch producedNone, and the caller prints nothing forNone. "I looked and it was fine" and "I could not look" were the same observation.What changed
target_branch()in that file is either the resolution expression itself or sits inside thebase_override.is_none()branch, where reading the stored target is correct.refs/remotes/origin/.Witnesses, and what each one pins
an_absent_base_ref_in_a_readable_repository_is_reported4 passed; 1 failedbase_override_is_the_branch_that_gets_checked_and_the_pr_that_gets_openedrun_pr_createend to end, asserting the mock server's received-request log1 passed; 1 failedwithout_an_override_the_stored_target_is_still_what_gets_checkedbase_binding_tests(3 cases)2 passed; 1 failedEach mutation kills only its own witness; the other module stays green. The first witness also carries a control requiring that some pre-flight check happened, so deleting the check could not pass it.
run_pr_createhad no test coverage before this.Scope stated rather than implied. The base-binding witnesses pin the helper, not the call site. Measured, not assumed: reverting only the call-site bind while leaving the helper intact passes the entire 743-test lib suite. The call site's protection is structural, that
repo.target_branch()no longer appears there as a standalone expression. An end-to-end witness would need the parent-assertion warning to be observable to a test; today it only reaches stdout.Deliberately not folded in.
run_pr_createreturnsOk(())after printing "Failed to create N PR(s)" — confirmed by the new test, which saw the command report failure and still return ok. That is the success-bound-to-control-flow class already tracked in #886 and #885; it changes exit semantics for scripts and deserves its own decision rather than riding along here.Verification
cargo test --all: 1188 passed, 0 failedcargo clippy --all-targets --all-features: exit 0, no errors. Warning count 153 to 155; the entire delta is one kind,function mock_branch_exists is never used, which is the pattern every other helper in that shared test module already producescargo fmt --all --check: cleanPremium boundary: grip is OSS because this is local workspace orchestration — resolving which branch a PR targets, and verifying the shape of the merge commit that results. No identity, no org state, no entitlement check.
Related: #886 and #885 for the exit-code class, which this deliberately leaves open.