merge-queue: direct-merge conflict-free PRs without update-churn (#2358) #2374
Reference in New Issue
Block a user
Delete Branch "fix/merge-queue-direct-merge-no-update-churn"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem (verified in prod)
The merge-queue cron does
decision=updateon any PR whose head does not contain current main →update_pull(POST/pulls/N/update). Gitea's dismiss_stale_approvals then dismisses the PR's 2 genuine approvals on that update → the PR drops to 0-genuine → needs re-review. With ~51 mergeable PRs open, this rebase-churn collapsed throughput to ~0/hr.Branch protection does not require strict up-to-date — direct merges of behind-main PRs succeed — so the update-before-merge is unnecessary churn.
Fix (small, targeted — not a merge-train rewrite)
In
evaluate_merge_readiness, reordered so the conflict-free case merges directly:/updatefirst), even if behind main. Approvals are not dismissed → no re-review churn.mergeable is Nonestays fail-closed (never merged).Merge bar is UNCHANGED
Still requires, on the current head: main required push-contexts green + ≥ required genuine approvals (not stale/dismissed, commit_id == head) + no open REQUEST_CHANGES + every BP-required status context green + mergeable + opt-out respected. Fail-closed on None/False.
force_mergestill bypasses only missing non-required governance reds, never a failing required context or an approval shortfall.Safety trade-off (please weigh for the gate decision)
With direct-merge, a behind-main PR's CI ran against its (possibly stale) base. So a semantic main-break — PR green standalone but broken when combined with newer main — is caught by post-merge main CI rather than pre-merge.
This is acceptable because:
test_process_once_pauses_when_main_not_green_no_direct_merge.If you'd prefer the stronger pre-merge guarantee, the alternative is a merge-train (serialize: update head-of-line PR, wait for its CI, merge, repeat). That is a larger change and trades throughput for pre-merge combined-CI. This PR deliberately does the small fix; requesting merge-train instead is a legitimate gate outcome.
Tests (§SOP-22), 62 green
update_pull(asserts update not invoked, merge invoked) —test_process_once_merges_conflict_free_pr_without_updatetest_process_once_behind_main_conflict_free_merges_directly,test_behind_main_but_mergeable_pr_merges_directlymergeable=False) —test_behind_main_and_not_mergeable_pr_updates,test_process_once_holds_pr_on_409_conflict_on_update,test_queue_advances_past_held_conflicted_prtest_current_base_but_not_mergeable_pr_waitstest_direct_merge_bar_unchanged_behind_main; opt-out via existing auto-discover teststest_process_once_pauses_when_main_not_green_no_direct_mergeAll pre-existing tests stay green.
🤖 Generated with Claude Code
APPROVED. Priority independent review on current head
a89e2680. Fresh origin/main merge-base diff is scoped to .gitea/scripts/gitea-merge-queue.py and .gitea/scripts/tests/test_gitea_merge_queue.py only. The merge bar is unchanged: process/evaluate still require main push-required green first, no current-head REQUEST_CHANGES, >= required_approvals distinct genuine official approvers on the current head, and every branch-protection-required PR context green before any merge decision. The only behavior change is that a behind-main but mergeable=True PR merges directly instead of calling /pulls/{n}/update; mergeable False/None still never merges, and behind-main + not-mergeable goes to update/hold path. Safety is in code, not just prose: main push-required contexts are checked before candidate processing and post-merge red main pauses subsequent ticks; force_merge remains limited to non-required reds after required green + approvals are satisfied. Tests cover direct merge, behind-main conflict-free direct merge, red-main pause, unchanged gate failures, and update-on-not-mergeable paths. Live PR is mergeable=True; latest required contexts observed green, with combined failure due non-required governance/review contexts.REQUEST_CHANGES on current head
a89e2680. The direct-merge gate itself keeps 2-genuine/current-head approvals, required PR contexts, main-green pause, and mergeable==True before merge, but there is a remaining churn/fail-closed gap in the non-mergeable path. Lines 1117-1122 state mergeable=None/missing should be fail-closed and return a wait decision, but line 1122 collapses None/missing/False to False and lines 711-712 return action=update whenever the PR is behind main. That means a behind-main PR while Gitea is still computing mergeability (mergeable=None/missing) can still call /pulls/{n}/update and dismiss genuine approvals again, which is the exact churn class this PR is meant to eliminate for unknown conflict state. Please distinguish explicit mergeable is False from None/missing: only explicit False + behind-main should update; None/missing should wait/retry without update. Also add a regression for behind-main mergeable=None/missing asserting no update_pull and no merge.New commits pushed, approval review dismissed automatically according to repository settings
APPROVED on head
c90b44fc47. Merge-base diff is scoped to .gitea/scripts/gitea-merge-queue.py and .gitea/scripts/tests/test_gitea_merge_queue.py. Verified the merge bar remains unchanged: main required contexts green first, current-head genuine approvals and no current-head REQUEST_CHANGES, PR required contexts green, and merge only on literal mergeable=True. The residual churn gap is closed: mergeable=None/missing now returns wait and never calls /update; only literal mergeable=False with a behind-main head enters the update/hold path. Direct merge still rechecks main before merge and relies on post-merge main-required CI pause backstop. Local pytest unavailable in this container, but live CI required contexts are green.APPROVED on head
c90b44fc47. Merge-base diff is scoped to .gitea/scripts/gitea-merge-queue.py and .gitea/scripts/tests/test_gitea_merge_queue.py. Verified tri-state mergeable handling: None/missing waits and never calls /update; True can direct-merge only after main green, current-head genuine approvals, no current-head REQUEST_CHANGES, and required contexts green; False remains fail-closed/update-only for behind-main definitive non-mergeable. Red main pause backstop remains present. Governance reds observed on this head are non-required/advisory relative to the queue-required bar; required contexts observed green.Re-reviewed current head
c90b44fc47. Merge-base diff is scoped to .gitea/scripts/gitea-merge-queue.py and tests. Verified the merge bar remains unchanged: main push-required contexts must be green, current-head REQUEST_CHANGES blocks, >= required genuine official approvals on the current head are required, and BP-required PR contexts must be green before any merge. Verified the direct-merge path only merges when mergeable is True. The RC gap is fixed: mergeable None/missing now returns wait and does not call /update or merge, while only explicit False can take the update/hold path. The red-main pause/backstop remains in code, and tests cover direct merge, red-main pause, mergeable=None wait/no-update, and unchanged gate failures. APPROVED.