Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions pr_agent/servers/github_action_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
83 changes: 83 additions & 0 deletions tests/unittest/test_github_action_runner_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Loading