feat: require approval at current PR head for merge (#471)

Add merge_approval_gate assessment so gitea_merge_pr fails closed when
visible APPROVED reviews do not pin the live head SHA. Expose
approval_at_current_head and stale_approval_block_reason in review
feedback. Document re-review requirement in canonical merge workflow.

Closes #471.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-08 00:37:54 -04:00
co-authored by Claude Opus 4.8
parent a1de4ab8f9
commit 89582a0f2a
7 changed files with 184 additions and 1 deletions
+23
View File
@@ -972,6 +972,29 @@ class TestMergePR(unittest.TestCase):
self.assertIn("[REDACTED]", blob)
self.assertNotIn("abc-secret-xyz", blob)
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_merge_blocked_on_stale_approval_head(self, _auth, mock_api):
old_sha = "8b61c4b41f1b49b271ed3b99657431cf06eeda3e"
new_sha = "3e4b721d60e97147ba0704773cf57cd0d42cbe31"
mock_api.side_effect = [
{"login": "merger-bot"}, self._pr("author-bot", sha=new_sha),
self._pr("author-bot", sha=new_sha),
[_formal_review("reviewer-bot", "APPROVED", sha=old_sha)],
]
env = {"GITEA_PROFILE_NAME": "gitea-merger",
"GITEA_ALLOWED_OPERATIONS": "read,merge"}
with patch.dict(os.environ, env, clear=True):
r = gitea_merge_pr(
pr_number=8, confirmation=self._confirm(8), remote="prgs")
self.assertFalse(r["performed"])
self.assertTrue(r.get("approval_visible"))
self.assertFalse(r.get("approval_at_current_head"))
self.assertTrue(any("stale approval" in x for x in r["reasons"]))
self.assertTrue(any(old_sha in x for x in r["reasons"]))
self.assertTrue(any(new_sha in x for x in r["reasons"]))
self._assert_no_merge_call(mock_api)
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header", return_value=FAKE_AUTH)
def test_merge_blocked_without_visible_approval(self, _auth, mock_api):