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