diff --git a/pr_agent/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index 6331c2ef61..bae6c943b9 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -146,6 +146,14 @@ async def run_action(): elif GITHUB_EVENT_NAME == "issue_comment" or GITHUB_EVENT_NAME == "pull_request_review_comment": action = event_payload.get("action") if action in ["created", "edited"]: + # Skip comments authored by bots (including pr-agent's own + # "Preparing review..." messages), which would otherwise re-fire + # the action and be parsed as a command, causing a feedback loop. + # Mirrors the `if: github.event.sender.type != 'Bot'` workflow + # guard so users don't have to add it themselves. See issue #2398. + if event_payload.get("sender", {}).get("type") == "Bot": + get_logger().info("Skipping comment event from a bot sender to avoid a feedback loop") + return comment_body = event_payload.get("comment", {}).get("body") try: if GITHUB_EVENT_NAME == "pull_request_review_comment": diff --git a/tests/unittest/test_github_action_runner_core.py b/tests/unittest/test_github_action_runner_core.py index 5dcb5d700d..dc23a382e3 100644 --- a/tests/unittest/test_github_action_runner_core.py +++ b/tests/unittest/test_github_action_runner_core.py @@ -103,3 +103,86 @@ class FakeSuggestions(FakeTool): settings.set("GITHUB_ACTION_CONFIG", original_github_action_config) else: settings.unset("GITHUB_ACTION_CONFIG", force=True) + + +@pytest.fixture +def restore_github_settings(): + """run_action mutates global GITHUB/GITHUB_ACTION_CONFIG settings; snapshot + and restore them so these tests don't leak state into others.""" + settings = get_settings() + had_github = "GITHUB" in settings + original_github = copy.deepcopy(settings.get("GITHUB", None)) + had_cfg = "GITHUB_ACTION_CONFIG" in settings + original_cfg = copy.deepcopy(settings.get("GITHUB_ACTION_CONFIG", None)) + yield + if had_github: + settings.set("GITHUB", original_github) + else: + settings.unset("GITHUB", force=True) + if had_cfg: + settings.set("GITHUB_ACTION_CONFIG", original_cfg) + else: + settings.unset("GITHUB_ACTION_CONFIG", force=True) + + +def _write_issue_comment_event(tmp_path, sender_type): + event_path = tmp_path / "event.json" + event_path.write_text(json.dumps({ + "action": "created", + "comment": {"body": "/review", "id": 123}, + "issue": { + "pull_request": {"url": "https://api.github.com/repos/org/repo/pulls/1"}, + "url": "https://api.github.com/repos/org/repo/issues/1", + }, + "sender": {"type": sender_type}, + })) + return event_path + + +def _patch_issue_comment_deps(monkeypatch, handled): + monkeypatch.setattr(github_action_runner, "apply_repo_settings", lambda pr_url: None) + + class FakeProvider: + def __init__(self, pr_url=None): + self.pr_url = pr_url + + def add_eyes_reaction(self, comment_id, disable_eyes=False): + return None + + monkeypatch.setattr(github_action_runner, "get_git_provider", lambda: FakeProvider) + + class FakeAgent: + async def handle_request(self, url, body, notify=None): + handled.append((url, body)) + + monkeypatch.setattr(github_action_runner, "PRAgent", FakeAgent) + + +@pytest.mark.asyncio +async def test_issue_comment_from_bot_sender_is_skipped(monkeypatch, tmp_path, restore_github_settings): + """Regression for #2398: a comment authored by a bot (e.g. pr-agent's own + 'Preparing review...' message) must not be parsed as a command, which would + re-trigger the action in a feedback loop.""" + handled = [] + _patch_issue_comment_deps(monkeypatch, handled) + monkeypatch.setenv("GITHUB_EVENT_NAME", "issue_comment") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(_write_issue_comment_event(tmp_path, "Bot"))) + monkeypatch.setenv("GITHUB_TOKEN", "token") + + await github_action_runner.run_action() + + assert handled == [] # bot comment skipped; no command handled + + +@pytest.mark.asyncio +async def test_issue_comment_from_user_is_processed(monkeypatch, tmp_path, restore_github_settings): + """The bot guard must not over-skip: a human comment is still handled.""" + handled = [] + _patch_issue_comment_deps(monkeypatch, handled) + monkeypatch.setenv("GITHUB_EVENT_NAME", "issue_comment") + monkeypatch.setenv("GITHUB_EVENT_PATH", str(_write_issue_comment_event(tmp_path, "User"))) + monkeypatch.setenv("GITHUB_TOKEN", "token") + + await github_action_runner.run_action() + + assert handled == [("https://api.github.com/repos/org/repo/pulls/1", "/review")]