Skip to content

Commit 65b6ede

Browse files
Guilherme GalloMarge Bot
Guilherme Gallo
authored and
Marge Bot
committed
ci/bin: Add utility to find jobs dependencies
Use GraphQL API from Gitlab to find jobs dependencies in a pipeline. E.g: Find all dependencies for jobs starting with "iris-" ```sh .gitlab-ci/bin/gitlab_gql.py --sha $(git -C ../mesa-fast-fix rev-parse HEAD) --print-dag --regex "iris-.*" ``` Signed-off-by: Guilherme Gallo <[email protected]> Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/17791>
1 parent 63082cf commit 65b6ede

File tree

5 files changed

+217
-0
lines changed

5 files changed

+217
-0
lines changed

.gitlab-ci/bin/download_gl_schema.sh

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/sh
2+
3+
# Helper script to download the schema GraphQL from Gitlab to enable IDEs to
4+
# assist the developer to edit gql files
5+
6+
SOURCE_DIR=$(dirname "$(realpath "$0")")
7+
8+
(
9+
cd $SOURCE_DIR || exit 1
10+
gql-cli https://gitlab.freedesktop.org/api/graphql --print-schema > schema.graphql
11+
)

.gitlab-ci/bin/gitlab_gql.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env python3
2+
3+
import re
4+
from argparse import ArgumentParser, Namespace
5+
from dataclasses import dataclass, field
6+
from itertools import chain
7+
from pathlib import Path
8+
from typing import Any, Pattern
9+
10+
from gql import Client, gql
11+
from gql.transport.aiohttp import AIOHTTPTransport
12+
from graphql import DocumentNode
13+
14+
Dag = dict[str, list[str]]
15+
16+
17+
@dataclass
18+
class GitlabGQL:
19+
_transport: Any = field(init=False)
20+
client: Client = field(init=False)
21+
url: str = "https://gitlab.freedesktop.org/api/graphql"
22+
23+
def __post_init__(self):
24+
self._setup_gitlab_gql_client()
25+
26+
def _setup_gitlab_gql_client(self) -> Client:
27+
# Select your transport with a defined url endpoint
28+
self._transport = AIOHTTPTransport(url=self.url)
29+
30+
# Create a GraphQL client using the defined transport
31+
self.client = Client(
32+
transport=self._transport, fetch_schema_from_transport=True
33+
)
34+
35+
def query(self, gql_file: Path | str, params: dict[str, Any]) -> dict[str, Any]:
36+
# Provide a GraphQL query
37+
source_path = Path(__file__).parent
38+
pipeline_query_file = source_path / gql_file
39+
40+
query: DocumentNode
41+
with open(pipeline_query_file, "r") as f:
42+
pipeline_query = f.read()
43+
query = gql(pipeline_query)
44+
45+
# Execute the query on the transport
46+
return self.client.execute(query, variable_values=params)
47+
48+
49+
def create_job_needs_dag(
50+
gl_gql: GitlabGQL, params
51+
) -> tuple[Dag, dict[str, dict[str, Any]]]:
52+
53+
result = gl_gql.query("pipeline_details.gql", params)
54+
dag = {}
55+
jobs = {}
56+
pipeline = result["project"]["pipeline"]
57+
if not pipeline:
58+
raise RuntimeError(f"Could not find any pipelines for {params}")
59+
60+
for stage in pipeline["stages"]["nodes"]:
61+
for stage_job in stage["groups"]["nodes"]:
62+
for job in stage_job["jobs"]["nodes"]:
63+
needs = job.pop("needs")["nodes"]
64+
jobs[job["name"]] = job
65+
dag[job["name"]] = {node["name"] for node in needs}
66+
67+
for job, needs in dag.items():
68+
needs: set
69+
partial = True
70+
71+
while partial:
72+
next_depth = {n for dn in needs for n in dag[dn]}
73+
partial = not needs.issuperset(next_depth)
74+
needs = needs.union(next_depth)
75+
76+
dag[job] = needs
77+
78+
return dag, jobs
79+
80+
81+
def filter_dag(dag: Dag, regex: Pattern) -> Dag:
82+
return {job: needs for job, needs in dag.items() if re.match(regex, job)}
83+
84+
85+
def print_dag(dag: Dag) -> None:
86+
for job, needs in dag.items():
87+
print(f"{job}:")
88+
print(f"\t{' '.join(needs)}")
89+
print()
90+
91+
92+
def parse_args() -> Namespace:
93+
parser = ArgumentParser()
94+
parser.add_argument("-pp", "--project-path", type=str, default="mesa/mesa")
95+
parser.add_argument("--sha", type=str, required=True)
96+
parser.add_argument("--regex", type=str, required=False)
97+
parser.add_argument("--print-dag", action="store_true")
98+
99+
return parser.parse_args()
100+
101+
102+
def main():
103+
args = parse_args()
104+
gl_gql = GitlabGQL()
105+
106+
if args.print_dag:
107+
dag, jobs = create_job_needs_dag(
108+
gl_gql, {"projectPath": args.project_path, "sha": args.sha}
109+
)
110+
111+
if args.regex:
112+
dag = filter_dag(dag, re.compile(args.regex))
113+
print_dag(dag)
114+
115+
116+
if __name__ == "__main__":
117+
main()

.gitlab-ci/bin/pipeline_details.gql

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
fragment LinkedPipelineData on Pipeline {
2+
id
3+
iid
4+
path
5+
cancelable
6+
retryable
7+
userPermissions {
8+
updatePipeline
9+
}
10+
status: detailedStatus {
11+
id
12+
group
13+
label
14+
icon
15+
}
16+
sourceJob {
17+
id
18+
name
19+
}
20+
project {
21+
id
22+
name
23+
fullPath
24+
}
25+
}
26+
27+
query getPipelineDetails($projectPath: ID!, $sha: String!) {
28+
project(fullPath: $projectPath) {
29+
id
30+
pipeline(sha: $sha) {
31+
id
32+
iid
33+
complete
34+
downstream {
35+
nodes {
36+
...LinkedPipelineData
37+
}
38+
}
39+
upstream {
40+
...LinkedPipelineData
41+
}
42+
stages {
43+
nodes {
44+
id
45+
name
46+
status: detailedStatus {
47+
id
48+
action {
49+
id
50+
icon
51+
path
52+
title
53+
}
54+
}
55+
groups {
56+
nodes {
57+
id
58+
status: detailedStatus {
59+
id
60+
label
61+
group
62+
icon
63+
}
64+
name
65+
size
66+
jobs {
67+
nodes {
68+
id
69+
name
70+
kind
71+
scheduledAt
72+
needs {
73+
nodes {
74+
id
75+
name
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}

.gitlab-ci/bin/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
colorama==0.4.5
2+
gql==3.4.0
23
python-gitlab==3.5.0

.graphqlrc.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: 'schema.graphql'
2+
documents: 'src/**/*.{graphql,js,ts,jsx,tsx}'

0 commit comments

Comments
 (0)