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