xref: /aosp_15_r20/external/mesa3d/bin/ci/ci_post_gantt.py (revision 6104692788411f58d303aa86923a9ff6ecaded22)
1*61046927SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*61046927SAndroid Build Coastguard Worker# Copyright © 2023 Collabora Ltd.
3*61046927SAndroid Build Coastguard Worker# Authors:
4*61046927SAndroid Build Coastguard Worker#   Helen Koike <[email protected]>
5*61046927SAndroid Build Coastguard Worker#
6*61046927SAndroid Build Coastguard Worker# For the dependencies, see the requirements.txt
7*61046927SAndroid Build Coastguard Worker# SPDX-License-Identifier: MIT
8*61046927SAndroid Build Coastguard Worker
9*61046927SAndroid Build Coastguard Worker
10*61046927SAndroid Build Coastguard Workerimport argparse
11*61046927SAndroid Build Coastguard Workerimport gitlab
12*61046927SAndroid Build Coastguard Workerimport re
13*61046927SAndroid Build Coastguard Workerimport os
14*61046927SAndroid Build Coastguard Workerimport pytz
15*61046927SAndroid Build Coastguard Workerimport traceback
16*61046927SAndroid Build Coastguard Workerfrom datetime import datetime, timedelta
17*61046927SAndroid Build Coastguard Workerfrom gitlab_common import (
18*61046927SAndroid Build Coastguard Worker    read_token,
19*61046927SAndroid Build Coastguard Worker    GITLAB_URL,
20*61046927SAndroid Build Coastguard Worker    get_gitlab_pipeline_from_url,
21*61046927SAndroid Build Coastguard Worker)
22*61046927SAndroid Build Coastguard Workerfrom ci_gantt_chart import generate_gantt_chart
23*61046927SAndroid Build Coastguard Worker
24*61046927SAndroid Build Coastguard WorkerMARGE_USER_ID = 9716  # Marge
25*61046927SAndroid Build Coastguard Worker
26*61046927SAndroid Build Coastguard WorkerLAST_MARGE_EVENT_FILE = os.path.expanduser("~/.config/last_marge_event")
27*61046927SAndroid Build Coastguard Worker
28*61046927SAndroid Build Coastguard Worker
29*61046927SAndroid Build Coastguard Workerdef read_last_event_date_from_file():
30*61046927SAndroid Build Coastguard Worker    try:
31*61046927SAndroid Build Coastguard Worker        with open(LAST_MARGE_EVENT_FILE, "r") as f:
32*61046927SAndroid Build Coastguard Worker            last_event_date = f.read().strip()
33*61046927SAndroid Build Coastguard Worker    except FileNotFoundError:
34*61046927SAndroid Build Coastguard Worker        # 3 days ago
35*61046927SAndroid Build Coastguard Worker        last_event_date = (datetime.now() - timedelta(days=3)).isoformat()
36*61046927SAndroid Build Coastguard Worker    return last_event_date
37*61046927SAndroid Build Coastguard Worker
38*61046927SAndroid Build Coastguard Worker
39*61046927SAndroid Build Coastguard Workerdef pretty_time(time_str):
40*61046927SAndroid Build Coastguard Worker    """Pretty print time"""
41*61046927SAndroid Build Coastguard Worker    local_timezone = datetime.now().astimezone().tzinfo
42*61046927SAndroid Build Coastguard Worker
43*61046927SAndroid Build Coastguard Worker    time_d = datetime.fromisoformat(time_str.replace("Z", "+00:00")).astimezone(
44*61046927SAndroid Build Coastguard Worker        local_timezone
45*61046927SAndroid Build Coastguard Worker    )
46*61046927SAndroid Build Coastguard Worker    return f'{time_str} ({time_d.strftime("%d %b %Y %Hh%Mm%Ss")} {local_timezone})'
47*61046927SAndroid Build Coastguard Worker
48*61046927SAndroid Build Coastguard Worker
49*61046927SAndroid Build Coastguard Workerdef compose_message(file_name, attachment_url):
50*61046927SAndroid Build Coastguard Worker    return f"""
51*61046927SAndroid Build Coastguard WorkerHere is the Gantt chart for the referred pipeline, I hope it helps �� (tip: click on the "Pan" button on the top right bar):
52*61046927SAndroid Build Coastguard Worker
53*61046927SAndroid Build Coastguard Worker[{file_name}]({attachment_url})
54*61046927SAndroid Build Coastguard Worker
55*61046927SAndroid Build Coastguard Worker<details>
56*61046927SAndroid Build Coastguard Worker<summary>more info</summary>
57*61046927SAndroid Build Coastguard Worker
58*61046927SAndroid Build Coastguard WorkerThis message was generated by the ci_post_gantt.py script, which is running on a server at Collabora.
59*61046927SAndroid Build Coastguard Worker</details>
60*61046927SAndroid Build Coastguard Worker"""
61*61046927SAndroid Build Coastguard Worker
62*61046927SAndroid Build Coastguard Worker
63*61046927SAndroid Build Coastguard Workerdef gitlab_upload_file_get_url(gl, project_id, filepath):
64*61046927SAndroid Build Coastguard Worker    project = gl.projects.get(project_id)
65*61046927SAndroid Build Coastguard Worker    uploaded_file = project.upload(filepath, filepath=filepath)
66*61046927SAndroid Build Coastguard Worker    return uploaded_file["url"]
67*61046927SAndroid Build Coastguard Worker
68*61046927SAndroid Build Coastguard Worker
69*61046927SAndroid Build Coastguard Workerdef gitlab_post_reply_to_note(gl, event, reply_message):
70*61046927SAndroid Build Coastguard Worker    """
71*61046927SAndroid Build Coastguard Worker    Post a reply to a note in thread based on a GitLab event.
72*61046927SAndroid Build Coastguard Worker
73*61046927SAndroid Build Coastguard Worker    :param gl: The GitLab connection instance.
74*61046927SAndroid Build Coastguard Worker    :param event: The event object containing the note details.
75*61046927SAndroid Build Coastguard Worker    :param reply_message: The reply message.
76*61046927SAndroid Build Coastguard Worker    """
77*61046927SAndroid Build Coastguard Worker    try:
78*61046927SAndroid Build Coastguard Worker        note_id = event.target_id
79*61046927SAndroid Build Coastguard Worker        merge_request_iid = event.note["noteable_iid"]
80*61046927SAndroid Build Coastguard Worker
81*61046927SAndroid Build Coastguard Worker        project = gl.projects.get(event.project_id)
82*61046927SAndroid Build Coastguard Worker        merge_request = project.mergerequests.get(merge_request_iid)
83*61046927SAndroid Build Coastguard Worker
84*61046927SAndroid Build Coastguard Worker        # Find the discussion to which the note belongs
85*61046927SAndroid Build Coastguard Worker        discussions = merge_request.discussions.list(iterator=True)
86*61046927SAndroid Build Coastguard Worker        target_discussion = next(
87*61046927SAndroid Build Coastguard Worker            (
88*61046927SAndroid Build Coastguard Worker                d
89*61046927SAndroid Build Coastguard Worker                for d in discussions
90*61046927SAndroid Build Coastguard Worker                if any(n["id"] == note_id for n in d.attributes["notes"])
91*61046927SAndroid Build Coastguard Worker            ),
92*61046927SAndroid Build Coastguard Worker            None,
93*61046927SAndroid Build Coastguard Worker        )
94*61046927SAndroid Build Coastguard Worker
95*61046927SAndroid Build Coastguard Worker        if target_discussion is None:
96*61046927SAndroid Build Coastguard Worker            raise ValueError("Discussion for the note not found.")
97*61046927SAndroid Build Coastguard Worker
98*61046927SAndroid Build Coastguard Worker        # Add a reply to the discussion
99*61046927SAndroid Build Coastguard Worker        reply = target_discussion.notes.create({"body": reply_message})
100*61046927SAndroid Build Coastguard Worker        return reply
101*61046927SAndroid Build Coastguard Worker
102*61046927SAndroid Build Coastguard Worker    except gitlab.exceptions.GitlabError as e:
103*61046927SAndroid Build Coastguard Worker        print(f"Failed to post a reply to '{event.note['body']}': {e}")
104*61046927SAndroid Build Coastguard Worker        return None
105*61046927SAndroid Build Coastguard Worker
106*61046927SAndroid Build Coastguard Worker
107*61046927SAndroid Build Coastguard Workerdef parse_args() -> None:
108*61046927SAndroid Build Coastguard Worker    parser = argparse.ArgumentParser(description="Monitor rejected pipelines by Marge.")
109*61046927SAndroid Build Coastguard Worker    parser.add_argument(
110*61046927SAndroid Build Coastguard Worker        "--token",
111*61046927SAndroid Build Coastguard Worker        metavar="token",
112*61046927SAndroid Build Coastguard Worker        help="force GitLab token, otherwise it's read from ~/.config/gitlab-token",
113*61046927SAndroid Build Coastguard Worker    )
114*61046927SAndroid Build Coastguard Worker    parser.add_argument(
115*61046927SAndroid Build Coastguard Worker        "--since",
116*61046927SAndroid Build Coastguard Worker        metavar="since",
117*61046927SAndroid Build Coastguard Worker        help="consider only events after this date (ISO format), otherwise it's read from ~/.config/last_marge_event",
118*61046927SAndroid Build Coastguard Worker    )
119*61046927SAndroid Build Coastguard Worker    return parser.parse_args()
120*61046927SAndroid Build Coastguard Worker
121*61046927SAndroid Build Coastguard Worker
122*61046927SAndroid Build Coastguard Workerif __name__ == "__main__":
123*61046927SAndroid Build Coastguard Worker    args = parse_args()
124*61046927SAndroid Build Coastguard Worker
125*61046927SAndroid Build Coastguard Worker    token = read_token(args.token)
126*61046927SAndroid Build Coastguard Worker
127*61046927SAndroid Build Coastguard Worker    gl = gitlab.Gitlab(url=GITLAB_URL, private_token=token, retry_transient_errors=True)
128*61046927SAndroid Build Coastguard Worker
129*61046927SAndroid Build Coastguard Worker    user = gl.users.get(MARGE_USER_ID)
130*61046927SAndroid Build Coastguard Worker    last_event_at = args.since if args.since else read_last_event_date_from_file()
131*61046927SAndroid Build Coastguard Worker
132*61046927SAndroid Build Coastguard Worker    print(f"Retrieving Marge messages since {pretty_time(last_event_at)}\n")
133*61046927SAndroid Build Coastguard Worker
134*61046927SAndroid Build Coastguard Worker    # the "after" only considers the "2023-10-24" part, it doesn't consider the time
135*61046927SAndroid Build Coastguard Worker    events = user.events.list(
136*61046927SAndroid Build Coastguard Worker        all=True,
137*61046927SAndroid Build Coastguard Worker        target_type="note",
138*61046927SAndroid Build Coastguard Worker        after=(datetime.now() - timedelta(days=3)).isoformat(),
139*61046927SAndroid Build Coastguard Worker        sort="asc",
140*61046927SAndroid Build Coastguard Worker    )
141*61046927SAndroid Build Coastguard Worker
142*61046927SAndroid Build Coastguard Worker    last_event_at_date = datetime.fromisoformat(
143*61046927SAndroid Build Coastguard Worker        last_event_at.replace("Z", "+00:00")
144*61046927SAndroid Build Coastguard Worker    ).replace(tzinfo=pytz.UTC)
145*61046927SAndroid Build Coastguard Worker
146*61046927SAndroid Build Coastguard Worker    for event in events:
147*61046927SAndroid Build Coastguard Worker        created_at_date = datetime.fromisoformat(
148*61046927SAndroid Build Coastguard Worker            event.created_at.replace("Z", "+00:00")
149*61046927SAndroid Build Coastguard Worker        ).replace(tzinfo=pytz.UTC)
150*61046927SAndroid Build Coastguard Worker        if created_at_date <= last_event_at_date:
151*61046927SAndroid Build Coastguard Worker            continue
152*61046927SAndroid Build Coastguard Worker        last_event_at = event.created_at
153*61046927SAndroid Build Coastguard Worker
154*61046927SAndroid Build Coastguard Worker        match = re.search(r"https://[^ ]+", event.note["body"])
155*61046927SAndroid Build Coastguard Worker        if match:
156*61046927SAndroid Build Coastguard Worker            try:
157*61046927SAndroid Build Coastguard Worker                print("Found message:", event.note["body"])
158*61046927SAndroid Build Coastguard Worker                pipeline_url = match.group(0)[:-1]
159*61046927SAndroid Build Coastguard Worker                pipeline, _ = get_gitlab_pipeline_from_url(gl, pipeline_url)
160*61046927SAndroid Build Coastguard Worker                print("Generating gantt chart...")
161*61046927SAndroid Build Coastguard Worker                fig = generate_gantt_chart(pipeline)
162*61046927SAndroid Build Coastguard Worker                file_name = "Gantt.html"
163*61046927SAndroid Build Coastguard Worker                fig.write_html(file_name)
164*61046927SAndroid Build Coastguard Worker                print("Uploading gantt file...")
165*61046927SAndroid Build Coastguard Worker                file_url = gitlab_upload_file_get_url(gl, event.project_id, file_name)
166*61046927SAndroid Build Coastguard Worker                print("Posting reply ...\n")
167*61046927SAndroid Build Coastguard Worker                message = compose_message(file_name, file_url)
168*61046927SAndroid Build Coastguard Worker                gitlab_post_reply_to_note(gl, event, message)
169*61046927SAndroid Build Coastguard Worker            except Exception as e:
170*61046927SAndroid Build Coastguard Worker                print(f"Failed to generate gantt chart, not posting reply.{e}")
171*61046927SAndroid Build Coastguard Worker                traceback.print_exc()
172*61046927SAndroid Build Coastguard Worker
173*61046927SAndroid Build Coastguard Worker        if not args.since:
174*61046927SAndroid Build Coastguard Worker            print(
175*61046927SAndroid Build Coastguard Worker                f"Updating last event date to {pretty_time(last_event_at)} on {LAST_MARGE_EVENT_FILE}\n"
176*61046927SAndroid Build Coastguard Worker            )
177*61046927SAndroid Build Coastguard Worker            with open(LAST_MARGE_EVENT_FILE, "w") as f:
178*61046927SAndroid Build Coastguard Worker                f.write(last_event_at)
179