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