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 plotly.express as px 13*61046927SAndroid Build Coastguard Workerfrom gitlab_common import pretty_duration 14*61046927SAndroid Build Coastguard Workerfrom datetime import datetime, timedelta 15*61046927SAndroid Build Coastguard Workerfrom gitlab_common import read_token, GITLAB_URL, get_gitlab_pipeline_from_url 16*61046927SAndroid Build Coastguard Worker 17*61046927SAndroid Build Coastguard Worker 18*61046927SAndroid Build Coastguard Workerdef calculate_queued_at(job): 19*61046927SAndroid Build Coastguard Worker # we can have queued_duration without started_at when a job is canceled 20*61046927SAndroid Build Coastguard Worker if not job.queued_duration or not job.started_at: 21*61046927SAndroid Build Coastguard Worker return None 22*61046927SAndroid Build Coastguard Worker started_at = job.started_at.replace("Z", "+00:00") 23*61046927SAndroid Build Coastguard Worker return datetime.fromisoformat(started_at) - timedelta(seconds=job.queued_duration) 24*61046927SAndroid Build Coastguard Worker 25*61046927SAndroid Build Coastguard Worker 26*61046927SAndroid Build Coastguard Workerdef calculate_time_difference(time1, time2): 27*61046927SAndroid Build Coastguard Worker if not time1 or not time2: 28*61046927SAndroid Build Coastguard Worker return None 29*61046927SAndroid Build Coastguard Worker if type(time1) is str: 30*61046927SAndroid Build Coastguard Worker time1 = datetime.fromisoformat(time1.replace("Z", "+00:00")) 31*61046927SAndroid Build Coastguard Worker if type(time2) is str: 32*61046927SAndroid Build Coastguard Worker time2 = datetime.fromisoformat(time2.replace("Z", "+00:00")) 33*61046927SAndroid Build Coastguard Worker 34*61046927SAndroid Build Coastguard Worker diff = time2 - time1 35*61046927SAndroid Build Coastguard Worker return pretty_duration(diff.seconds) 36*61046927SAndroid Build Coastguard Worker 37*61046927SAndroid Build Coastguard Worker 38*61046927SAndroid Build Coastguard Workerdef create_task_name(job): 39*61046927SAndroid Build Coastguard Worker status_color = {"success": "green", "failed": "red"}.get(job.status, "grey") 40*61046927SAndroid Build Coastguard Worker return f"{job.name}\t(<span style='color: {status_color}'>{job.status}</span>,<a href='{job.web_url}'>{job.id}</a>)" 41*61046927SAndroid Build Coastguard Worker 42*61046927SAndroid Build Coastguard Worker 43*61046927SAndroid Build Coastguard Workerdef add_gantt_bar(job, tasks): 44*61046927SAndroid Build Coastguard Worker queued_at = calculate_queued_at(job) 45*61046927SAndroid Build Coastguard Worker task_name = create_task_name(job) 46*61046927SAndroid Build Coastguard Worker 47*61046927SAndroid Build Coastguard Worker tasks.append( 48*61046927SAndroid Build Coastguard Worker { 49*61046927SAndroid Build Coastguard Worker "Job": task_name, 50*61046927SAndroid Build Coastguard Worker "Start": job.created_at, 51*61046927SAndroid Build Coastguard Worker "Finish": queued_at, 52*61046927SAndroid Build Coastguard Worker "Duration": calculate_time_difference(job.created_at, queued_at), 53*61046927SAndroid Build Coastguard Worker "Phase": "Waiting dependencies", 54*61046927SAndroid Build Coastguard Worker } 55*61046927SAndroid Build Coastguard Worker ) 56*61046927SAndroid Build Coastguard Worker tasks.append( 57*61046927SAndroid Build Coastguard Worker { 58*61046927SAndroid Build Coastguard Worker "Job": task_name, 59*61046927SAndroid Build Coastguard Worker "Start": queued_at, 60*61046927SAndroid Build Coastguard Worker "Finish": job.started_at, 61*61046927SAndroid Build Coastguard Worker "Duration": calculate_time_difference(queued_at, job.started_at), 62*61046927SAndroid Build Coastguard Worker "Phase": "Queued", 63*61046927SAndroid Build Coastguard Worker } 64*61046927SAndroid Build Coastguard Worker ) 65*61046927SAndroid Build Coastguard Worker tasks.append( 66*61046927SAndroid Build Coastguard Worker { 67*61046927SAndroid Build Coastguard Worker "Job": task_name, 68*61046927SAndroid Build Coastguard Worker "Start": job.started_at, 69*61046927SAndroid Build Coastguard Worker "Finish": job.finished_at, 70*61046927SAndroid Build Coastguard Worker "Duration": calculate_time_difference(job.started_at, job.finished_at), 71*61046927SAndroid Build Coastguard Worker "Phase": "Running", 72*61046927SAndroid Build Coastguard Worker } 73*61046927SAndroid Build Coastguard Worker ) 74*61046927SAndroid Build Coastguard Worker 75*61046927SAndroid Build Coastguard Worker 76*61046927SAndroid Build Coastguard Workerdef generate_gantt_chart(pipeline): 77*61046927SAndroid Build Coastguard Worker if pipeline.yaml_errors: 78*61046927SAndroid Build Coastguard Worker raise ValueError("Pipeline YAML errors detected") 79*61046927SAndroid Build Coastguard Worker 80*61046927SAndroid Build Coastguard Worker # Convert the data into a list of dictionaries for plotly 81*61046927SAndroid Build Coastguard Worker tasks = [] 82*61046927SAndroid Build Coastguard Worker 83*61046927SAndroid Build Coastguard Worker for job in pipeline.jobs.list(all=True, include_retried=True): 84*61046927SAndroid Build Coastguard Worker add_gantt_bar(job, tasks) 85*61046927SAndroid Build Coastguard Worker 86*61046927SAndroid Build Coastguard Worker # Make it easier to see retried jobs 87*61046927SAndroid Build Coastguard Worker tasks.sort(key=lambda x: x["Job"]) 88*61046927SAndroid Build Coastguard Worker 89*61046927SAndroid Build Coastguard Worker title = f"Gantt chart of jobs in pipeline <a href='{pipeline.web_url}'>{pipeline.web_url}</a>." 90*61046927SAndroid Build Coastguard Worker title += ( 91*61046927SAndroid Build Coastguard Worker f" Total duration {str(timedelta(seconds=pipeline.duration))}" 92*61046927SAndroid Build Coastguard Worker if pipeline.duration 93*61046927SAndroid Build Coastguard Worker else "" 94*61046927SAndroid Build Coastguard Worker ) 95*61046927SAndroid Build Coastguard Worker 96*61046927SAndroid Build Coastguard Worker # Create a Gantt chart 97*61046927SAndroid Build Coastguard Worker fig = px.timeline( 98*61046927SAndroid Build Coastguard Worker tasks, 99*61046927SAndroid Build Coastguard Worker x_start="Start", 100*61046927SAndroid Build Coastguard Worker x_end="Finish", 101*61046927SAndroid Build Coastguard Worker y="Job", 102*61046927SAndroid Build Coastguard Worker color="Phase", 103*61046927SAndroid Build Coastguard Worker title=title, 104*61046927SAndroid Build Coastguard Worker hover_data=["Duration"], 105*61046927SAndroid Build Coastguard Worker ) 106*61046927SAndroid Build Coastguard Worker 107*61046927SAndroid Build Coastguard Worker # Calculate the height dynamically 108*61046927SAndroid Build Coastguard Worker fig.update_layout(height=len(tasks) * 10, yaxis_tickfont_size=14) 109*61046927SAndroid Build Coastguard Worker 110*61046927SAndroid Build Coastguard Worker # Add a deadline line to the chart 111*61046927SAndroid Build Coastguard Worker created_at = datetime.fromisoformat(pipeline.created_at.replace("Z", "+00:00")) 112*61046927SAndroid Build Coastguard Worker timeout_at = created_at + timedelta(hours=1) 113*61046927SAndroid Build Coastguard Worker fig.add_vrect( 114*61046927SAndroid Build Coastguard Worker x0=timeout_at, 115*61046927SAndroid Build Coastguard Worker x1=timeout_at, 116*61046927SAndroid Build Coastguard Worker annotation_text="1h Timeout", 117*61046927SAndroid Build Coastguard Worker fillcolor="gray", 118*61046927SAndroid Build Coastguard Worker line_width=2, 119*61046927SAndroid Build Coastguard Worker line_color="gray", 120*61046927SAndroid Build Coastguard Worker line_dash="dash", 121*61046927SAndroid Build Coastguard Worker annotation_position="top left", 122*61046927SAndroid Build Coastguard Worker annotation_textangle=90, 123*61046927SAndroid Build Coastguard Worker ) 124*61046927SAndroid Build Coastguard Worker 125*61046927SAndroid Build Coastguard Worker return fig 126*61046927SAndroid Build Coastguard Worker 127*61046927SAndroid Build Coastguard Worker 128*61046927SAndroid Build Coastguard Workerdef parse_args() -> None: 129*61046927SAndroid Build Coastguard Worker parser = argparse.ArgumentParser( 130*61046927SAndroid Build Coastguard Worker description="Generate the Gantt chart from a given pipeline." 131*61046927SAndroid Build Coastguard Worker ) 132*61046927SAndroid Build Coastguard Worker parser.add_argument("pipeline_url", type=str, help="URLs to the pipeline.") 133*61046927SAndroid Build Coastguard Worker parser.add_argument( 134*61046927SAndroid Build Coastguard Worker "-o", 135*61046927SAndroid Build Coastguard Worker "--output", 136*61046927SAndroid Build Coastguard Worker type=str, 137*61046927SAndroid Build Coastguard Worker help="Output file name. Use html ou image suffixes to choose the format.", 138*61046927SAndroid Build Coastguard Worker ) 139*61046927SAndroid Build Coastguard Worker parser.add_argument( 140*61046927SAndroid Build Coastguard Worker "--token", 141*61046927SAndroid Build Coastguard Worker metavar="token", 142*61046927SAndroid Build Coastguard Worker help="force GitLab token, otherwise it's read from ~/.config/gitlab-token", 143*61046927SAndroid Build Coastguard Worker ) 144*61046927SAndroid Build Coastguard Worker return parser.parse_args() 145*61046927SAndroid Build Coastguard Worker 146*61046927SAndroid Build Coastguard Worker 147*61046927SAndroid Build Coastguard Workerif __name__ == "__main__": 148*61046927SAndroid Build Coastguard Worker args = parse_args() 149*61046927SAndroid Build Coastguard Worker 150*61046927SAndroid Build Coastguard Worker token = read_token(args.token) 151*61046927SAndroid Build Coastguard Worker 152*61046927SAndroid Build Coastguard Worker gl = gitlab.Gitlab(url=GITLAB_URL, private_token=token, retry_transient_errors=True) 153*61046927SAndroid Build Coastguard Worker 154*61046927SAndroid Build Coastguard Worker pipeline, _ = get_gitlab_pipeline_from_url(gl, args.pipeline_url) 155*61046927SAndroid Build Coastguard Worker fig = generate_gantt_chart(pipeline) 156*61046927SAndroid Build Coastguard Worker if args.output and "htm" in args.output: 157*61046927SAndroid Build Coastguard Worker fig.write_html(args.output) 158*61046927SAndroid Build Coastguard Worker elif args.output: 159*61046927SAndroid Build Coastguard Worker fig.update_layout(width=1000) 160*61046927SAndroid Build Coastguard Worker fig.write_image(args.output) 161*61046927SAndroid Build Coastguard Worker else: 162*61046927SAndroid Build Coastguard Worker fig.show() 163