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