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