xref: /optee_os/scripts/notify_maintainers.py (revision b0e5abb0dccd4fc2ba12967574cc5782359ea4bf)
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    message = ""
101    handles_to_mention = get_handles_for_pr(pr_number)
102    if not handles_to_mention:
103        print("# No maintainers or reviewers to mention.")
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 comment.user:
117                existing_handles.add(comment.user.login)
118        if existing_handles:
119            print("# Already mentioned: " +
120                  " ".join(f"@{h}" for h in existing_handles))
121
122        # Skip PR author, assignees, and requested reviewers
123        skip_handles = {pr.user.login}
124        skip_handles.update(a.login for a in pr.assignees)
125        requested_reviewers, _ = pr.get_review_requests()
126        skip_handles.update(r.login for r in requested_reviewers)
127        if skip_handles:
128            print("# Excluding author, assignees and requested reviewers: " +
129                  " ".join(f"@{h}" for h in skip_handles))
130
131        # Exclude all these from new notifications
132        new_handles = handles_to_mention - existing_handles - skip_handles
133        if new_handles:
134            message = "FYI " + " ".join(f"@{h}" for h in new_handles)
135        else:
136            print("# All relevant handles have already been mentioned "
137                  "or are already notified by GitHub.")
138
139    print(f"message={message}")
140
141
142if __name__ == "__main__":
143    main()
144