#!/usr/bin/env python3 # Copyright © 2023 Collabora Ltd. # Authors: # Helen Koike # # For the dependencies, see the requirements.txt # SPDX-License-Identifier: MIT import argparse import gitlab import plotly.express as px from gitlab_common import pretty_duration from datetime import datetime, timedelta from gitlab_common import read_token, GITLAB_URL, get_gitlab_pipeline_from_url def calculate_queued_at(job): # we can have queued_duration without started_at when a job is canceled if not job.queued_duration or not job.started_at: return None started_at = job.started_at.replace("Z", "+00:00") return datetime.fromisoformat(started_at) - timedelta(seconds=job.queued_duration) def calculate_time_difference(time1, time2): if not time1 or not time2: return None if type(time1) is str: time1 = datetime.fromisoformat(time1.replace("Z", "+00:00")) if type(time2) is str: time2 = datetime.fromisoformat(time2.replace("Z", "+00:00")) diff = time2 - time1 return pretty_duration(diff.seconds) def create_task_name(job): status_color = {"success": "green", "failed": "red"}.get(job.status, "grey") return f"{job.name}\t({job.status},{job.id})" def add_gantt_bar(job, tasks): queued_at = calculate_queued_at(job) task_name = create_task_name(job) tasks.append( { "Job": task_name, "Start": job.created_at, "Finish": queued_at, "Duration": calculate_time_difference(job.created_at, queued_at), "Phase": "Waiting dependencies", } ) tasks.append( { "Job": task_name, "Start": queued_at, "Finish": job.started_at, "Duration": calculate_time_difference(queued_at, job.started_at), "Phase": "Queued", } ) tasks.append( { "Job": task_name, "Start": job.started_at, "Finish": job.finished_at, "Duration": calculate_time_difference(job.started_at, job.finished_at), "Phase": "Running", } ) def generate_gantt_chart(pipeline): if pipeline.yaml_errors: raise ValueError("Pipeline YAML errors detected") # Convert the data into a list of dictionaries for plotly tasks = [] for job in pipeline.jobs.list(all=True, include_retried=True): add_gantt_bar(job, tasks) # Make it easier to see retried jobs tasks.sort(key=lambda x: x["Job"]) title = f"Gantt chart of jobs in pipeline {pipeline.web_url}." title += ( f" Total duration {str(timedelta(seconds=pipeline.duration))}" if pipeline.duration else "" ) # Create a Gantt chart fig = px.timeline( tasks, x_start="Start", x_end="Finish", y="Job", color="Phase", title=title, hover_data=["Duration"], ) # Calculate the height dynamically fig.update_layout(height=len(tasks) * 10, yaxis_tickfont_size=14) # Add a deadline line to the chart created_at = datetime.fromisoformat(pipeline.created_at.replace("Z", "+00:00")) timeout_at = created_at + timedelta(hours=1) fig.add_vrect( x0=timeout_at, x1=timeout_at, annotation_text="1h Timeout", fillcolor="gray", line_width=2, line_color="gray", line_dash="dash", annotation_position="top left", annotation_textangle=90, ) return fig def parse_args() -> None: parser = argparse.ArgumentParser( description="Generate the Gantt chart from a given pipeline." ) parser.add_argument("pipeline_url", type=str, help="URLs to the pipeline.") parser.add_argument( "-o", "--output", type=str, help="Output file name. Use html ou image suffixes to choose the format.", ) parser.add_argument( "--token", metavar="token", help="force GitLab token, otherwise it's read from ~/.config/gitlab-token", ) return parser.parse_args() if __name__ == "__main__": args = parse_args() token = read_token(args.token) gl = gitlab.Gitlab(url=GITLAB_URL, private_token=token, retry_transient_errors=True) pipeline, _ = get_gitlab_pipeline_from_url(gl, args.pipeline_url) fig = generate_gantt_chart(pipeline) if args.output and "htm" in args.output: fig.write_html(args.output) elif args.output: fig.update_layout(width=1000) fig.write_image(args.output) else: fig.show()