When a readiness gate blocks the merge, gr pr merge reports the refusal clearly and then exits 0.
Reproduction
$ gr pr merge --method merge --skip-gate approval --repo synapt -y
⚠ Some PRs have issues:
- synapt PR #981: checks failing [checks]
Waive one gate with --skip-gate <approval|checks|mergeable>, or --force to waive all.
$ echo $?
0
The message is genuinely good: it names the PR, names the failing gate in brackets, and states the exact remedy. The exit code contradicts every word of it.
Impact
To any script, hook, CI step, or agent reading $?, a gate refusal and a completed merge are the same value. A caller that does the correct thing — trust the exit code rather than parse human-readable output — concludes the merge succeeded and moves on to whatever it does after merging.
This is the same root class as the sibling issue about an unmatched --repo selector: the exit code does not carry whether the intended act occurred. Different fix, same consequence.
Expected
Exit non-zero when a gate blocks the merge. Two codes would be more useful than one:
| code |
meaning |
| 0 |
the merge happened |
| 2 |
refused by a readiness gate — the PR is not ready |
| 1 |
operational failure — the tool could not complete |
Distinguishing 2 from 1 lets a caller tell your PR is not ready yet from something broke, which are different situations with different responses.
Not a complaint about the gate itself
--skip-gate <name> behaved exactly as designed in the same session: it waived only the gate named, left the others live, and printed method=Merge WAIVED=approval,checks at the moment of the irreversible act. That design is right and should not change. Only the exit status on the refusal path is wrong.
When a readiness gate blocks the merge,
gr pr mergereports the refusal clearly and then exits 0.Reproduction
The message is genuinely good: it names the PR, names the failing gate in brackets, and states the exact remedy. The exit code contradicts every word of it.
Impact
To any script, hook, CI step, or agent reading
$?, a gate refusal and a completed merge are the same value. A caller that does the correct thing — trust the exit code rather than parse human-readable output — concludes the merge succeeded and moves on to whatever it does after merging.This is the same root class as the sibling issue about an unmatched
--reposelector: the exit code does not carry whether the intended act occurred. Different fix, same consequence.Expected
Exit non-zero when a gate blocks the merge. Two codes would be more useful than one:
Distinguishing 2 from 1 lets a caller tell your PR is not ready yet from something broke, which are different situations with different responses.
Not a complaint about the gate itself
--skip-gate <name>behaved exactly as designed in the same session: it waived only the gate named, left the others live, and printedmethod=Merge WAIVED=approval,checksat the moment of the irreversible act. That design is right and should not change. Only the exit status on the refusal path is wrong.