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