Skip to content

Commit d190be8

Browse files
authored
fix(stamphog): consume trusted PR reactions
Generated-By: PostHog Code Task-Id: fa3ff167-712b-41c4-aa27-aa8ec1700f4e
1 parent 29caa24 commit d190be8

3 files changed

Lines changed: 45 additions & 64 deletions

File tree

tools/pr-approval-agent/github.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"""
77

88
import json
9-
import re
109
import subprocess
1110
from dataclasses import dataclass, field
1211
from pathlib import Path
@@ -54,8 +53,14 @@ def has_new_files(self) -> bool:
5453

5554

5655
_TRUSTED_ASSOCIATIONS = {"MEMBER", "OWNER", "COLLABORATOR"}
57-
_REVIEWHOG_BOT_LOGIN = "posthog[bot]"
58-
_REVIEWHOG_STATUS_RE = re.compile(r"<!--\s*reviewhog:status:[0-9a-f-]+\s*-->")
56+
TRUSTED_REACTOR_BOTS = {
57+
"chatgpt-codex-connector[bot]",
58+
"copilot-pull-request-reviewer[bot]",
59+
"greptile-apps[bot]",
60+
"hex-security-app[bot]",
61+
"posthog[bot]",
62+
"veria-ai[bot]",
63+
}
5964

6065

6166
def _normalize_reviews_for_prompt(reviews_raw: list[dict], head_sha: str) -> list[dict]:
@@ -89,24 +94,20 @@ def _normalize_reviews_for_prompt(reviews_raw: list[dict], head_sha: str) -> lis
8994
return normalized_reviews
9095

9196

92-
def _reviewhog_clean_reactions(comments_raw: list[dict]) -> list[dict]:
93-
for comment in reversed(comments_raw):
94-
user = comment.get("user") or {}
95-
if user.get("login", "").lower() != _REVIEWHOG_BOT_LOGIN or user.get("type") != "Bot":
96-
continue
97-
body = comment.get("body") or ""
98-
if "Found no issues worth raising, so no review was posted." not in body:
97+
def _normalize_pr_reactions(reactions_raw: list[dict], author: str) -> list[dict]:
98+
normalized = []
99+
for reaction in reactions_raw:
100+
login = (reaction.get("user") or {}).get("login", "")
101+
if login == author or login.lower() not in TRUSTED_REACTOR_BOTS:
99102
continue
100-
if _REVIEWHOG_STATUS_RE.search(body) is None:
101-
continue
102-
return [
103+
normalized.append(
103104
{
104-
"user": "reviewhog[bot]",
105-
"emoji": "👍",
106-
"created_at": comment.get("updated_at") or comment.get("created_at"),
105+
"user": login,
106+
"emoji": {"+1": "👍", "-1": "👎", "eyes": "👀"}.get(reaction.get("content"), reaction.get("content")),
107+
"created_at": reaction.get("created_at"),
107108
}
108-
]
109-
return []
109+
)
110+
return normalized
110111

111112

112113
def _gh_api(endpoint: str, *, paginate: bool = False) -> dict | list:
@@ -282,12 +283,12 @@ def fetch_pr(pr_number: int, repo: str, repo_root: Path | None = None) -> PRData
282283
base_sha = pr["base"]["sha"]
283284
head_sha = pr["head"]["sha"]
284285
check_runs_resp = _gh_api(f"repos/{repo}/commits/{head_sha}/check-runs")
285-
reviewhog_reactions: list[dict] = []
286+
pr_reactions: list[dict] = []
286287
try:
287-
discussion_raw = _gh_api(f"repos/{repo}/issues/{pr_number}/comments", paginate=True)
288-
reviewhog_reactions = _reviewhog_clean_reactions(discussion_raw)
288+
reactions_raw = _gh_api(f"repos/{repo}/issues/{pr_number}/reactions", paginate=True)
289+
pr_reactions = _normalize_pr_reactions(reactions_raw, pr["user"]["login"])
289290
except Exception as exc:
290-
print(f"warning: discussion fetch failed ({exc}); continuing without ReviewHog assurance")
291+
print(f"warning: reaction fetch failed ({exc}); continuing without reaction context")
291292

292293
git_root = repo_root or Path.cwd()
293294
ensure_commits(pr_number, head_sha, git_root)
@@ -310,7 +311,7 @@ def fetch_pr(pr_number: int, repo: str, repo_root: Path | None = None) -> PRData
310311
reviews=_normalize_reviews_for_prompt(reviews_raw, head_sha),
311312
review_comments=review_comments,
312313
check_runs=check_runs_resp.get("check_runs", []),
313-
pr_reactions=reviewhog_reactions,
314+
pr_reactions=pr_reactions,
314315
)
315316

316317

tools/pr-approval-agent/reviewer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,9 @@ def _validate_verdict(result: dict) -> dict:
174174
reviews as a concern and ESCALATE unless there's a strong,
175175
specific justification to APPROVE.
176176
- Bot comments with valid concerns that were ignored → ESCALATE
177-
- ReviewHog's authenticated clean status appears as 👍 @reviewhog[bot]. Treat
178-
it as the same mild positive signal as a clean Greptile or Hex reaction:
179-
useful corroboration, but never sufficient by itself to approve.
177+
- Trusted reviewer reactions are included. A 👍 is mild positive evidence and
178+
a 👎 is mild negative evidence; neither is sufficient by itself. An 👀 means
179+
a review is still in flight, so do not approve until it finishes.
180180
181181
Tools: You have Read, Grep, and Glob (restricted to the repo directory).
182182
All PR metadata (comments, ownership) is in the prompt — do NOT fetch

tools/pr-approval-agent/test_github.py

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from github import _normalize_reviews_for_prompt, _reviewhog_clean_reactions
5+
from github import _normalize_pr_reactions, _normalize_reviews_for_prompt
66

77

88
def test_normalize_reviews_marks_current_head_and_preserves_stale_reviews() -> None:
@@ -77,63 +77,43 @@ def test_normalize_reviews_filters_by_trust_source(
7777
assert len(normalized) == expected_count
7878

7979

80-
def test_reviewhog_clean_status_is_normalized_as_a_trusted_positive() -> None:
81-
reactions = _reviewhog_clean_reactions(
80+
def test_normalize_pr_reactions_keeps_trusted_reviewer_bots() -> None:
81+
reactions = _normalize_pr_reactions(
8282
[
8383
{
84-
"user": {"login": "posthog[bot]", "type": "Bot"},
85-
"body": (
86-
"Found no issues worth raising, so no review was posted.\n\n"
87-
"<!-- reviewhog:status:019f807c-68a6-7d18-b010-85409c5ed4ad -->"
88-
),
84+
"user": {"login": "posthog[bot]"},
85+
"content": "+1",
8986
"created_at": "2026-07-20T17:04:26Z",
90-
"updated_at": "2026-07-20T17:05:26Z",
9187
}
92-
]
88+
],
89+
"author",
9390
)
9491

9592
assert reactions == [
9693
{
97-
"user": "reviewhog[bot]",
94+
"user": "posthog[bot]",
9895
"emoji": "👍",
99-
"created_at": "2026-07-20T17:05:26Z",
96+
"created_at": "2026-07-20T17:04:26Z",
10097
}
10198
]
10299

103100

104-
def _clean_reviewhog_body() -> str:
105-
return (
106-
"Found no issues worth raising, so no review was posted.\n\n"
107-
"<!-- reviewhog:status:019f807c-68a6-7d18-b010-85409c5ed4ad -->"
108-
)
109-
110-
111101
@pytest.mark.parametrize(
112-
"login,user_type,body",
102+
"login,author",
113103
[
114-
pytest.param("posthog[bot]", "User", _clean_reviewhog_body(), id="not-app-bot"),
115-
pytest.param("someone[bot]", "Bot", _clean_reviewhog_body(), id="wrong-bot"),
116-
pytest.param(
117-
"posthog[bot]", "Bot", "Found no issues worth raising, so no review was posted.", id="missing-marker"
118-
),
119-
pytest.param(
120-
"posthog[bot]",
121-
"Bot",
122-
"Found 1 should fix.\n<!-- reviewhog:status:019f807c-68a6-7d18-b010-85409c5ed4ad -->",
123-
id="not-clean",
124-
),
104+
pytest.param("someone[bot]", "author", id="untrusted-bot"),
105+
pytest.param("posthog[bot]", "posthog[bot]", id="author-self-reaction"),
125106
],
126107
)
127-
def test_reviewhog_clean_status_rejects_untrusted_or_non_clean_comments(
128-
login: str, user_type: str, body: str
129-
) -> None:
130-
reactions = _reviewhog_clean_reactions(
108+
def test_normalize_pr_reactions_rejects_untrusted_or_author_reactions(login: str, author: str) -> None:
109+
reactions = _normalize_pr_reactions(
131110
[
132111
{
133-
"user": {"login": login, "type": user_type},
134-
"body": body,
112+
"user": {"login": login},
113+
"content": "+1",
135114
}
136-
]
115+
],
116+
author,
137117
)
138118

139119
assert reactions == []

0 commit comments

Comments
 (0)