44Fetches all issue comments via gh api, then extracts:
55- last_reviewed_sha: the SHA from the <!-- review-state: ... --> marker
66- review_mode: "incremental" when a GitHub API compare diff is available, otherwise "full"
7- - All comments (for dedup of existing findings )
7+ - Trusted owner/member/collaborator comments (for human review context )
88
99Writes structured JSON to .github/pr-context.json.
1010"""
2424
2525# Bot logins that post review comments via GitHub Actions.
2626BOT_LOGINS = {"github-actions[bot]" , "github-actions" }
27+ TRUSTED_COMMENT_ASSOCIATIONS = {"OWNER" , "MEMBER" , "COLLABORATOR" }
2728DEFAULT_REVIEW_SUMMARY_HEADING = "### Connector PR Review:"
2829LEGACY_REVIEW_SUMMARY_HEADING = "### PR Review:"
2930DEFAULT_API_ATTEMPTS = 3
@@ -140,7 +141,7 @@ def parse_paginated_json(output: str) -> list[dict]:
140141
141142
142143def fetch_compare_diff (head_repo : str , base_sha : str , head_sha : str ) -> Optional [str ]:
143- """Fetch a compare diff from the PR head repo without checking out PR code ."""
144+ """Fetch a compare diff from the PR head repo for incremental review ."""
144145 endpoint = f"repos/{ head_repo } /compare/{ base_sha } ...{ head_sha } "
145146 try :
146147 metadata = gh_api ([endpoint ])
@@ -164,9 +165,24 @@ def fetch_compare_diff(head_repo: str, base_sha: str, head_sha: str) -> Optional
164165 return result .stdout
165166
166167
168+ def current_checkout_sha () -> Optional [str ]:
169+ """Return the current git checkout SHA, if the workspace is a git repo."""
170+ try :
171+ result = subprocess .run (
172+ ["git" , "rev-parse" , "HEAD" ],
173+ capture_output = True ,
174+ text = True ,
175+ check = True ,
176+ )
177+ except subprocess .CalledProcessError :
178+ return None
179+ return result .stdout .strip ()
180+
181+
167182def main ():
168183 repo = os .environ .get ("GITHUB_REPOSITORY" , "" )
169184 pr_number = os .environ .get ("PR_NUMBER" , "" )
185+ expected_head_sha = os .environ .get ("PR_HEAD_SHA" , "" ).strip ()
170186 workflow_ref = os .environ .get ("GITHUB_WORKFLOW_REF" , "" )
171187 run_id = os .environ .get ("GITHUB_RUN_ID" , "" )
172188 server_url = os .environ .get ("GITHUB_SERVER_URL" , "https://github.com" ).rstrip ("/" )
@@ -198,18 +214,32 @@ def main():
198214 raise
199215 print (f"Found { len (raw_comments )} comments" )
200216
201- # Extract comment summaries
202- comments = []
217+ # Keep bot review comments for authoritative state, but only expose trusted
218+ # owner/member/collaborator human comments to the review prompt. Public repo
219+ # comments from contributors or random users are untrusted prompt input.
220+ state_comments = []
221+ trusted_context_comments = []
203222 for c in raw_comments :
204- comments .append ({
223+ author_association = c .get ("author_association" , "NONE" )
224+ user = c .get ("user" ) or {}
225+ comment = {
205226 "id" : c ["id" ],
206- "user" : c .get ("user" , {}).get ("login" , "unknown" ),
227+ "user" : user .get ("login" , "unknown" ),
228+ "user_type" : user .get ("type" , "unknown" ),
229+ "author_association" : author_association ,
207230 "body" : c .get ("body" , "" ),
208- })
231+ }
232+ state_comments .append (comment )
233+ if user .get ("type" ) == "User" and author_association in TRUSTED_COMMENT_ASSOCIATIONS :
234+ trusted_context_comments .append (comment )
235+
236+ ignored_count = len (state_comments ) - len (trusted_context_comments )
237+ print (f"Trusted review-context comments: { len (trusted_context_comments )} " )
238+ print (f"Ignored untrusted or bot comments for prompt context: { ignored_count } " )
209239
210240 # Only bot-authored review comments are authoritative state. User-authored
211241 # markers are untrusted PR content and must not influence review mode.
212- review_comments = [c for c in comments if is_bot_review_comment (c , summary_heading )]
242+ review_comments = [c for c in state_comments if is_bot_review_comment (c , summary_heading )]
213243
214244 # Extract state from the newest bot review comment owned by this workflow.
215245 # If only legacy markerless comments exist, reuse the newest one so the first
@@ -246,15 +276,37 @@ def main():
246276 pr_endpoint = f"repos/{ repo } /pulls/{ pr_number } "
247277 pr_result = gh_api ([pr_endpoint ])
248278 pr = json .loads (pr_result .stdout )
249- current_sha = pr ["head" ]["sha" ]
279+ live_head_sha = pr ["head" ]["sha" ]
280+ if expected_head_sha and live_head_sha != expected_head_sha :
281+ print (
282+ f"PR head changed before review started: event={ expected_head_sha } , live={ live_head_sha } " ,
283+ file = sys .stderr ,
284+ )
285+ sys .exit (1 )
286+
287+ checkout_sha = current_checkout_sha ()
288+ if expected_head_sha and checkout_sha != expected_head_sha :
289+ print (
290+ f"Checkout SHA does not match event PR head: checkout={ checkout_sha } , event={ expected_head_sha } " ,
291+ file = sys .stderr ,
292+ )
293+ sys .exit (1 )
294+ if not expected_head_sha and checkout_sha and checkout_sha != live_head_sha :
295+ print (
296+ f"Checkout SHA does not match live PR head: checkout={ checkout_sha } , live={ live_head_sha } " ,
297+ file = sys .stderr ,
298+ )
299+ sys .exit (1 )
300+
301+ current_sha = expected_head_sha or live_head_sha
250302 current_base_sha = pr ["base" ]["sha" ]
251303 head_repo = (pr ["head" ].get ("repo" ) or {}).get ("full_name" )
252304 print (f"Current PR head: { current_sha [:12 ]} " )
253305 print (f"Current PR base: { current_base_sha [:12 ]} " )
254306
255- # This action intentionally does not check out PR head code under
256- # pull_request_target. Use GitHub-provided diffs instead of relying on
257- # local git history from untrusted code .
307+ # Review runs only for same-repo PRs with PR head checked out. GitHub
308+ # compare diffs are used only to select incremental/full review mode and to
309+ # provide a compact incremental artifact .
258310 review_mode = "full"
259311 incremental_diff_path = None
260312 if not last_reviewed_sha :
@@ -279,8 +331,9 @@ def main():
279331 last_reviewed_sha = None
280332
281333 # Collect existing findings from bot review comments to help with dedup.
282- # Human comments remain available as context, but they are not authoritative
283- # review state and cannot suppress findings by mimicking the summary format.
334+ # Trusted human comments remain available as context, but they are not
335+ # authoritative review state and cannot suppress findings by mimicking the
336+ # summary format.
284337 existing_findings = []
285338 for c in review_comments :
286339 body = c ["body" ]
@@ -303,7 +356,7 @@ def main():
303356 "summary_comment_id" : summary_comment_id ,
304357 "incremental_diff_path" : incremental_diff_path ,
305358 "existing_findings" : existing_findings ,
306- "comments" : comments ,
359+ "comments" : trusted_context_comments ,
307360 }
308361
309362 output_path = os .path .join (".github" , "pr-context.json" )
0 commit comments