@@ -11,6 +11,7 @@ def __init__(self, cfg):
1111 self ._repo = cfg .repo
1212 self ._timeout = cfg .github_api_timeout
1313 self ._dry_run = cfg .dry_run
14+ self ._cfg = cfg
1415 self ._repo_root = os .getenv ("GITHUB_WORKSPACE" , os .path .abspath (os .path .join (os .path .dirname (__file__ ), ".." , ".." )))
1516 self ._headers = {
1617 "Authorization" : f"token { self ._token } " ,
@@ -48,6 +49,59 @@ def get_pr_diff(self, number):
4849 logger .error (f"PR diff fetch failed: { e } " )
4950 return ""
5051
52+ def get_compare_diff (self , base_sha , head_sha ):
53+ """Fetch the diff between two commits using the Compare API.
54+ Returns the diff text, or empty string on failure (e.g. force-push
55+ where base_sha no longer exists)."""
56+ headers = {** self ._headers , "Accept" : "application/vnd.github.v3.diff" }
57+ try :
58+ resp = requests .get (
59+ f"https://api.github.com/repos/{ self ._repo } /compare/{ base_sha } ...{ head_sha } " ,
60+ headers = headers , timeout = self ._timeout ,
61+ )
62+ if resp .status_code == 200 :
63+ return resp .text
64+ logger .warning (f"Compare API { base_sha [:7 ]} ...{ head_sha [:7 ]} : { resp .status_code } " )
65+ return ""
66+ except Exception as e :
67+ logger .error (f"Compare diff failed: { e } " )
68+ return ""
69+
70+ def get_ci_status (self , sha ):
71+ """Check commit statuses and check runs. Returns (passed, summary).
72+ passed: True (all green), False (something failed), None (pending/unknown)."""
73+ status = self ._get (f"/repos/{ self ._repo } /commits/{ sha } /status" )
74+ if status is None :
75+ return None , "CI status unavailable"
76+ combined_state = status .get ("state" , "pending" )
77+
78+ check_data = self ._get (f"/repos/{ self ._repo } /commits/{ sha } /check-runs" )
79+ runs = check_data .get ("check_runs" , []) if check_data else []
80+
81+ def _is_own_check (name ):
82+ lower = name .lower ()
83+ return "bot" in lower and ("analyze" in lower or "/ act" in lower )
84+
85+ external_runs = [r for r in runs if not _is_own_check (r .get ("name" , "" ))]
86+
87+ failed = []
88+ pending = []
89+ for r in external_runs :
90+ if r .get ("status" ) != "completed" :
91+ pending .append (r ["name" ])
92+ elif r .get ("conclusion" ) not in ("success" , "neutral" , "skipped" ):
93+ failed .append (r ["name" ])
94+
95+ if failed :
96+ return False , f"CI failing: { ', ' .join (failed )} "
97+ if pending :
98+ return None , f"CI pending: { ', ' .join (pending )} "
99+ if combined_state == "failure" :
100+ return False , "CI failing (status checks)"
101+ if combined_state == "pending" :
102+ return None , "CI pending (status checks)"
103+ return True , "CI passed"
104+
51105 def get_pr_files (self , number ):
52106 return self ._get (f"/repos/{ self ._repo } /pulls/{ number } /files" ) or []
53107
@@ -64,16 +118,18 @@ def get_pr_review_comments(self, number, max_pages=10):
64118 page += 1
65119 return comments
66120
67- def get_codebase_map (self , src_dir = "pydeequ" ):
68- """List all Python source files (excluding tests) as relative paths."""
121+ def get_codebase_map (self ):
122+ """List source files (excluding tests) as relative paths."""
123+ src_dir = self ._cfg .codebase_src_dir
124+ file_ext = self ._cfg .codebase_file_ext
69125 full_dir = os .path .join (self ._repo_root , src_dir )
70126 prefix = self ._repo_root .rstrip ("/" ) + "/"
71127 try :
72128 paths = []
73129 for root , dirs , files in os .walk (full_dir ):
74- dirs [:] = [d for d in dirs if d not in ("tests " , "__pycache__" , ".git" )]
130+ dirs [:] = [d for d in dirs if d not in ("examples " , "__pycache__" , ".git" , "tests" , "test " )]
75131 for f in files :
76- if f .endswith (".py" ):
132+ if f .endswith (file_ext ):
77133 full = os .path .join (root , f )
78134 rel = full [len (prefix ):] if full .startswith (prefix ) else full
79135 paths .append (rel )
@@ -116,9 +172,9 @@ def post_comment(self, number, body):
116172 return True
117173 return self ._post (f"/repos/{ self ._repo } /issues/{ number } /comments" , {"body" : body })
118174
119- def post_pr_review (self , number , summary , inline_comments ):
175+ def post_pr_review (self , number , summary , inline_comments , event = "COMMENT" ):
120176 if self ._dry_run :
121- logger .info (f"[DRY RUN] PR review on #{ number } : { len (inline_comments )} inline comments" )
177+ logger .info (f"[DRY RUN] PR review on #{ number } : { len (inline_comments )} inline comments, event= { event } " )
122178 return True
123179
124180 # Get valid diff lines per file from the PR
@@ -134,31 +190,34 @@ def post_pr_review(self, number, summary, inline_comments):
134190 else :
135191 invalid_comments .append (ic )
136192
193+ body = summary
194+ if invalid_comments :
195+ body += "\n \n **Additional feedback:**\n "
196+ for ic in invalid_comments :
197+ line_ref = f":{ ic ['line' ]} " if ic .get ('line' ) else ""
198+ body += f"\n `{ ic ['file' ]} { line_ref } ` — { ic ['comment' ]} \n "
199+
200+ payload = {"body" : body , "event" : event }
137201 if valid_comments :
138- body = summary
139- if invalid_comments :
140- body += "\n \n **Additional feedback:**\n "
141- for ic in invalid_comments :
142- line_ref = f":{ ic ['line' ]} " if ic .get ('line' ) else ""
143- body += f"\n `{ ic ['file' ]} { line_ref } ` — { ic ['comment' ]} \n "
144- payload = {"body" : body , "event" : "REQUEST_CHANGES" , "comments" : valid_comments }
145- try :
146- resp = requests .post (
147- f"https://api.github.com/repos/{ self ._repo } /pulls/{ number } /reviews" ,
148- headers = self ._headers , json = payload , timeout = self ._timeout ,
149- )
150- if resp .status_code in (200 , 201 ):
151- return True
152- logger .error (f"PR review API failed: { resp .status_code } , falling back to comment" )
153- except Exception as e :
154- logger .error (f"PR review API failed: { e } , falling back to comment" )
155-
156- # Fallback: post all as regular comment
157- all_comments = inline_comments
202+ payload ["comments" ] = valid_comments
203+
204+ try :
205+ resp = requests .post (
206+ f"https://api.github.com/repos/{ self ._repo } /pulls/{ number } /reviews" ,
207+ headers = self ._headers , json = payload , timeout = self ._timeout ,
208+ )
209+ if resp .status_code in (200 , 201 ):
210+ return True
211+ logger .error (f"PR review API failed: { resp .status_code } , falling back to comment" )
212+ logger .error (f"Response: { resp .text [:500 ]} " )
213+ except Exception as e :
214+ logger .error (f"PR review API failed: { e } , falling back to comment" )
215+
216+ # Fallback: post as regular comment if review API fails
158217 body = summary
159- if all_comments :
218+ if inline_comments :
160219 body += "\n \n **Inline feedback:**\n "
161- for ic in all_comments :
220+ for ic in inline_comments :
162221 line_ref = f":{ ic ['line' ]} " if ic .get ('line' ) else ""
163222 body += f"\n `{ ic ['file' ]} { line_ref } ` — { ic ['comment' ]} \n "
164223 return self ._post (f"/repos/{ self ._repo } /issues/{ number } /comments" , {"body" : body })
0 commit comments