xref: /optee_os/scripts/notify_maintainers.py (revision 35db2aec46b9b8ae850f90ed923d80e813986446)
1bcfbef15SJerome Forissier#!/usr/bin/env python3
2bcfbef15SJerome Forissier# SPDX-License-Identifier: BSD-2-Clause
3bcfbef15SJerome Forissier#
4bcfbef15SJerome Forissier# Copyright 2025, Linaro Ltd.
5bcfbef15SJerome Forissier#
6023b04ceSJerome Forissier# Build a message to notify maintainers/reviewers for a PR. Invoked by the
7023b04ceSJerome Forissier# notify.yml workflow which posts the content of the message output by this
8023b04ceSJerome Forissier# script as a PR comment. The get_maintainer.py script is used to obtain the
9023b04ceSJerome Forissier# handles of the people responsible for the modified files. Handles already
10023b04ceSJerome Forissier# mentioned in the PR are not repeated, nor are requested reviewers, assignees
11023b04ceSJerome Forissier# and maintainers for 'THE REST'.
12023b04ceSJerome Forissier#
13023b04ceSJerome Forissier# Input: environment variables
14023b04ceSJerome Forissier#   REPO: the name of the target repository (normally: OP-TEE/optee_os)
15023b04ceSJerome Forissier#   PR_NUMBER: pull request number
16023b04ceSJerome Forissier#   GITHUB_TOKEN: authentication token with read access to PR to read comments
17023b04ceSJerome Forissier#
18023b04ceSJerome Forissier# Output: multiple lines of text in the following format
19023b04ceSJerome Forissier#   # Some information
20023b04ceSJerome Forissier#   # Some other information
21023b04ceSJerome Forissier#   message=FYI @handle1 @handle2...
22bcfbef15SJerome Forissier
23bcfbef15SJerome Forissierimport os
24bcfbef15SJerome Forissierimport subprocess
25bcfbef15SJerome Forissierimport re
26bcfbef15SJerome Forissierfrom github import Github
27e258d9a5SJerome Forissierfrom github import Auth
28bcfbef15SJerome Forissier
29bcfbef15SJerome Forissier
30bcfbef15SJerome Forissierdef parse_get_maintainer_output(output: str):
31bcfbef15SJerome Forissier    """Parse get_maintainer.py output and return GitHub handles to notify.
32bcfbef15SJerome Forissier
33bcfbef15SJerome Forissier    All entries are parsed, but handles listed for 'THE REST' are removed
34bcfbef15SJerome Forissier    from the final notification set.
35bcfbef15SJerome Forissier    """
36bcfbef15SJerome Forissier    handles = set()
37bcfbef15SJerome Forissier    the_rest_handles = set()
38bcfbef15SJerome Forissier
39bcfbef15SJerome Forissier    for line in output.splitlines():
40bcfbef15SJerome Forissier        handle_start = line.find("[@")
41bcfbef15SJerome Forissier        handle_end = line.find("]", handle_start)
42bcfbef15SJerome Forissier        if handle_start == -1 or handle_end == -1:
43bcfbef15SJerome Forissier            continue
44bcfbef15SJerome Forissier        handle = line[handle_start + 2:handle_end].strip()
45bcfbef15SJerome Forissier
46bcfbef15SJerome Forissier        paren_start = line.find("(", handle_end)
47bcfbef15SJerome Forissier        paren_end = line.rfind(")")
48bcfbef15SJerome Forissier        target = None
49bcfbef15SJerome Forissier        if paren_start != -1 and paren_end != -1:
50bcfbef15SJerome Forissier            content = line[paren_start + 1:paren_end].strip()
51bcfbef15SJerome Forissier            if ":" in content:
52bcfbef15SJerome Forissier                _, target = content.split(":", 1)
53bcfbef15SJerome Forissier                target = target.strip()
54bcfbef15SJerome Forissier
55bcfbef15SJerome Forissier        if target and target.upper() == "THE REST":
56bcfbef15SJerome Forissier            the_rest_handles.add(handle)
57bcfbef15SJerome Forissier        else:
58bcfbef15SJerome Forissier            handles.add(handle)
59bcfbef15SJerome Forissier
60bcfbef15SJerome Forissier    allh = set()
61bcfbef15SJerome Forissier    allh.update(handles)
62bcfbef15SJerome Forissier    allh.update(the_rest_handles)
63bcfbef15SJerome Forissier
64bcfbef15SJerome Forissier    if allh:
65023b04ceSJerome Forissier        print("# For information: all relevant maintainers/reviewers: " +
66bcfbef15SJerome Forissier              " ".join(f"@{h}" for h in allh))
67bcfbef15SJerome Forissier    if handles:
68023b04ceSJerome Forissier        print("# Subsystem/platform maintainers/reviewers: " +
69bcfbef15SJerome Forissier              " ".join(f"@{h}" for h in handles))
70bcfbef15SJerome Forissier    if the_rest_handles:
71023b04ceSJerome Forissier        print("# Excluding handles from THE REST: " +
72bcfbef15SJerome Forissier              " ".join(f"@{h}" for h in the_rest_handles))
73bcfbef15SJerome Forissier
74bcfbef15SJerome Forissier    # Remove any handle that was marked as THE REST
75bcfbef15SJerome Forissier    handles_to_mention = handles - the_rest_handles
76bcfbef15SJerome Forissier    return handles_to_mention
77bcfbef15SJerome Forissier
78bcfbef15SJerome Forissier
79bcfbef15SJerome Forissierdef get_handles_for_pr(pr_number: str):
80bcfbef15SJerome Forissier    """Run get_maintainer.py with -g PR_NUMBER and parse handles."""
81bcfbef15SJerome Forissier    cmd = [
82bcfbef15SJerome Forissier        os.path.join(os.getcwd(), "scripts/get_maintainer.py"),
83bcfbef15SJerome Forissier        "-g", pr_number
84bcfbef15SJerome Forissier    ]
85bcfbef15SJerome Forissier    output = subprocess.check_output(cmd, text=True)
86bcfbef15SJerome Forissier    return parse_get_maintainer_output(output)
87bcfbef15SJerome Forissier
88bcfbef15SJerome Forissier
89bcfbef15SJerome Forissierdef main():
90023b04ceSJerome Forissier    github_env = all(os.getenv(var) for var in ("REPO", "PR_NUMBER",
91023b04ceSJerome Forissier                                                "GITHUB_TOKEN"))
92023b04ceSJerome Forissier    if not github_env:
93023b04ceSJerome Forissier        print('This script must be run in GitHub Actions')
94023b04ceSJerome Forissier        return
95bcfbef15SJerome Forissier
96bcfbef15SJerome Forissier    repo_name = os.getenv("REPO")
97023b04ceSJerome Forissier    pr_number = os.getenv("PR_NUMBER")
98bcfbef15SJerome Forissier    token = os.getenv("GITHUB_TOKEN")
99bcfbef15SJerome Forissier
100*35db2aecSJerome Forissier    message = ""
101bcfbef15SJerome Forissier    handles_to_mention = get_handles_for_pr(pr_number)
102bcfbef15SJerome Forissier    if not handles_to_mention:
103023b04ceSJerome Forissier        print("# No maintainers or reviewers to mention.")
104bcfbef15SJerome Forissier    else:
105023b04ceSJerome Forissier        print("# Final list of subsystem/platform maintainers/reviewers: " +
106bcfbef15SJerome Forissier              " ".join(f"@{h}" for h in handles_to_mention))
107bcfbef15SJerome Forissier
108023b04ceSJerome Forissier        g = Github(token)
109bcfbef15SJerome Forissier        repo = g.get_repo(repo_name)
110bcfbef15SJerome Forissier        pr = repo.get_pull(int(pr_number))
111bcfbef15SJerome Forissier
112bcfbef15SJerome Forissier        # Gather existing handles mentioned in previous comments
113bcfbef15SJerome Forissier        existing_handles = set()
114bcfbef15SJerome Forissier        for comment in pr.get_issue_comments():
1152b891b87SJerome Forissier            existing_handles.update(re.findall(r"@([\w-]+)", comment.body))
116bcfbef15SJerome Forissier        if existing_handles:
117023b04ceSJerome Forissier            print("# Already mentioned: " +
118528a70a4SJerome Forissier                  " ".join(f"@{h}" for h in existing_handles))
119bcfbef15SJerome Forissier
120bcfbef15SJerome Forissier        # Skip PR author, assignees, and requested reviewers
121bcfbef15SJerome Forissier        skip_handles = {pr.user.login}
122bcfbef15SJerome Forissier        skip_handles.update(a.login for a in pr.assignees)
123bcfbef15SJerome Forissier        requested_reviewers, _ = pr.get_review_requests()
124bcfbef15SJerome Forissier        skip_handles.update(r.login for r in requested_reviewers)
125bcfbef15SJerome Forissier        if skip_handles:
126023b04ceSJerome Forissier            print("# Excluding author, assignees and requested reviewers: " +
127bcfbef15SJerome Forissier                  " ".join(f"@{h}" for h in skip_handles))
128bcfbef15SJerome Forissier
129bcfbef15SJerome Forissier        # Exclude all these from new notifications
130bcfbef15SJerome Forissier        new_handles = handles_to_mention - existing_handles - skip_handles
131*35db2aecSJerome Forissier        if new_handles:
132*35db2aecSJerome Forissier            message = "FYI " + " ".join(f"@{h}" for h in new_handles)
133*35db2aecSJerome Forissier        else:
134023b04ceSJerome Forissier            print("# All relevant handles have already been mentioned "
135bcfbef15SJerome Forissier                  "or are already notified by GitHub.")
136bcfbef15SJerome Forissier
137023b04ceSJerome Forissier    print(f"message={message}")
138bcfbef15SJerome Forissier
139bcfbef15SJerome Forissier
140bcfbef15SJerome Forissierif __name__ == "__main__":
141bcfbef15SJerome Forissier    main()
142