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