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