1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-2-Clause 3# 4# Copyright 2025, Linaro Ltd. 5# 6 7import os 8import subprocess 9import argparse 10import re 11from github import Github 12from github import Auth 13 14 15def parse_get_maintainer_output(output: str): 16 """Parse get_maintainer.py output and return GitHub handles to notify. 17 18 All entries are parsed, but handles listed for 'THE REST' are removed 19 from the final notification set. 20 """ 21 handles = set() 22 the_rest_handles = set() 23 24 for line in output.splitlines(): 25 handle_start = line.find("[@") 26 handle_end = line.find("]", handle_start) 27 if handle_start == -1 or handle_end == -1: 28 continue 29 handle = line[handle_start + 2:handle_end].strip() 30 31 paren_start = line.find("(", handle_end) 32 paren_end = line.rfind(")") 33 target = None 34 if paren_start != -1 and paren_end != -1: 35 content = line[paren_start + 1:paren_end].strip() 36 if ":" in content: 37 _, target = content.split(":", 1) 38 target = target.strip() 39 40 if target and target.upper() == "THE REST": 41 the_rest_handles.add(handle) 42 else: 43 handles.add(handle) 44 45 allh = set() 46 allh.update(handles) 47 allh.update(the_rest_handles) 48 49 if allh: 50 print("For information: all relevant maintainers/reviewers: " + 51 " ".join(f"@{h}" for h in allh)) 52 if handles: 53 print("Subsystem/platform maintainers/reviewers: " + 54 " ".join(f"@{h}" for h in handles)) 55 if the_rest_handles: 56 print("Excluding handles from THE REST: " + 57 " ".join(f"@{h}" for h in the_rest_handles)) 58 59 # Remove any handle that was marked as THE REST 60 handles_to_mention = handles - the_rest_handles 61 return handles_to_mention 62 63 64def get_handles_for_pr(pr_number: str): 65 """Run get_maintainer.py with -g PR_NUMBER and parse handles.""" 66 cmd = [ 67 os.path.join(os.getcwd(), "scripts/get_maintainer.py"), 68 "-g", pr_number 69 ] 70 output = subprocess.check_output(cmd, text=True) 71 return parse_get_maintainer_output(output) 72 73 74def main(): 75 parser = argparse.ArgumentParser( 76 description=( 77 "Notify maintainers/reviewers for a PR using " 78 "get_maintainer.py.\n" 79 "If run in a GitHub environment (GITHUB_TOKEN and REPO set), " 80 "the notification message is posted directly to the PR. " 81 "Otherwise, the script outputs the message to the console.\n" 82 "The message lists the GitHub handles of all reviewers and " 83 "maintainers responsible for the modified files. Handles " 84 "already mentioned in the PR are not repeated, nor are " 85 "requested reviewers, assignees and maintainers for 'THE REST' " 86 ) 87 ) 88 parser.add_argument( 89 "--pr", 90 help="GitHub PR number (required outside GitHub environment)." 91 ) 92 args = parser.parse_args() 93 94 github_env = all(os.getenv(var) for var in ("GITHUB_TOKEN", "REPO")) 95 96 if github_env: 97 pr_number = args.pr or os.getenv("PR_NUMBER") 98 repo_name = os.getenv("REPO") 99 token = os.getenv("GITHUB_TOKEN") 100 if not pr_number: 101 print( 102 "Running in GitHub environment but no PR_NUMBER found. " 103 "Doing nothing." 104 ) 105 return 106 else: 107 pr_number = args.pr 108 if not pr_number: 109 print("Error: --pr must be provided outside GitHub environment.") 110 return 111 repo_name = None 112 token = None 113 114 handles_to_mention = get_handles_for_pr(pr_number) 115 if not handles_to_mention: 116 print("No maintainers or reviewers to mention.") 117 return 118 else: 119 print("Final list of subsystem/platform maintainers/reviewers: " + 120 " ".join(f"@{h}" for h in handles_to_mention)) 121 122 if github_env: 123 auth = Auth.Token(token) 124 g = Github(auth=auth) 125 repo = g.get_repo(repo_name) 126 pr = repo.get_pull(int(pr_number)) 127 128 # Gather existing handles mentioned in previous comments 129 existing_handles = set() 130 for comment in pr.get_issue_comments(): 131 existing_handles.update(re.findall(r"@([\w-]+)", comment.body)) 132 if existing_handles: 133 print("Already mentioned: " + 134 " ".join(f"@{h}" for h in existing_handles)) 135 136 # Skip PR author, assignees, and requested reviewers 137 skip_handles = {pr.user.login} 138 skip_handles.update(a.login for a in pr.assignees) 139 requested_reviewers, _ = pr.get_review_requests() 140 skip_handles.update(r.login for r in requested_reviewers) 141 if skip_handles: 142 print("Excluding author, assignees and requested reviewers: " + 143 " ".join(f"@{h}" for h in skip_handles)) 144 145 # Exclude all these from new notifications 146 new_handles = handles_to_mention - existing_handles - skip_handles 147 if not new_handles: 148 print("All relevant handles have already been mentioned " 149 "or are already notified by GitHub.") 150 return 151 152 message = ("FYI " + " ".join(f"@{h}" for h in new_handles)) 153 print(f"Comment to add to PR: '{message}'") 154 pr.create_issue_comment(message) 155 print(f"Comment added") 156 else: 157 message = ("FYI " + " ".join(f"@{h}" for h in handles_to_mention)) 158 print(f"Comment added to PR would be: '{message}'") 159 160 161if __name__ == "__main__": 162 main() 163