gcip2 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gcip2/__init__.py +0 -0
- gcip2/builder.py +205 -0
- gcip2/ci.json +3238 -0
- gcip2/cli.py +93 -0
- gcip2/pipeline_core/__init__.py +640 -0
- gcip2-0.0.1.dist-info/METADATA +20 -0
- gcip2-0.0.1.dist-info/RECORD +10 -0
- gcip2-0.0.1.dist-info/WHEEL +4 -0
- gcip2-0.0.1.dist-info/entry_points.txt +3 -0
- gcip2-0.0.1.dist-info/licenses/LICENCE.md +21 -0
gcip2/__init__.py
ADDED
|
File without changes
|
gcip2/builder.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import os
|
|
3
|
+
import pathlib
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Optional, Self
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
from .pipeline_core import (
|
|
11
|
+
Artifacts,
|
|
12
|
+
Job,
|
|
13
|
+
JobBuilderImpl,
|
|
14
|
+
Needs,
|
|
15
|
+
Pipeline,
|
|
16
|
+
PipelineBuilderImpl,
|
|
17
|
+
Trigger,
|
|
18
|
+
TriggerForward,
|
|
19
|
+
TriggerIncludeArtifact,
|
|
20
|
+
TriggerStrategy,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CustomDumper(yaml.SafeDumper):
|
|
25
|
+
@staticmethod
|
|
26
|
+
def str_presenter(
|
|
27
|
+
dumper: yaml.representer.BaseRepresenter,
|
|
28
|
+
data: str,
|
|
29
|
+
) -> yaml.ScalarNode:
|
|
30
|
+
if "\n" in data:
|
|
31
|
+
return dumper.represent_scalar( # type: ignore
|
|
32
|
+
"tag:yaml.org,2002:str",
|
|
33
|
+
data,
|
|
34
|
+
style="|",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return dumper.represent_scalar( # type: ignore
|
|
38
|
+
"tag:yaml.org,2002:str",
|
|
39
|
+
data,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
yaml_representers = {
|
|
43
|
+
**yaml.SafeDumper.yaml_representers,
|
|
44
|
+
str: str_presenter,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def increase_indent(
|
|
49
|
+
self: Self,
|
|
50
|
+
flow: bool = False,
|
|
51
|
+
indentless: bool = False,
|
|
52
|
+
):
|
|
53
|
+
return super().increase_indent(flow, False)
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
def write_line_break(self, data: Optional[str] = None):
|
|
57
|
+
super().write_line_break(data)
|
|
58
|
+
|
|
59
|
+
if len(self.indents) == 1:
|
|
60
|
+
super().write_line_break()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TriggerPipelineDefaults(str, Enum):
|
|
64
|
+
build_pipeline = "build-pipeline"
|
|
65
|
+
trigger_pipeline = "trigger-pipeline"
|
|
66
|
+
out_dir = "out"
|
|
67
|
+
out_file = "out/pipeline.gitlab-ci.yml"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PipelineBuilder:
|
|
71
|
+
yaml_dumper = CustomDumper
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _gen_build_pipeline_job():
|
|
75
|
+
return Job(
|
|
76
|
+
name=TriggerPipelineDefaults.build_pipeline,
|
|
77
|
+
before_script=[
|
|
78
|
+
"pip3 install poetry",
|
|
79
|
+
],
|
|
80
|
+
script=[
|
|
81
|
+
"poetry install && . .venv/bin/activate",
|
|
82
|
+
"gcip2 build-pipeline",
|
|
83
|
+
],
|
|
84
|
+
tags=["immortal"],
|
|
85
|
+
artifacts=Artifacts(paths=[TriggerPipelineDefaults.out_dir]),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _gen_trigger_pipeline_job():
|
|
90
|
+
return Job(
|
|
91
|
+
name=TriggerPipelineDefaults.trigger_pipeline,
|
|
92
|
+
interruptible=False,
|
|
93
|
+
trigger=Trigger(
|
|
94
|
+
include=[
|
|
95
|
+
TriggerIncludeArtifact(
|
|
96
|
+
artifact=TriggerPipelineDefaults.out_file,
|
|
97
|
+
job=TriggerPipelineDefaults.build_pipeline,
|
|
98
|
+
)
|
|
99
|
+
],
|
|
100
|
+
strategy=TriggerStrategy.DEPEND,
|
|
101
|
+
forward=TriggerForward(
|
|
102
|
+
yaml_variables=True,
|
|
103
|
+
pipeline_variables=True,
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
needs=[
|
|
107
|
+
Needs(
|
|
108
|
+
job=TriggerPipelineDefaults.build_pipeline,
|
|
109
|
+
artifacts=True,
|
|
110
|
+
)
|
|
111
|
+
],
|
|
112
|
+
variables={
|
|
113
|
+
"PARENT_PIPELINE_ID": "${CI_PIPELINE_ID}",
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def build_gitlab_ci(
|
|
118
|
+
self: Self,
|
|
119
|
+
out_gitlab_ci: Any,
|
|
120
|
+
default_tags: str,
|
|
121
|
+
) -> None:
|
|
122
|
+
pipeline_obj = Pipeline(
|
|
123
|
+
jobs=[
|
|
124
|
+
self._gen_build_pipeline_job(),
|
|
125
|
+
self._gen_trigger_pipeline_job(),
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
self.build_pipeline_file(
|
|
129
|
+
pipeline=pipeline_obj,
|
|
130
|
+
path=out_gitlab_ci,
|
|
131
|
+
default_tags=default_tags,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def load_pipeline(self: Self, path: Any) -> PipelineBuilderImpl:
|
|
135
|
+
path = pathlib.Path(path)
|
|
136
|
+
spec: Any = importlib.util.spec_from_file_location(
|
|
137
|
+
"user_pipeline",
|
|
138
|
+
path,
|
|
139
|
+
)
|
|
140
|
+
module = importlib.util.module_from_spec(spec)
|
|
141
|
+
spec.loader.exec_module(module)
|
|
142
|
+
|
|
143
|
+
for obj in module.__dict__.values():
|
|
144
|
+
|
|
145
|
+
if callable(obj) and getattr(obj, "__gcip2_pipeline__", False):
|
|
146
|
+
return obj # type: ignore
|
|
147
|
+
|
|
148
|
+
if isinstance(obj, type) and issubclass(obj, PipelineBuilderImpl) and obj is not PipelineBuilderImpl:
|
|
149
|
+
return obj()
|
|
150
|
+
|
|
151
|
+
raise RuntimeError("Pipeline class not found")
|
|
152
|
+
|
|
153
|
+
def render_pipeline(self: Self, pipeline: Pipeline):
|
|
154
|
+
pipeline = Pipeline.model_validate(pipeline, strict=True)
|
|
155
|
+
result: dict[str, Any] = pipeline.dump(exclude=["jobs"])
|
|
156
|
+
|
|
157
|
+
for job in pipeline.jobs:
|
|
158
|
+
if isinstance(job, JobBuilderImpl):
|
|
159
|
+
job = job.build()
|
|
160
|
+
if not job.name:
|
|
161
|
+
raise ValueError("Job name is missing.")
|
|
162
|
+
if job.name in result.keys():
|
|
163
|
+
raise ValueError("Job with same name already added.")
|
|
164
|
+
result[job.name] = job.dump(exclude=["name"])
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
def build_pipeline_file(
|
|
168
|
+
self: Self,
|
|
169
|
+
pipeline: Pipeline,
|
|
170
|
+
path: Optional[pathlib.Path],
|
|
171
|
+
default_tags: Optional[str] = None,
|
|
172
|
+
**_: Any,
|
|
173
|
+
) -> None:
|
|
174
|
+
os.makedirs("out", exist_ok=True)
|
|
175
|
+
pipeline_copy = pipeline.model_copy(deep=True)
|
|
176
|
+
if default_tags:
|
|
177
|
+
for job in pipeline_copy.jobs:
|
|
178
|
+
if job.name == TriggerPipelineDefaults.build_pipeline:
|
|
179
|
+
job.tags = default_tags.split(sep=" ")
|
|
180
|
+
|
|
181
|
+
data = yaml.dump(
|
|
182
|
+
self.render_pipeline(pipeline=pipeline_copy),
|
|
183
|
+
sort_keys=False,
|
|
184
|
+
allow_unicode=True,
|
|
185
|
+
Dumper=CustomDumper,
|
|
186
|
+
)
|
|
187
|
+
path = path or pathlib.Path(os.getcwd()) / "out" / "pipeline.gitlab-ci.yml"
|
|
188
|
+
with open(path, "w") as f:
|
|
189
|
+
f.write(data)
|
|
190
|
+
|
|
191
|
+
def build_pipeline(self: Self, ci_file_path: str, out_pipeline_path: str) -> None:
|
|
192
|
+
pipeline_entry = self.load_pipeline(ci_file_path)
|
|
193
|
+
|
|
194
|
+
# decorator API
|
|
195
|
+
if callable(pipeline_entry):
|
|
196
|
+
pipeline: Pipeline = pipeline_entry() # type: ignore
|
|
197
|
+
|
|
198
|
+
# class API
|
|
199
|
+
else:
|
|
200
|
+
pipeline: Pipeline = pipeline_entry.apply().build()
|
|
201
|
+
|
|
202
|
+
self.build_pipeline_file(
|
|
203
|
+
pipeline=pipeline, # type: ignore
|
|
204
|
+
path=pathlib.Path(out_pipeline_path),
|
|
205
|
+
)
|