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