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