From 12f50ab0cdca13b4a59c6342f3bd28fc4cd1a570 Mon Sep 17 00:00:00 2001 From: Arseniy Obolenskiy Date: Sun, 7 Jun 2026 14:10:10 +0200 Subject: [PATCH] [doc] Improve CI jobs graph generation Align `jobs_graph.py` to generate the actual svg with the same layout --- docs/_static/ci_graph.svg | 76 +++++++++++++++--------------- scripts/jobs_graph.py | 97 +++++++++++++++++++++++++++++++++------ 2 files changed, 119 insertions(+), 54 deletions(-) diff --git a/docs/_static/ci_graph.svg b/docs/_static/ci_graph.svg index fa7f28903..ed924bb73 100644 --- a/docs/_static/ci_graph.svg +++ b/docs/_static/ci_graph.svg @@ -1,93 +1,91 @@ - - - + + CI_Overview - - + + -pre_commit - -pre-commit +pre-commit + +pre-commit ubuntu - -ubuntu + +ubuntu - + -pre_commit->ubuntu - - +pre-commit->ubuntu + + mac - -mac + +mac - + -pre_commit->mac - - +pre-commit->mac + + windows -windows +windows - + -pre_commit->windows - +pre-commit->windows + - perf - -perf + +perf ubuntu->perf - - + + - mac->perf - - + + windows->perf - - + + pages - -pages + +pages perf->pages - - + + diff --git a/scripts/jobs_graph.py b/scripts/jobs_graph.py index 52824d22c..d8d1f34b7 100644 --- a/scripts/jobs_graph.py +++ b/scripts/jobs_graph.py @@ -1,4 +1,6 @@ +import argparse import os +import subprocess try: import yaml @@ -6,11 +8,7 @@ print("Please install pyyaml: pip install pyyaml") exit(1) -try: - import graphviz -except ImportError: - print("Please install graphviz: pip install graphviz") - exit(1) +EXCLUDED_JOBS = {"ci-scope"} def parse_gha_yml(file_path): @@ -20,27 +18,96 @@ def parse_gha_yml(file_path): def build_jobs_graph(gha_data): - jobs = gha_data.get("jobs", {}) - dot = graphviz.Digraph() + jobs = filter_jobs(gha_data.get("jobs", {})) + dot = [ + "digraph CI_Overview {", + ' graph [rankdir="LR", ranksep="1.0", nodesep="0.5", splines="ortho"];', + ' node [shape="box", style="rounded", fontname="Helvetica"];', + ' edge [color="#333333"];', + ] for job_name, job_data in jobs.items(): - dot.node(job_name) + dot.append(f" {quote_dot_id(job_name)};") needs = job_data.get("needs", []) if isinstance(needs, str): needs = [needs] for dependency in needs: - dot.edge(dependency, job_name) + if dependency not in jobs: + continue + dot.append(f" {quote_dot_id(dependency)} -> {quote_dot_id(job_name)};") + + for rank_jobs in get_ranked_jobs(jobs).values(): + same_rank_jobs = " ".join( + f"{quote_dot_id(job_name)};" for job_name in rank_jobs + ) + dot.append(f" {{ rank=same; {same_rank_jobs} }}") + + dot.append("}") + return "\n".join(dot) + + +def filter_jobs(jobs): + return { + job_name: job_data + for job_name, job_data in jobs.items() + if job_name not in EXCLUDED_JOBS + } + + +def quote_dot_id(value): + return f'"{value.replace("\\", "\\\\").replace('"', '\\"')}"' + - return dot +def get_ranked_jobs(jobs): + ranks = {} + + def get_rank(job_name): + if job_name in ranks: + return ranks[job_name] + + needs = jobs[job_name].get("needs", []) + if isinstance(needs, str): + needs = [needs] + needs = [dependency for dependency in needs if dependency in jobs] + + if not needs: + ranks[job_name] = 0 + else: + ranks[job_name] = max(get_rank(dependency) for dependency in needs) + 1 + return ranks[job_name] + + for job_name in jobs: + get_rank(job_name) + + ranked_jobs = {} + for job_name, rank in ranks.items(): + ranked_jobs.setdefault(rank, []).append(job_name) + return ranked_jobs def save_graph(dot, filename, file_format): - dot.render(filename, format=file_format, cleanup=True) + subprocess.run( + ["dot", f"-T{file_format}", "-o", f"{filename}.{file_format}"], + input=dot, + text=True, + check=True, + ) + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Generate a Graphviz CI jobs graph from a GitHub Actions workflow." + ) + parser.add_argument( + "--workflow", default=os.path.join(".github", "workflows", "main.yml") + ) + parser.add_argument("--out", default=os.path.join("docs", "_static", "ci_graph")) + parser.add_argument("--format", default="svg") + return parser.parse_args() if __name__ == "__main__": - gha_file_path = os.path.join(".github", "workflows", "main.yml") - svg_path = os.path.join("docs", "_static", "ci_graph") - gha_data = parse_gha_yml(gha_file_path) + args = parse_args() + gha_data = parse_gha_yml(args.workflow) jobs_graph = build_jobs_graph(gha_data) - save_graph(jobs_graph, svg_path, "svg") + save_graph(jobs_graph, args.out, args.format)