1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-2-Clause 3# 4# Copyright 2025, Linaro Ltd. 5# 6# Build a message to notify maintainers/reviewers for a PR. Invoked by the 7# notify.yml workflow which posts the content of the message output by this 8# script as a PR comment. The get_maintainer.py script is used to obtain the 9# handles of the people responsible for the modified files. Handles already 10# mentioned in the PR are not repeated, nor are requested reviewers, assignees 11# and maintainers for 'THE REST'. 12# 13# Input: environment variables 14# REPO: the name of the target repository (normally: OP-TEE/optee_os) 15# PR_NUMBER: pull request number 16# GITHUB_TOKEN: authentication token with read access to PR to read comments 17# 18# Output: multiple lines of text in the following format 19# # Some information 20# # Some other information 21# message=FYI @handle1 @handle2... 22 23import os 24import subprocess 25import re 26from github import Github 27from github import Auth 28 29 30def parse_get_maintainer_output(output: str): 31 """Parse get_maintainer.py output and return GitHub handles to notify. 32 33 All entries are parsed, but handles listed for 'THE REST' are removed 34 from the final notification set. 35 """ 36 handles = set() 37 the_rest_handles = set() 38 39 for line in output.splitlines(): 40 handle_start = line.find("[@") 41 handle_end = line.find("]", handle_start) 42 if handle_start == -1 or handle_end == -1: 43 continue 44 handle = line[handle_start + 2:handle_end].strip() 45 46 paren_start = line.find("(", handle_end) 47 paren_end = line.rfind(")") 48 target = None 49 if paren_start != -1 and paren_end != -1: 50 content = line[paren_start + 1:paren_end].strip() 51 if ":" in content: 52 _, target = content.split(":", 1) 53 target = target.strip() 54 55 if target and target.upper() == "THE REST": 56 the_rest_handles.add(handle) 57 else: 58 handles.add(handle) 59 60 allh = set() 61 allh.update(handles) 62 allh.update(the_rest_handles) 63 64 if allh: 65 print("# For information: all relevant maintainers/reviewers: " + 66 " ".join(f"@{h}" for h in allh)) 67 if handles: 68 print("# Subsystem/platform maintainers/reviewers: " + 69 " ".join(f"@{h}" for h in handles)) 70 if the_rest_handles: 71 print("# Excluding handles from THE REST: " + 72 " ".join(f"@{h}" for h in the_rest_handles)) 73 74 # Remove any handle that was marked as THE REST 75 handles_to_mention = handles - the_rest_handles 76 return handles_to_mention 77 78 79def get_handles_for_pr(pr_number: str): 80 """Run get_maintainer.py with -g PR_NUMBER and parse handles.""" 81 cmd = [ 82 os.path.join(os.getcwd(), "scripts/get_maintainer.py"), 83 "-g", pr_number 84 ] 85 output = subprocess.check_output(cmd, text=True) 86 return parse_get_maintainer_output(output) 87 88 89def main(): 90 github_env = all(os.getenv(var) for var in ("REPO", "PR_NUMBER", 91 "GITHUB_TOKEN")) 92 if not github_env: 93 print('This script must be run in GitHub Actions') 94 return 95 96 repo_name = os.getenv("REPO") 97 pr_number = os.getenv("PR_NUMBER") 98 token = os.getenv("GITHUB_TOKEN") 99 100 handles_to_mention = get_handles_for_pr(pr_number) 101 if not handles_to_mention: 102 print("# No maintainers or reviewers to mention.") 103 message = "" 104 else: 105 print("# Final list of subsystem/platform maintainers/reviewers: " + 106 " ".join(f"@{h}" for h in handles_to_mention)) 107 108 g = Github(token) 109 repo = g.get_repo(repo_name) 110 pr = repo.get_pull(int(pr_number)) 111 112 # Gather existing handles mentioned in previous comments 113 existing_handles = set() 114 for comment in pr.get_issue_comments(): 115 existing_handles.update(re.findall(r"@([\w-]+)", comment.body)) 116 if existing_handles: 117 print("# Already mentioned: " + 118 " ".join(f"@{h}" for h in existing_handles)) 119 120 # Skip PR author, assignees, and requested reviewers 121 skip_handles = {pr.user.login} 122 skip_handles.update(a.login for a in pr.assignees) 123 requested_reviewers, _ = pr.get_review_requests() 124 skip_handles.update(r.login for r in requested_reviewers) 125 if skip_handles: 126 print("# Excluding author, assignees and requested reviewers: " + 127 " ".join(f"@{h}" for h in skip_handles)) 128 129 # Exclude all these from new notifications 130 new_handles = handles_to_mention - existing_handles - skip_handles 131 if not new_handles: 132 print("# All relevant handles have already been mentioned " 133 "or are already notified by GitHub.") 134 135 message = "FYI " + " ".join(f"@{h}" for h in new_handles) 136 137 print(f"message={message}") 138 139 140if __name__ == "__main__": 141 main() 142