From 625e2144bb70e25b25f0784a294527a318a41136 Mon Sep 17 00:00:00 2001 From: Meni Yakove Date: Sat, 20 Jun 2026 12:22:04 +0300 Subject: [PATCH 1/5] feat(cherry-pick): add @mention to PR owner in AI-resolved conflict comments When cherry-pick conflicts are resolved by AI: - Add @pr_author mention in the original PR comment - Post a new comment on the cherry-pick PR mentioning @pr_author that AI resolved conflicts and needs their review/verification Closes #1124 --- webhook_server/libs/handlers/runner_handler.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/webhook_server/libs/handlers/runner_handler.py b/webhook_server/libs/handlers/runner_handler.py index d42d1a38d..3b81aac2a 100644 --- a/webhook_server/libs/handlers/runner_handler.py +++ b/webhook_server/libs/handlers/runner_handler.py @@ -1759,10 +1759,25 @@ async def cherry_pick( f"**Cherry-pick conflicts were resolved by AI**\n\n" f"Cherry-picked PR {pull_request.title} into {target_branch}: {cherry_pick_pr_url}\n" f"Conflicts were automatically resolved by AI ({ai_provider}/{ai_model}).\n\n" - f"**Manual verification is required** — please review the changes and test before merging.", + f"@{pr_author} **Manual verification is required** — " + f"please review the changes and test before merging.", logger=self.logger, log_prefix=self.log_prefix, ) + + if cherry_pick_pr: + await github_api_call( + cherry_pick_pr.create_issue_comment, + f"**⚠️ This cherry-pick had conflicts resolved by AI ({ai_provider}/{ai_model})**\n\n" + f"@{pr_author} — AI automatically resolved merge conflicts for this cherry-pick. " + f"Please review the changes carefully and verify correctness before merging.", + logger=self.logger, + log_prefix=self.log_prefix, + ) + self.logger.info( + f"{self.log_prefix} Posted AI-conflict-resolution comment on cherry-pick" + f" PR #{cherry_pick_pr.number}" + ) else: await github_api_call( pull_request.create_issue_comment, From a516486071d48bb43dc47e66a404015071de5ff9 Mon Sep 17 00:00:00 2001 From: Meni Yakove Date: Sat, 20 Jun 2026 12:36:21 +0300 Subject: [PATCH 2/5] fix(cherry-pick): add try/except for cherry-pick PR comment and add tests - Wrap cherry-pick PR AI-conflict comment in try/except for resilience - Add 3 tests: @mention in original PR, comment on cherry-pick PR, error handling --- .../libs/handlers/runner_handler.py | 32 +++--- webhook_server/tests/test_runner_handler.py | 102 ++++++++++++++++++ 2 files changed, 122 insertions(+), 12 deletions(-) diff --git a/webhook_server/libs/handlers/runner_handler.py b/webhook_server/libs/handlers/runner_handler.py index 3b81aac2a..a574b95d5 100644 --- a/webhook_server/libs/handlers/runner_handler.py +++ b/webhook_server/libs/handlers/runner_handler.py @@ -1766,18 +1766,26 @@ async def cherry_pick( ) if cherry_pick_pr: - await github_api_call( - cherry_pick_pr.create_issue_comment, - f"**⚠️ This cherry-pick had conflicts resolved by AI ({ai_provider}/{ai_model})**\n\n" - f"@{pr_author} — AI automatically resolved merge conflicts for this cherry-pick. " - f"Please review the changes carefully and verify correctness before merging.", - logger=self.logger, - log_prefix=self.log_prefix, - ) - self.logger.info( - f"{self.log_prefix} Posted AI-conflict-resolution comment on cherry-pick" - f" PR #{cherry_pick_pr.number}" - ) + try: + await github_api_call( + cherry_pick_pr.create_issue_comment, + f"**⚠️ This cherry-pick had conflicts resolved by AI ({ai_provider}/{ai_model})**\n\n" + f"@{pr_author} — AI automatically resolved merge conflicts for this cherry-pick. " + f"Please review the changes carefully and verify correctness before merging.", + logger=self.logger, + log_prefix=self.log_prefix, + ) + self.logger.info( + f"{self.log_prefix} Posted AI-conflict-resolution comment on cherry-pick" + f" PR #{cherry_pick_pr.number}" + ) + except asyncio.CancelledError: + raise + except Exception: + self.logger.exception( + f"{self.log_prefix} Failed to post AI-conflict-resolution comment" + f" on cherry-pick PR #{cherry_pick_pr.number}" + ) else: await github_api_call( pull_request.create_issue_comment, diff --git a/webhook_server/tests/test_runner_handler.py b/webhook_server/tests/test_runner_handler.py index 1b8f693fa..0f3986fb3 100644 --- a/webhook_server/tests/test_runner_handler.py +++ b/webhook_server/tests/test_runner_handler.py @@ -2061,6 +2061,108 @@ async def run_command_side_effect(command: str, **kwargs: Any) -> tuple[bool, st mock_set_failure.assert_called() mock_ai_cli.assert_not_called() + @pytest.mark.asyncio + async def test_cherry_pick_ai_resolved_mentions_pr_author( + self, runner_handler: RunnerHandler, mock_pull_request: Mock + ) -> None: + """Test that when cherry-pick has AI-resolved conflicts, the original PR comment includes @{pr_author}.""" + runner_handler.github_webhook.ai_features = { + "ai-provider": "claude", + "ai-model": "test-model", + "resolve-cherry-pick-conflicts-with-ai": {"enabled": True}, + } + + async def run_command_side_effect(command: str, **kwargs: Any) -> tuple[bool, str, str]: + if "cherry-pick" in command and "--continue" not in command: + return (False, "", "CONFLICT (content): Merge conflict") + if "gh pr create" in command: + return (True, "https://github.com/test-org/test-repo/pull/99", "") + return (True, "success", "") + + async with self.cherry_pick_setup(runner_handler, mock_pull_request) as mocks: + mocks.run_cmd.side_effect = run_command_side_effect + with patch.object( + runner_handler, + "_resolve_cherry_pick_with_ai", + new=AsyncMock(return_value=True), + ): + await runner_handler.cherry_pick(mock_pull_request, "main") + mocks.comment.assert_called() + comment_calls = mocks.comment.call_args_list + assert any("@test-pr-author" in str(c) for c in comment_calls), ( + f"Expected @test-pr-author in comment, got: {comment_calls}" + ) + assert any("Cherry-pick conflicts were resolved by AI" in str(c) for c in comment_calls), ( + f"Expected AI resolution message in comment, got: {comment_calls}" + ) + + @pytest.mark.asyncio + async def test_cherry_pick_ai_resolved_posts_comment_on_cherry_pick_pr( + self, runner_handler: RunnerHandler, mock_pull_request: Mock + ) -> None: + """Test that a new comment is posted on the cherry-pick PR with @mention.""" + runner_handler.github_webhook.ai_features = { + "ai-provider": "claude", + "ai-model": "test-model", + "resolve-cherry-pick-conflicts-with-ai": {"enabled": True}, + } + + async def run_command_side_effect(command: str, **kwargs: Any) -> tuple[bool, str, str]: + if "cherry-pick" in command and "--continue" not in command: + return (False, "", "CONFLICT (content): Merge conflict") + if "gh pr create" in command: + return (True, "https://github.com/test-org/test-repo/pull/99", "") + return (True, "success", "") + + async with self.cherry_pick_setup(runner_handler, mock_pull_request) as mocks: + mocks.run_cmd.side_effect = run_command_side_effect + cherry_pick_pr = runner_handler.repository.get_pull.return_value + with patch.object( + runner_handler, + "_resolve_cherry_pick_with_ai", + new=AsyncMock(return_value=True), + ): + await runner_handler.cherry_pick(mock_pull_request, "main") + cherry_pick_pr.create_issue_comment.assert_called() + cp_comment_calls = cherry_pick_pr.create_issue_comment.call_args_list + assert any("@test-pr-author" in str(c) for c in cp_comment_calls), ( + f"Expected @test-pr-author in cherry-pick PR comment, got: {cp_comment_calls}" + ) + assert any("AI automatically resolved merge conflicts" in str(c) for c in cp_comment_calls), ( + f"Expected AI resolution message in cherry-pick PR comment, got: {cp_comment_calls}" + ) + + @pytest.mark.asyncio + async def test_cherry_pick_ai_resolved_comment_failure_does_not_crash( + self, runner_handler: RunnerHandler, mock_pull_request: Mock + ) -> None: + """Test that if the cherry-pick PR comment fails, the overall cherry_pick() still succeeds.""" + runner_handler.github_webhook.ai_features = { + "ai-provider": "claude", + "ai-model": "test-model", + "resolve-cherry-pick-conflicts-with-ai": {"enabled": True}, + } + + async def run_command_side_effect(command: str, **kwargs: Any) -> tuple[bool, str, str]: + if "cherry-pick" in command and "--continue" not in command: + return (False, "", "CONFLICT (content): Merge conflict") + if "gh pr create" in command: + return (True, "https://github.com/test-org/test-repo/pull/99", "") + return (True, "success", "") + + async with self.cherry_pick_setup(runner_handler, mock_pull_request) as mocks: + mocks.run_cmd.side_effect = run_command_side_effect + cherry_pick_pr = runner_handler.repository.get_pull.return_value + cherry_pick_pr.create_issue_comment = Mock(side_effect=Exception("API error")) + with patch.object( + runner_handler, + "_resolve_cherry_pick_with_ai", + new=AsyncMock(return_value=True), + ): + await runner_handler.cherry_pick(mock_pull_request, "main") + mocks.set_success.assert_called_once() + mocks.comment.assert_called() + class TestRestoreOriginalAuthorForCherryPick: """Test suite for _restore_original_author_for_cherry_pick method.""" From d6e9261c3dce4fc4fe442c733daee403f4520390 Mon Sep 17 00:00:00 2001 From: Meni Yakove Date: Sat, 20 Jun 2026 12:45:10 +0300 Subject: [PATCH 3/5] fix(cherry-pick): narrow except Exception to except GithubException --- webhook_server/libs/handlers/runner_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhook_server/libs/handlers/runner_handler.py b/webhook_server/libs/handlers/runner_handler.py index a574b95d5..48445c3c0 100644 --- a/webhook_server/libs/handlers/runner_handler.py +++ b/webhook_server/libs/handlers/runner_handler.py @@ -1781,7 +1781,7 @@ async def cherry_pick( ) except asyncio.CancelledError: raise - except Exception: + except GithubException: self.logger.exception( f"{self.log_prefix} Failed to post AI-conflict-resolution comment" f" on cherry-pick PR #{cherry_pick_pr.number}" From 9fb7a7b3938a31dae5a8a12e51e64121c54061da Mon Sep 17 00:00:00 2001 From: Meni Yakove Date: Sat, 20 Jun 2026 12:49:02 +0300 Subject: [PATCH 4/5] fix(cherry-pick): wrap original PR AI-conflict comment in try/except GithubException --- .../libs/handlers/runner_handler.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/webhook_server/libs/handlers/runner_handler.py b/webhook_server/libs/handlers/runner_handler.py index 48445c3c0..ebed67c99 100644 --- a/webhook_server/libs/handlers/runner_handler.py +++ b/webhook_server/libs/handlers/runner_handler.py @@ -1754,16 +1754,24 @@ async def cherry_pick( ai_config = self.github_webhook.ai_features ai_result = get_ai_config(ai_config) ai_provider, ai_model = ai_result if ai_result else ("unknown", "unknown") - await github_api_call( - pull_request.create_issue_comment, - f"**Cherry-pick conflicts were resolved by AI**\n\n" - f"Cherry-picked PR {pull_request.title} into {target_branch}: {cherry_pick_pr_url}\n" - f"Conflicts were automatically resolved by AI ({ai_provider}/{ai_model}).\n\n" - f"@{pr_author} **Manual verification is required** — " - f"please review the changes and test before merging.", - logger=self.logger, - log_prefix=self.log_prefix, - ) + try: + await github_api_call( + pull_request.create_issue_comment, + f"**Cherry-pick conflicts were resolved by AI**\n\n" + f"Cherry-picked PR {pull_request.title} into {target_branch}: {cherry_pick_pr_url}\n" + f"Conflicts were automatically resolved by AI ({ai_provider}/{ai_model}).\n\n" + f"@{pr_author} **Manual verification is required** — " + f"please review the changes and test before merging.", + logger=self.logger, + log_prefix=self.log_prefix, + ) + except asyncio.CancelledError: + raise + except GithubException: + self.logger.exception( + f"{self.log_prefix} Failed to post AI-conflict-resolution comment" + f" on original PR #{pull_request.number}" + ) if cherry_pick_pr: try: From c80e0ebb30f0b39b4fcfbfd61619de5eb7a1c5cf Mon Sep 17 00:00:00 2001 From: Meni Yakove Date: Sat, 20 Jun 2026 12:57:12 +0300 Subject: [PATCH 5/5] fix(tests): raise GithubException instead of Exception in AI comment failure test --- webhook_server/tests/test_runner_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhook_server/tests/test_runner_handler.py b/webhook_server/tests/test_runner_handler.py index 0f3986fb3..fcf1cdf0c 100644 --- a/webhook_server/tests/test_runner_handler.py +++ b/webhook_server/tests/test_runner_handler.py @@ -2153,7 +2153,7 @@ async def run_command_side_effect(command: str, **kwargs: Any) -> tuple[bool, st async with self.cherry_pick_setup(runner_handler, mock_pull_request) as mocks: mocks.run_cmd.side_effect = run_command_side_effect cherry_pick_pr = runner_handler.repository.get_pull.return_value - cherry_pick_pr.create_issue_comment = Mock(side_effect=Exception("API error")) + cherry_pick_pr.create_issue_comment = Mock(side_effect=GithubException(403, "API error", {})) with patch.object( runner_handler, "_resolve_cherry_pick_with_ai",