66"""
77
88import json
9+ import re
910import subprocess
10- from dataclasses import dataclass
11+ from dataclasses import dataclass , field
1112from pathlib import Path
1213
1314
@@ -29,6 +30,7 @@ class PRData:
2930 reviews : list [dict ]
3031 review_comments : list [dict ]
3132 check_runs : list [dict ]
33+ pr_reactions : list [dict ] = field (default_factory = list )
3234
3335 @property
3436 def file_paths (self ) -> list [str ]:
@@ -52,6 +54,8 @@ def has_new_files(self) -> bool:
5254
5355
5456_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*-->" )
5559
5660
5761def _normalize_reviews_for_prompt (reviews_raw : list [dict ], head_sha : str ) -> list [dict ]:
@@ -85,6 +89,26 @@ def _normalize_reviews_for_prompt(reviews_raw: list[dict], head_sha: str) -> lis
8589 return normalized_reviews
8690
8791
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 :
99+ continue
100+ if _REVIEWHOG_STATUS_RE .search (body ) is None :
101+ continue
102+ return [
103+ {
104+ "user" : "reviewhog[bot]" ,
105+ "emoji" : "👍" ,
106+ "created_at" : comment .get ("updated_at" ) or comment .get ("created_at" ),
107+ }
108+ ]
109+ return []
110+
111+
88112def _gh_api (endpoint : str , * , paginate : bool = False ) -> dict | list :
89113 cmd = ["gh" , "api" , endpoint ]
90114 if paginate :
@@ -258,6 +282,12 @@ def fetch_pr(pr_number: int, repo: str, repo_root: Path | None = None) -> PRData
258282 base_sha = pr ["base" ]["sha" ]
259283 head_sha = pr ["head" ]["sha" ]
260284 check_runs_resp = _gh_api (f"repos/{ repo } /commits/{ head_sha } /check-runs" )
285+ reviewhog_reactions : list [dict ] = []
286+ try :
287+ discussion_raw = _gh_api (f"repos/{ repo } /issues/{ pr_number } /comments" , paginate = True )
288+ reviewhog_reactions = _reviewhog_clean_reactions (discussion_raw )
289+ except Exception as exc :
290+ print (f"warning: discussion fetch failed ({ exc } ); continuing without ReviewHog assurance" )
261291
262292 git_root = repo_root or Path .cwd ()
263293 ensure_commits (pr_number , head_sha , git_root )
@@ -280,6 +310,7 @@ def fetch_pr(pr_number: int, repo: str, repo_root: Path | None = None) -> PRData
280310 reviews = _normalize_reviews_for_prompt (reviews_raw , head_sha ),
281311 review_comments = review_comments ,
282312 check_runs = check_runs_resp .get ("check_runs" , []),
313+ pr_reactions = reviewhog_reactions ,
283314 )
284315
285316
0 commit comments