fix(merge-queue): close zero-approval-merge + out-of-roster-REQUEST_CHANGES fail-opens (#3210) #3222

Merged
devops-engineer merged 1 commits from fix/merge-gate-failopen-3210 into main 2026-06-24 08:52:09 +00:00
4 changed files with 261 additions and 7 deletions
+19 -3
View File
@@ -212,6 +212,20 @@ def classify_reviews(
# cannot overwrite or erase a genuine review. A user's verdict is the
# state of their latest VALID (official, current-head, non-dismissed,
# non-stale, commit_id-present-and-matching) review.
#
# REVIEWER_SET DECOUPLING (internal#3210 FIX-2, fail-open).
#
# The `reviewer_set` membership filter must apply ONLY to the APPROVER
# tally — it is the "recognised reviewers count toward the N-genuine
# floor" roster. It must NOT gate the REQUEST_CHANGES (block) set: an
# official, current-head, non-stale, non-dismissed REQUEST_CHANGES from
# ANY login (e.g. the CTO/founder or any non-roster reviewer) MUST block
# the merge regardless of roster membership. The earlier code applied the
# `user not in reviewer_set_set: continue` filter in this reduce loop,
# BEFORE the verdict was known, so an out-of-roster REQUEST_CHANGES was
# silently dropped and did not block — a fail-open. We therefore reduce
# over ALL users here (no roster gate) and apply the roster check only at
# the `approvers.add(...)` step below.
latest_valid_by_user: dict = {}
for review in reviews:
if not isinstance(review, dict):
@@ -219,8 +233,6 @@ def classify_reviews(
user = (review.get("user") or {}).get("login")
if not isinstance(user, str):
continue
if reviewer_set_set is not None and user not in reviewer_set_set:
continue
# EXACT-ENUM (fail-closed): exact constants only, no coercion. A
# case-coerced row must not become eligible to overwrite/erase a
# genuine per-user verdict in the reduce below.
@@ -240,7 +252,11 @@ def classify_reviews(
# Each surviving review already passed is_official_current_head, so
# the state alone determines the verdict. We still go through the
# per-verdict SSOT predicates so the rule cannot drift.
if is_genuine_approval(review, headsha=headsha, reviewer_set=None):
#
# APPROVERS: gated on reviewer_set membership (recognised-reviewer
# roster). REQUEST_CHANGES: NEVER gated — any official current-head
# block counts, regardless of roster (internal#3210 FIX-2).
if is_genuine_approval(review, headsha=headsha, reviewer_set=reviewer_set_set):
approvers.add(user)
elif is_open_request_changes(review, headsha=headsha):
request_changes.append(user)
+37 -4
View File
@@ -713,10 +713,29 @@ def parse_branch_protection(body: Any) -> BranchProtection:
raise BranchProtectionUnavailable(
"enable_status_check is true but status_check_contexts is empty"
)
# FAIL-CLOSED approval floor (internal#3210 FIX-1).
#
# `required_approvals` is the genuine-approval bar consumed by
# evaluate_merge_readiness step 3. A degraded BP value here would silently
# weaken or zero that gate, so we clamp it UP to the default floor and never
# accept a value that lowers/skips it:
#
# - bool: `isinstance(True, int)` is True in Python, so an upstream bool
# True would coerce to int 1 and HALVE a 2-genuine bar to 1. Reject bool
# explicitly → fall back to the default floor.
# - 0 / negative: an admin lowering the bar, a Gitea migration/default
# reset, or a blanked/restored BP record can yield 0 (or a negative that
# passes a naive `< N` check). These must clamp UP to the default, NEVER
# down/skip.
# - valid positive int >= default: honored as-is (a stricter BP wins).
#
# max(int(approvals), REQUIRED_APPROVALS_DEFAULT) gives exactly this: the
# derived bar is never below the SSOT default floor.
approvals = body.get("required_approvals")
required_approvals = (
int(approvals) if isinstance(approvals, int) else REQUIRED_APPROVALS_DEFAULT
)
if isinstance(approvals, bool) or not isinstance(approvals, int):
required_approvals = REQUIRED_APPROVALS_DEFAULT
else:
required_approvals = max(approvals, REQUIRED_APPROVALS_DEFAULT)
return BranchProtection(
required_contexts=contexts,
required_approvals=required_approvals,
@@ -1420,7 +1439,21 @@ def evaluate_merge_readiness(
# bar, which a bot cannot satisfy by construction. Required
# approvals is forced to 0 for the check below when the exemption
# holds.
effective_required_approvals = 0 if runtime_bump_exempt else required_approvals
#
# DEFENSIVE FLOOR (internal#3210 FIX-1, defence-in-depth). On the
# NON-exempt path the bar can never legitimately be below 1 genuine
# approval — a single upstream `required_approvals == 0` (degraded BP,
# a caller that bypassed parse_branch_protection, a future refactor)
# must NOT be able to zero the genuine-approval gate. parse_branch_
# protection already clamps the derived value up to the default floor;
# this is the second, independent guard at the consumption site so the
# two would both have to fail to open the gate. The runtime-bump
# exemption path is UNTOUCHED — it legitimately zeroes the HUMAN bar
# (a bot cannot self-approve), and the floor must not break it.
if runtime_bump_exempt:
effective_required_approvals = 0
else:
effective_required_approvals = max(required_approvals, 1)
if len(approvers) < effective_required_approvals:
return MergeDecision(
False, "wait",
@@ -327,6 +327,72 @@ class ClassifyReviewsContract(unittest.TestCase):
)
self.assertEqual(approvers, {"alice"})
# -----------------------------------------------------------------
# FIX-2 (internal#3210, HIGH fail-open): reviewer_set DECOUPLING.
#
# The reviewer_set roster gates ONLY the approver tally. An official,
# current-head, non-stale, non-dismissed REQUEST_CHANGES from a login
# OUTSIDE the roster (e.g. the CTO/founder, or any non-roster reviewer)
# MUST still block the merge. The earlier code applied the roster
# membership filter in the reduce loop BEFORE the verdict was known, so
# an out-of-roster REQUEST_CHANGES was silently dropped and did not
# block — exactly the fail-open these tests pin shut.
# -----------------------------------------------------------------
def test_out_of_roster_request_changes_still_blocks(self):
"""An out-of-roster official current-head REQUEST_CHANGES MUST block,
while in-roster approvals are still tallied normally."""
reviews = [
_review(user="alice", state="APPROVED", commit_id=HEAD),
_review(user="bob", state="APPROVED", commit_id=HEAD),
# CTO/founder is NOT in the reviewer roster, but their
# REQUEST_CHANGES must not evaporate.
_review(user="cto-founder", state="REQUEST_CHANGES", commit_id=HEAD),
]
approvers, request_changes = classify_reviews(
reviews, headsha=HEAD, reviewer_set={"alice", "bob"}
)
# Roster approvals are counted (out-of-roster gate unchanged for them).
self.assertEqual(approvers, {"alice", "bob"})
# The out-of-roster block is honored.
self.assertIn("cto-founder", request_changes)
def test_out_of_roster_approval_still_excluded(self):
"""The roster gate is unchanged for APPROVERS: an out-of-roster
APPROVED still does NOT count toward the genuine-approval floor."""
reviews = [
_review(user="alice", state="APPROVED", commit_id=HEAD),
_review(user="outsider", state="APPROVED", commit_id=HEAD),
]
approvers, request_changes = classify_reviews(
reviews, headsha=HEAD, reviewer_set={"alice"}
)
self.assertEqual(approvers, {"alice"})
self.assertEqual(request_changes, [])
def test_out_of_roster_request_changes_blocks_with_no_roster_approvals(self):
"""Even with zero in-roster approvers, an out-of-roster
REQUEST_CHANGES is surfaced (the merge-queue step-2 gate then waits)."""
reviews = [
_review(user="random-non-roster", state="REQUEST_CHANGES", commit_id=HEAD),
]
approvers, request_changes = classify_reviews(
reviews, headsha=HEAD, reviewer_set={"alice", "bob"}
)
self.assertEqual(approvers, set())
self.assertEqual(request_changes, ["random-non-roster"])
def test_out_of_roster_stale_request_changes_does_not_block(self):
"""The decouple does NOT weaken the fail-closed head/stale contract:
an out-of-roster REQUEST_CHANGES on a PRIOR head must NOT block."""
reviews = [
_review(user="cto-founder", state="REQUEST_CHANGES", commit_id=OTHER_HEAD),
]
_, request_changes = classify_reviews(
reviews, headsha=HEAD, reviewer_set={"alice"}
)
self.assertEqual(request_changes, [])
def test_latest_review_per_user_wins(self):
# alice's REQUEST_CHANGES (latest) supersedes her earlier APPROVED.
reviews = [
@@ -317,6 +317,37 @@ def test_genuine_approvals_latest_review_supersedes_earlier():
assert rc == ["agent-reviewer-cr2"]
def test_genuine_approvals_out_of_roster_request_changes_still_blocks():
"""FIX-2 (internal#3210): an official current-head REQUEST_CHANGES from a
login OUTSIDE REVIEWER_SET (e.g. the CTO/founder) must be surfaced in the
request_changes list so the merge is blocked — while roster approvals are
still tallied. The earlier reviewer_set filter dropped it silently."""
reviews = [
{"state": "APPROVED", "user": {"login": "agent-researcher"},
"official": True, "stale": False, "dismissed": False, "commit_id": "HEAD"},
{"state": "APPROVED", "user": {"login": "agent-reviewer-cr2"},
"official": True, "stale": False, "dismissed": False, "commit_id": "HEAD"},
# CTO/founder — NOT in REVIEWER_SET — requests changes on current head.
{"state": "REQUEST_CHANGES", "user": {"login": "hongming-cto"},
"official": True, "stale": False, "dismissed": False, "commit_id": "HEAD"},
]
approvers, rc = mq.genuine_approvals(reviews, headsha="HEAD", reviewer_set=REVIEWERS)
# Roster approvals are still counted (roster gate unchanged for approvers).
assert approvers == {"agent-researcher", "agent-reviewer-cr2"}
# The out-of-roster block is honored.
assert "hongming-cto" in rc
# End-to-end: that block flows into evaluate_merge_readiness and WAITS,
# even though the 2-genuine approval floor is otherwise satisfied.
decision = mq.evaluate_merge_readiness(
**_ready_kwargs(approvers=approvers, request_changes=rc, required_approvals=2)
)
assert decision.ready is False
assert decision.action == "wait"
assert "REQUEST_CHANGES" in decision.reason
assert "hongming-cto" in decision.reason
def test_merge_blocked_when_open_request_changes_on_current_head():
decision = mq.evaluate_merge_readiness(
**_ready_kwargs(request_changes=["agent-reviewer-cr2"])
@@ -566,6 +597,114 @@ def test_parse_branch_protection_fail_closed_on_non_object():
mq.parse_branch_protection(None)
# --------------------------------------------------------------------------
# FIX-1 (internal#3210, CRITICAL fail-open): required_approvals FLOOR.
#
# A degraded `required_approvals` from branch protection — 0 (admin lowered
# it / migration reset / blanked-restored BP), a negative (passes a naive
# `< N`), or a bool True (isinstance(True, int) is True → coerces to 1,
# HALVING a 2-genuine bar) — must NEVER weaken or skip the genuine-approval
# gate. parse_branch_protection clamps UP to REQUIRED_APPROVALS_DEFAULT;
# evaluate_merge_readiness applies an independent floor of 1 on the
# non-exempt path. These tests pin both layers.
# --------------------------------------------------------------------------
def _bp_body(required_approvals):
return {
"enable_status_check": True,
"status_check_contexts": ["CI / all-required (pull_request)"],
"required_approvals": required_approvals,
"block_on_rejected_reviews": True,
}
def test_parse_branch_protection_clamps_zero_up_to_default():
"""required_approvals: 0 from BP must clamp UP to the default floor (2),
never zero/skip the genuine-approval gate."""
bp = mq.parse_branch_protection(_bp_body(0))
assert bp.required_approvals == mq.REQUIRED_APPROVALS_DEFAULT
assert bp.required_approvals >= 1
def test_parse_branch_protection_clamps_negative_up_to_default():
"""A negative required_approvals (would pass a naive `len < N`) must
clamp UP to the default floor, never below it."""
bp = mq.parse_branch_protection(_bp_body(-1))
assert bp.required_approvals == mq.REQUIRED_APPROVALS_DEFAULT
def test_parse_branch_protection_rejects_bool_true_uses_default():
"""bool True is NOT a valid approval count. isinstance(True, int) is True
in Python, so the naive int() path would coerce True->1 and HALVE a
2-genuine bar. It must be rejected and fall back to the default floor."""
bp = mq.parse_branch_protection(_bp_body(True))
assert bp.required_approvals == mq.REQUIRED_APPROVALS_DEFAULT
# Defensive: a False would also be rejected (treated as invalid).
bp_false = mq.parse_branch_protection(_bp_body(False))
assert bp_false.required_approvals == mq.REQUIRED_APPROVALS_DEFAULT
def test_parse_branch_protection_honors_stricter_value():
"""A BP value ABOVE the default floor is honored as-is (stricter wins)."""
bp = mq.parse_branch_protection(_bp_body(3))
assert bp.required_approvals == 3
def test_parse_branch_protection_missing_approvals_uses_default():
"""No required_approvals key at all → the default floor, not zero."""
body = _bp_body(0)
del body["required_approvals"]
bp = mq.parse_branch_protection(body)
assert bp.required_approvals == mq.REQUIRED_APPROVALS_DEFAULT
def test_evaluate_merge_readiness_floors_zero_required_approvals_nonexempt():
"""Defence-in-depth: even if a degraded required_approvals=0 reaches
evaluate_merge_readiness on the NON-exempt path, the genuine-approval
gate is NOT skipped — a PR with zero approvers must still WAIT."""
decision = mq.evaluate_merge_readiness(
**_ready_kwargs(
approvers=set(),
required_approvals=0,
# runtime_bump_exempt omitted → defaults to False (non-exempt)
)
)
assert decision.ready is False
assert decision.action == "wait"
assert "insufficient genuine approvals" in decision.reason
# The floor is reported as need >= 1, never 0.
assert "need 1" in decision.reason
def test_evaluate_merge_readiness_floors_negative_required_approvals_nonexempt():
"""A negative required_approvals must not pass a naive `len(approvers) < N`
check — the floor forces the gate to require at least 1."""
decision = mq.evaluate_merge_readiness(
**_ready_kwargs(
approvers=set(),
required_approvals=-5,
)
)
assert decision.ready is False
assert decision.action == "wait"
assert "insufficient genuine approvals" in decision.reason
def test_evaluate_merge_readiness_floor_does_not_break_runtime_bump_exemption():
"""The non-exempt floor must NOT touch the runtime-bump exemption path,
which legitimately zeroes the HUMAN approval bar (a bot cannot
self-approve). With exempt=True + 0 approvers the PR still merges."""
decision = mq.evaluate_merge_readiness(
**_ready_kwargs(
approvers=set(),
required_approvals=0,
runtime_bump_exempt=True,
)
)
assert decision.ready is True
assert decision.action == "merge"
# --------------------------------------------------------------------------
# Fix 2: HOL — a permanent merge error HOLDS the PR (applies HOLD_LABEL)
# --------------------------------------------------------------------------