77from datetime import datetime , timezone
88from pathlib import Path
99
10+ from .automation import (
11+ find_open_pr_for_branch_status as automation_find_open_pr_for_branch_status ,
12+ )
1013from .config import AssignmentAttempt
1114from .guidance import get_issue_guidance , get_pr_guidance
1215
@@ -110,13 +113,22 @@ def _apply_assignment_side_effects(
110113 return assignment_attempt , failure_comment
111114
112115
116+ def _current_assignees_or_error (bot , issue_number : int ) -> tuple [list [str ] | None , str | None ]:
117+ current_assignees = bot .get_issue_assignees (issue_number )
118+ if current_assignees is None :
119+ return None , "❌ Unable to determine current assignees/reviewers from GitHub; refusing to continue."
120+ return current_assignees , None
121+
122+
113123def handle_pass_command (bot , state : dict , issue_number : int , comment_author : str , reason : str | None ) -> tuple [str , bool ]:
114124 issue_data = bot .ensure_review_entry (state , issue_number , create = True )
115125 if issue_data is None :
116126 return "❌ Unable to load review state." , False
117127 passed_reviewer = issue_data .get ("current_reviewer" )
118128 if not passed_reviewer :
119- current_assignees = bot .get_issue_assignees (issue_number )
129+ current_assignees , assignee_error = _current_assignees_or_error (bot , issue_number )
130+ if assignee_error :
131+ return assignee_error , False
120132 passed_reviewer = current_assignees [0 ] if current_assignees else None
121133 if not passed_reviewer :
122134 return "❌ No reviewer is currently assigned to pass." , False
@@ -195,7 +207,9 @@ def handle_pass_until_command(bot, state: dict, issue_number: int, comment_autho
195207 issue_data = state ["active_reviews" ][issue_key ]
196208 if isinstance (issue_data , dict ):
197209 tracked_reviewer = issue_data .get ("current_reviewer" )
198- current_assignees = bot .get_issue_assignees (issue_number )
210+ current_assignees , assignee_error = _current_assignees_or_error (bot , issue_number )
211+ if assignee_error :
212+ return assignee_error , False
199213 is_current_reviewer = ((tracked_reviewer and tracked_reviewer .lower () == comment_author .lower ()) or comment_author .lower () in [a .lower () for a in current_assignees ])
200214 reassigned_msg = ""
201215 if is_current_reviewer :
@@ -321,16 +335,10 @@ def get_default_branch(bot) -> str:
321335
322336
323337def find_open_pr_for_branch (bot , branch : str ) -> dict | None :
324- owner = os .environ .get ("REPO_OWNER" , "" ).strip ()
325- branch = branch .strip ()
326- if not owner or not branch :
338+ status , pr = automation_find_open_pr_for_branch_status (bot , branch )
339+ if status != "found" :
327340 return None
328- response = bot .github_api ("GET" , f"pulls?state=open&head={ owner } :{ branch } " )
329- if isinstance (response , list ) and response :
330- first = response [0 ]
331- if isinstance (first , dict ):
332- return first
333- return None
341+ return pr
334342
335343
336344def resolve_workflow_run_pr_number (bot ) -> int :
@@ -351,9 +359,10 @@ def resolve_workflow_run_pr_number(bot) -> int:
351359 raise RuntimeError ("Missing WORKFLOW_RUN_HEAD_SHA for workflow_run reconcile" )
352360 if reconcile_head_sha != workflow_run_head_sha :
353361 raise RuntimeError ("Workflow_run reconcile context SHA mismatch between artifact and workflow payload" )
354- pull_request = bot .github_api ("GET" , f"pulls/{ pr_number } " )
355- if not isinstance (pull_request , dict ):
362+ response = bot .github_api_request ("GET" , f"pulls/{ pr_number } " , retry_policy = "idempotent_read " )
363+ if not response . ok or not isinstance (response . payload , dict ):
356364 raise RuntimeError (f"Failed to fetch pull request #{ pr_number } during workflow_run reconcile" )
365+ pull_request = response .payload
357366 head = pull_request .get ("head" )
358367 pull_request_head_sha = ""
359368 if isinstance (head , dict ):
@@ -369,9 +378,11 @@ def resolve_workflow_run_pr_number(bot) -> int:
369378
370379
371380def create_pull_request (bot , branch : str , base : str , issue_number : int ) -> dict | None :
372- existing = bot . find_open_pr_for_branch ( branch )
373- if existing :
381+ lookup_status , existing = automation_find_open_pr_for_branch_status ( bot , branch )
382+ if lookup_status == "found" :
374383 return existing
384+ if lookup_status == "unavailable" :
385+ raise RuntimeError (f"Unable to determine whether branch '{ branch } ' already has an open PR" )
375386 title = "chore: update spec.lock (no guideline impact)"
376387 body = "Updates `src/spec.lock` after confirming the audit reported no affected guidelines.\n \n " f"Closes #{ issue_number } "
377388 response = bot .github_api ("POST" , "pulls" , {"title" : title , "head" : branch , "base" : base , "body" : body })
@@ -386,7 +397,10 @@ def handle_accept_no_fls_changes_command(bot, issue_number: int, comment_author:
386397 labels = bot .parse_issue_labels ()
387398 if bot .FLS_AUDIT_LABEL not in labels :
388399 return "❌ This command is only available on issues labeled `fls-audit`." , False
389- if not bot .check_user_permission (comment_author , "triage" ):
400+ permission_status = bot .get_user_permission_status (comment_author , "triage" )
401+ if permission_status == "unavailable" :
402+ return "❌ Unable to verify triage permissions right now; refusing to run this command." , False
403+ if permission_status != "granted" :
390404 return "❌ You must have triage permissions to run this command." , False
391405 repo_root = get_target_repo_root ()
392406 if bot .list_changed_files (repo_root ):
@@ -474,7 +488,9 @@ def handle_claim_command(bot, state: dict, issue_number: int, comment_author: st
474488 return (f"❌ @{ comment_author } is not in the reviewer queue. Only Producers can claim reviews." ), False
475489 if is_away :
476490 return (f"❌ @{ comment_author } is currently marked as away. Please use `{ bot .BOT_MENTION } /away YYYY-MM-DD` to update your return date first, or wait until your scheduled return." ), False
477- current_assignees = bot .get_issue_assignees (issue_number )
491+ current_assignees , assignee_error = _current_assignees_or_error (bot , issue_number )
492+ if assignee_error :
493+ return assignee_error , False
478494 for assignee in current_assignees :
479495 bot .unassign_reviewer (issue_number , assignee )
480496 issue_author = os .environ .get ("ISSUE_AUTHOR" , "" )
@@ -503,7 +519,10 @@ def handle_release_command(bot, state: dict, issue_number: int, comment_author:
503519 target_username = args [0 ].lstrip ("@" )
504520 reason = " " .join (args [1 :]) if len (args ) > 1 else None
505521 releasing_other = target_username .lower () != comment_author .lower ()
506- if releasing_other and not bot .check_user_permission (comment_author , "triage" ):
522+ permission_status = bot .get_user_permission_status (comment_author , "triage" )
523+ if permission_status == "unavailable" :
524+ return "❌ Unable to verify triage permissions right now; refusing to continue." , False
525+ if releasing_other and permission_status != "granted" :
507526 return (f"❌ @{ comment_author } does not have permission to release other reviewers. Triage access or higher is required." ), False
508527 else :
509528 target_username = comment_author
@@ -516,7 +535,9 @@ def handle_release_command(bot, state: dict, issue_number: int, comment_author:
516535 if isinstance (issue_data , dict ):
517536 tracked_reviewer = issue_data .get ("current_reviewer" )
518537 assignment_method = issue_data .get ("assignment_method" )
519- current_assignees = bot .get_issue_assignees (issue_number )
538+ current_assignees , assignee_error = _current_assignees_or_error (bot , issue_number )
539+ if assignee_error :
540+ return assignee_error , False
520541 is_tracked = tracked_reviewer and tracked_reviewer .lower () == target_username .lower ()
521542 is_assigned = target_username .lower () in [assignee .lower () for assignee in current_assignees ]
522543 if not is_tracked and not is_assigned :
@@ -555,7 +576,9 @@ def handle_assign_command(bot, state: dict, issue_number: int, username: str) ->
555576 if entry ["github" ].lower () == username .lower ():
556577 return_date = entry .get ("return_date" , "unknown" )
557578 return (f"⚠️ @{ username } is currently marked as away until { return_date } . Consider assigning someone else or waiting." ), False
558- current_assignees = bot .get_issue_assignees (issue_number )
579+ current_assignees , assignee_error = _current_assignees_or_error (bot , issue_number )
580+ if assignee_error :
581+ return assignee_error , False
559582 for assignee in current_assignees :
560583 bot .unassign_reviewer (issue_number , assignee )
561584 issue_author = os .environ .get ("ISSUE_AUTHOR" , "" )
@@ -577,7 +600,9 @@ def handle_assign_command(bot, state: dict, issue_number: int, username: str) ->
577600
578601
579602def handle_assign_from_queue_command (bot , state : dict , issue_number : int ) -> tuple [str , bool ]:
580- current_assignees = bot .get_issue_assignees (issue_number )
603+ current_assignees , assignee_error = _current_assignees_or_error (bot , issue_number )
604+ if assignee_error :
605+ return assignee_error , False
581606 for assignee in current_assignees :
582607 bot .unassign_reviewer (issue_number , assignee )
583608 issue_author = os .environ .get ("ISSUE_AUTHOR" , "" )
0 commit comments