xref: /optee_os/scripts/notify_maintainers.py (revision c5dcc5a1aeba164e9bb1e74dd569df0860f7e06e)
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