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