diff --git a/.gitlab-ci/bin/download_gl_schema.sh b/.gitlab-ci/bin/download_gl_schema.sh new file mode 100755 index 0000000000000..41858df4c1232 --- /dev/null +++ b/.gitlab-ci/bin/download_gl_schema.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Helper script to download the schema GraphQL from Gitlab to enable IDEs to +# assist the developer to edit gql files + +SOURCE_DIR=$(dirname "$(realpath "$0")") + +( + cd $SOURCE_DIR || exit 1 + gql-cli https://gitlab.freedesktop.org/api/graphql --print-schema > schema.graphql +) diff --git a/.gitlab-ci/bin/gitlab_gql.py b/.gitlab-ci/bin/gitlab_gql.py new file mode 100755 index 0000000000000..04082ea7a3519 --- /dev/null +++ b/.gitlab-ci/bin/gitlab_gql.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import re +from argparse import ArgumentParser, Namespace +from dataclasses import dataclass, field +from itertools import chain +from pathlib import Path +from typing import Any, Pattern + +from gql import Client, gql +from gql.transport.aiohttp import AIOHTTPTransport +from graphql import DocumentNode + +Dag = dict[str, list[str]] + + +@dataclass +class GitlabGQL: + _transport: Any = field(init=False) + client: Client = field(init=False) + url: str = "https://gitlab.freedesktop.org/api/graphql" + + def __post_init__(self): + self._setup_gitlab_gql_client() + + def _setup_gitlab_gql_client(self) -> Client: + # Select your transport with a defined url endpoint + self._transport = AIOHTTPTransport(url=self.url) + + # Create a GraphQL client using the defined transport + self.client = Client( + transport=self._transport, fetch_schema_from_transport=True + ) + + def query(self, gql_file: Path | str, params: dict[str, Any]) -> dict[str, Any]: + # Provide a GraphQL query + source_path = Path(__file__).parent + pipeline_query_file = source_path / gql_file + + query: DocumentNode + with open(pipeline_query_file, "r") as f: + pipeline_query = f.read() + query = gql(pipeline_query) + + # Execute the query on the transport + return self.client.execute(query, variable_values=params) + + +def create_job_needs_dag( + gl_gql: GitlabGQL, params +) -> tuple[Dag, dict[str, dict[str, Any]]]: + + result = gl_gql.query("pipeline_details.gql", params) + dag = {} + jobs = {} + pipeline = result["project"]["pipeline"] + if not pipeline: + raise RuntimeError(f"Could not find any pipelines for {params}") + + for stage in pipeline["stages"]["nodes"]: + for stage_job in stage["groups"]["nodes"]: + for job in stage_job["jobs"]["nodes"]: + needs = job.pop("needs")["nodes"] + jobs[job["name"]] = job + dag[job["name"]] = {node["name"] for node in needs} + + for job, needs in dag.items(): + needs: set + partial = True + + while partial: + next_depth = {n for dn in needs for n in dag[dn]} + partial = not needs.issuperset(next_depth) + needs = needs.union(next_depth) + + dag[job] = needs + + return dag, jobs + + +def filter_dag(dag: Dag, regex: Pattern) -> Dag: + return {job: needs for job, needs in dag.items() if re.match(regex, job)} + + +def print_dag(dag: Dag) -> None: + for job, needs in dag.items(): + print(f"{job}:") + print(f"\t{' '.join(needs)}") + print() + + +def parse_args() -> Namespace: + parser = ArgumentParser() + parser.add_argument("-pp", "--project-path", type=str, default="mesa/mesa") + parser.add_argument("--sha", type=str, required=True) + parser.add_argument("--regex", type=str, required=False) + parser.add_argument("--print-dag", action="store_true") + + return parser.parse_args() + + +def main(): + args = parse_args() + gl_gql = GitlabGQL() + + if args.print_dag: + dag, jobs = create_job_needs_dag( + gl_gql, {"projectPath": args.project_path, "sha": args.sha} + ) + + if args.regex: + dag = filter_dag(dag, re.compile(args.regex)) + print_dag(dag) + + +if __name__ == "__main__": + main() diff --git a/.gitlab-ci/bin/pipeline_details.gql b/.gitlab-ci/bin/pipeline_details.gql new file mode 100644 index 0000000000000..e735ed8d3961b --- /dev/null +++ b/.gitlab-ci/bin/pipeline_details.gql @@ -0,0 +1,86 @@ +fragment LinkedPipelineData on Pipeline { + id + iid + path + cancelable + retryable + userPermissions { + updatePipeline + } + status: detailedStatus { + id + group + label + icon + } + sourceJob { + id + name + } + project { + id + name + fullPath + } +} + +query getPipelineDetails($projectPath: ID!, $sha: String!) { + project(fullPath: $projectPath) { + id + pipeline(sha: $sha) { + id + iid + complete + downstream { + nodes { + ...LinkedPipelineData + } + } + upstream { + ...LinkedPipelineData + } + stages { + nodes { + id + name + status: detailedStatus { + id + action { + id + icon + path + title + } + } + groups { + nodes { + id + status: detailedStatus { + id + label + group + icon + } + name + size + jobs { + nodes { + id + name + kind + scheduledAt + needs { + nodes { + id + name + } + } + } + } + } + } + } + } + } + } +} diff --git a/.gitlab-ci/bin/requirements.txt b/.gitlab-ci/bin/requirements.txt index 115e8c2949203..d07b936555b2c 100644 --- a/.gitlab-ci/bin/requirements.txt +++ b/.gitlab-ci/bin/requirements.txt @@ -1,2 +1,3 @@ colorama==0.4.5 +gql==3.4.0 python-gitlab==3.5.0 diff --git a/.graphqlrc.yml b/.graphqlrc.yml new file mode 100644 index 0000000000000..d807b387b5e4e --- /dev/null +++ b/.graphqlrc.yml @@ -0,0 +1,2 @@ +schema: 'schema.graphql' +documents: 'src/**/*.{graphql,js,ts,jsx,tsx}'