1# ruff: noqa: E501
2import dataclasses
3import json
4from enum import Enum
5from typing import Any, ClassVar, Optional, Self
6
7import injector
8import jsonschema
9import pydantic
10
11from gcip2.project_config import ProjectConfig
12from gcip2.vault import SecretsHandler
13
14__all__ = (
15 "Trigger",
16 "Pipeline",
17 "Job",
18 "Stage",
19 "Image",
20 "Needs",
21 "Artifacts",
22 "ArtifactsReports",
23 "IncludeComponent",
24 "BaseInput",
25 "Workflow",
26 "WorkflowAutoCancel",
27 "Rule",
28 "WorkflowAutoCancelOnJobFailure",
29 "RuleChanges",
30 "WorkflowAutoCancelOnNewCommit",
31 "JobVariables",
32 "GlobalVariables",
33 "Default",
34)
35
36
37class BaseModel(pydantic.BaseModel):
38 model_config = pydantic.ConfigDict(extra="allow")
39
40 def dump(self: Self, **kwargs: Any) -> dict[str, Any]:
41 return self.model_dump(
42 mode="json",
43 exclude_none=True,
44 exclude_defaults=True,
45 by_alias=True,
46 **kwargs,
47 )
48
49
50class BaseInputType(Enum):
51 ARRAY = "array"
52 BOOLEAN = "boolean"
53 NUMBER = "number"
54 STRING = "string"
55
56
[docs]
57class Stage(str, Enum):
58 PRE = ".pre"
59 POST = ".post"
60 JOBS = "jobs"
61 BUILD = "build"
62 TEST = "test"
63 DEPLOY = "deploy"
64
65
66class ArtifactsWhen(str, Enum):
67 ON_SUCCESS = "on_success"
68 ON_FAILURE = "on_failure"
69 ALWAYS = "always"
70
71
72class WorkflowWhen(str, Enum):
73 ALWAYS = "always"
74 NEVER = "never"
75
76
77class JobWhen(str, Enum):
78 ALWAYS = "always"
79 NEVER = "never"
80 ON_SUCCESS = "on_success"
81 ON_FAILURE = "on_failure"
82 MANUAL = "manual"
83 DELAYED = "delayed"
84
85
101
102
103class TriggerStrategy(str, Enum):
104 DEPEND = "depend"
105 MIRROR = "mirror"
106
107
108class TriggerIncludeArtifact(BaseModel):
109 job: Optional[str] = None
110
111 artifact: Optional[str] = None
112 "Relative path to the generated YAML file which is extracted from the artifacts and used as the configuration for triggering the child pipeline."
113
114
115class TriggerIncludeLocal(BaseModel):
116 local: Optional[str] = None
117 "Relative path from local repository root (`/`) to the local YAML file to define the pipeline configuration."
118
119
120class TriggerIncludeTemplate(BaseModel):
121 template: Optional[str] = None
122 "Name of the template YAML file to use in the pipeline configuration."
123
124
125class TriggerForward(BaseModel):
126 yaml_variables: Optional[bool] = None
127 "Variables defined in the trigger job are passed to downstream pipelines."
128
129 pipeline_variables: Optional[bool] = None
130 "Variables added for manual pipeline runs and scheduled pipelines are passed to downstream pipelines."
131
132
[docs]
133class Trigger(BaseModel):
134 "Trigger allows you to define downstream pipeline trigger."
135
136 "When a job created from trigger definition is started by GitLab, a downstream pipeline gets created."
137 "https://docs.gitlab.com/ci/yaml/#trigger"
138
139 project: Optional[str] = None
140 "Path to the project, e.g. `group/project`, or `group/sub-group/project`."
141
142 ref: Optional[str] = None
143 "Branch/Tag/Commit hash for the target project."
144
145 file: Optional[str] = None
146 "Relative path from repository root (`/`) to the pipeline configuration YAML file."
147
148 branch: Optional[str] = None
149 "The branch name that a downstream pipeline will use"
150
151 strategy: Optional[TriggerStrategy] = None
152 "You can mirror or depend on the pipeline status from the triggered pipeline to the source bridge job by using strategy: `depend` or `mirror`"
153
154 include: Optional[list[TriggerIncludeArtifact | TriggerIncludeLocal | TriggerIncludeTemplate]] = None
155
156 forward: Optional[TriggerForward] = None
157 "Specify what to forward to the downstream pipeline."
158
159 component: Optional[str] = None
160 "Local path to component directory or full path to external component directory."
161
162 remote: Optional[str] = None
163 "URL to a `yaml`/`yml` template file using HTTP/HTTPS."
164
165 inputs: Optional[Any] = None
166 "Used to pass input values to included templates, components, downstream pipelines, or child pipelines."
167 "https://docs.gitlab.com/ci/inputs"
168
169
170class Parallel(BaseModel):
171 matrix: list[dict[str, list[str]]] = pydantic.Field(default_factory=lambda: [])
172
173
[docs]
174class Needs(BaseModel):
175 pipeline: Optional[str] = None
176
177 job: Optional[str] = None
178
179 project: Optional[str] = None
180
181 optional: Optional[bool] = None
182
183 artifacts: Optional[bool] = None
184
185 parallel: Optional[Parallel | int] = None
186
187
[docs]
188class Image(BaseModel):
189 name: Optional[str] = None
190
191 entrypoint: list[str] = pydantic.Field(default_factory=list)
192
193
194class ArtifactsReportsCoverage(BaseModel):
195 coverage_format: Optional[str] = None
196 path: Optional[str] = None
197
198
[docs]
199class ArtifactsReports(BaseModel):
200 accessibility: Optional[list[str]] = None
201 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsaccessibility"
202
203 annotations: Optional[list[str]] = None
204 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsannotations"
205
206 api_fuzzing: Optional[list[str]] = None
207 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsapi_fuzzing"
208
209 browser_performance: Optional[list[str]] = None
210 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsbrowser_performance"
211
212 coverage_report: Optional[ArtifactsReportsCoverage] = None
213 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscoverage_report"
214
215 codequality: Optional[list[str]] = None
216 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscodequality"
217
218 container_scanning: Optional[list[str]] = None
219 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscontainer_scanning"
220
221 coverage_fuzzing: Optional[list[str]] = None
222 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscoverage_fuzzing"
223
224 cyclonedx: Optional[list[str]] = None
225 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportscyclonedx"
226
227 dast: Optional[list[str]] = None
228 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsdast"
229
230 dependency_scanning: Optional[list[str]] = None
231 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsdependency_scanning"
232
233 dotenv: Optional[list[str]] = None
234 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsdotenv"
235
236 junit: Optional[str | list[str]] = None
237 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit"
238
239 load_performance: Optional[list[str]] = None
240 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsload_performance"
241
242 metrics: Optional[list[str]] = None
243 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsmetrics"
244
245 requirements: Optional[list[str]] = None
246 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsrequirements"
247
248 sarif: Optional[list[str]] = None
249 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportssarif"
250
251 sast: Optional[list[str]] = None
252 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportssast"
253
254 secret_detection: Optional[list[str]] = None
255 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportssecret_detection"
256
257 terraform: Optional[list[str]] = None
258 "https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsterraform"
259
260
261class ArtifactsAccess(str, Enum):
262 ALL = "all"
263 DEVELOPER = "developer"
264 MAINTAINER = "maintainer"
265 NONE = "none"
266
267
[docs]
268class Artifacts(BaseModel):
269 paths: Optional[list[str]] = None
270 "A list of paths to files/folders that should be included in the artifact."
271 "https://docs.gitlab.com/ci/yaml/#artifactspaths"
272
273 exclude: Optional[list[str]] = None
274 "A list of paths to files/folders that should be excluded in the artifact."
275 "https://docs.gitlab.com/ci/yaml/#artifactsexclude"
276
277 expose_as: Optional[str] = None
278 "Can be used to expose job artifacts in the merge request UI. GitLab will add a link <expose_as> to the relevant merge request that points to the artifact."
279 "https://docs.gitlab.com/ci/yaml/#artifactsexpose_as"
280
281 expire_in: Optional[str] = None
282 "Use expire_in to specify how long job artifacts are stored before they expire and are deleted."
283 "https://archives.docs.gitlab.com/17.11/ci/yaml/#artifactsexpire_in"
284
285 name: Optional[str] = None
286 "Use the artifacts:name keyword to define the name of the created artifacts archive. You can specify a unique name for every archive."
287 "https://archives.docs.gitlab.com/17.11/ci/yaml/#artifactsname"
288
289 public: Optional[bool] = None
290 "Use artifacts:public to determine whether the job artifacts should be publicly available."
291 "https://archives.docs.gitlab.com/17.11/ci/yaml/#artifactspublic"
292
293 access: Optional[ArtifactsAccess] = None
294 "Use artifacts:access to determine who can access the job artifacts from the GitLab UI or API."
295 "This option does not prevent you from forwarding artifacts to downstream pipelines."
296 "https://archives.docs.gitlab.com/17.11/ci/yaml/#artifactsaccess"
297
298 reports: Optional[ArtifactsReports] = None
299
300 untracked: Optional[bool] = None
301 "Use artifacts:untracked to add all Git untracked files as artifacts (along with the paths defined in artifacts:paths)."
302 "artifacts:untracked ignores configuration in the repository's .gitignore, so matching artifacts in .gitignore are included."
303 "https://archives.docs.gitlab.com/17.11/ci/yaml/#artifactsuntracked"
304
305 when: Optional[ArtifactsWhen] = None
306 "Use artifacts:when to upload artifacts on job failure or despite the failure."
307 "https://archives.docs.gitlab.com/17.11/ci/yaml/#artifactswhen"
308
309
[docs]
310class JobVariables(BaseModel):
311 "Defines default variables for all jobs. Job level property overrides global variables."
312
313 "https://docs.gitlab.com/ci/yaml/#variables)."
314
315 value: Optional[str | bool | int | float] = None
316 "Default value of the variable. If used with `options`, `value` must be included in the array."
317 "https://docs.gitlab.com/ci/yaml/#variablesvalue"
318
319 expand: Optional[bool] = None
320 "If the variable is expandable or not."
321 "https://docs.gitlab.com/ci/yaml/#variablesexpand"
322
323
[docs]
324class GlobalVariables(JobVariables):
325 description: Optional[str] = None
326 "Explains what the variable is used for, what the acceptable values are."
327 "Variables with `description` are prefilled when running a pipeline manually."
328 "https://docs.gitlab.com/ci/yaml/#variablesdescription"
329
330 options: Optional[list[str | bool | int | float]] = pydantic.Field(default_factory=lambda: [])
331 "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually."
332 "https://docs.gitlab.com/ci/yaml/#variablesoptions"
333
334
335class RuleVariables(JobVariables):
336 ...
337 "Defines variables for a rule result."
338 "https://docs.gitlab.com/ci/yaml/#rulesvariables"
339
340
[docs]
341class RuleChanges(BaseModel):
342 paths: list[str] = pydantic.Field(default_factory=list)
343 "List of file paths."
344
345 compare_to: Optional[str] = None
346 "Ref for comparing changes."
347
348
349class RuleExists(BaseModel):
350 paths: list[str] = pydantic.Field(default_factory=list)
351 "List of file paths."
352
353 project: Optional[str] = None
354 "Path of the project to search in."
355
356 ref: Optional[str] = None
357 "Ref of the project to search in."
358
359
[docs]
360class WorkflowAutoCancelOnJobFailure(str, Enum):
361 NONE = "none"
362 ALL = "all"
363
364
[docs]
365class WorkflowAutoCancelOnNewCommit(str, Enum):
366 NONE = "none"
367 CONSERVATIVE = "conservative"
368 INTERRUPTIBLE = "interruptible"
369
370
[docs]
371class WorkflowAutoCancel(BaseModel):
372 on_job_failure: Optional[WorkflowAutoCancelOnJobFailure] = None
373 "Define which jobs to stop after a job fails."
374
375 on_new_commit: Optional[WorkflowAutoCancelOnNewCommit] = None
376 "Configure the behavior of the auto-cancel redundant pipelines feature."
377 "https://docs.gitlab.com/ci/yaml/#workflowauto_cancelon_new_commit"
378
379
[docs]
380class Rule(BaseModel):
381 model_config = pydantic.ConfigDict(populate_by_name=True)
382
383 if_: Optional[str] = pydantic.Field(serialization_alias="if", validation_alias="if", default=None)
384 "Expression to evaluate whether additional attributes should be provided to the job."
385 "https://docs.gitlab.com/ci/yaml/#rulesif"
386
387 changes: Optional[RuleChanges | list[str]] = None
388 "Additional attributes will be provided to job if any of the provided paths matches a modified file."
389 "https://docs.gitlab.com/ci/yaml/#ruleschanges"
390
391 exists: Optional[RuleExists | list[str]] = None
392 "Additional attributes will be provided to job if any of the provided paths matches an existing file in the repository."
393 "https://docs.gitlab.com/ci/yaml/#rulesexists"
394
395 variables: Optional[dict[str, str]] = None
396 "Defines variables for a rule result."
397 "https://docs.gitlab.com/ci/yaml/#rulesvariables"
398
399
400class IncludeRule(BaseModel):
401 pass
402
403
404class IncludeItem(BaseModel):
405 inputs: Optional[dict[str, BaseInput]] = None
406
407 rules: Optional[list[IncludeRule]] = None
408 "You can use rules to conditionally include other configuration files."
409 "https://docs.gitlab.com/ci/yaml/includes/#use-rules-with-include"
410
411
412class IncludeLocalItem(BaseModel):
413 local: str
414 "Relative path from local repository root (`/`) to the `yaml`/`yml` file template. The file must be on the same branch, and does not work across git submodules."
415
416
[docs]
417class IncludeComponent(IncludeItem):
418 component: Optional[str] = None
419 "Local path to component directory or full path to external component directory."
420
421
422class IdTokens(BaseModel):
423 aud: Optional[str | list[str]] = pydantic.Field(default_factory=list)
424
425
426class Identity(str, Enum):
427 GOOGLE_CLOUD = "google_cloud"
428
429
430class JobRule(Rule):
431 when: Optional[JobWhen] = None
432
433
434class Docker(BaseModel):
435 platform: Optional[str] = None
436 "Image architecture to pull."
437
438 user: Optional[str] = None
439 "Username or UID to use for the container."
440
441
442class Kubernetes(BaseModel):
443 user: str | int
444
445
446class PullPolicy(str, Enum):
447 ALWAYS = "always"
448 NEVER = "never"
449 IFNOTPRESENT = "if-not-present"
450
451
452class Services(BaseModel):
453 "Similar to `image` property, but will link the specified services to the `image` container."
454
455 name: str
456 "Full name of the image that should be used. It should contain the Registry part if needed."
457
458 entrypoint: Optional[list[str]] = None
459 "Command or script that should be executed as the container's entrypoint."
460 "It will be translated to Docker's --entrypoint option while creating the container."
461 "The syntax is similar to Dockerfile's ENTRYPOINT directive, where each shell token is a separate string in the array."
462 "https://docs.gitlab.com/ci/services/#available-settings-for-services"
463
464 docker: Optional[Docker] = None
465 "Options to pass to Runners Docker Executor."
466 "https://docs.gitlab.com/ci/yaml/#servicesdocker"
467
468 kubernetes: Optional[Kubernetes] = None
469 "Options to pass to Runners Kubernetes Executor."
470 "https://docs.gitlab.com/ci/yaml/#imagekubernetes"
471
472 pull_policy: Optional[PullPolicy | list[PullPolicy]] = None
473 "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`."
474 "https://docs.gitlab.com/ci/yaml/#servicespull_policy"
475
476 command: Optional[list[str] | str | list[list[str]]] = None
477 "Command or script that should be used as the container's command. It will be translated to arguments passed to Docker after the image's name."
478 "The syntax is similar to Dockerfile's CMD directive, where each shell token is a separate string in the array."
479 "https://docs.gitlab.com/ci/services/#available-settings-for-services"
480
481 alias: Optional[str] = None
482 "Additional alias that can be used to access the service from the job's container. Read Accessing the services for more information."
483 "https://docs.gitlab.com/ci/services/#available-settings-for-services"
484
485 variables: Optional[JobVariables] = None
486 "Additional environment variables that are passed exclusively to the service. Service variables cannot reference themselves."
487 "https://docs.gitlab.com/ci/services/#available-settings-for-services)"
488
489
490class JobTemplate(BaseModel):
491 model_config = pydantic.ConfigDict(populate_by_name=True)
492
493 image: Optional[Image] = None
494 "Full name of the image that should be used. It should contain the Registry part if needed."
495
496 services: Optional[list[str | Services]] = None
497 "https://docs.gitlab.com/ci/yaml/#services"
498
499 before_script: Optional[list[str] | str] = None
500
501 after_script: Optional[list[str] | str] = None
502
503 hooks: Optional[Any] = None
504
505 rules: Optional[list[JobRule]] = None
506
507 variables: Optional[dict[str, JobVariables | str]] = None
508
509 cache: Optional[Any] = None
510
511 id_tokens: Optional[IdTokens] = None
512
513 identity: Optional[Identity] = None
514
515 inputs: Optional[BaseInput] = None
516
517 secrets: Optional[Any] = None
518
519 script: Optional[list[str] | str] = None
520
521 run: Optional[Any] = None
522 "Specifies a list of steps to execute in the job. The `run` keyword is an alternative to `script` and allows for more advanced job configuration."
523 "Each step is an object that defines a single task or command."
524 "Use either `run` or `script` in a job, but not both, otherwise the pipeline will error out."
525
526 stage: Optional[Stage | str] = None
527
528 only: Optional[Any] = None
529 "Job will run *only* when these filtering options match."
530
531 extends: Optional[str | list[str]] = None
532 "The name of one or more jobs to inherit configuration from."
533
534 needs: Optional[list[Needs | str]] = None
535 "The list of jobs in previous stages whose sole completion is needed to start the current job."
536
537 except_: Optional[Any] = pydantic.Field(serialization_alias="except", validation_alias="except", default=None)
538
539 tags: list[str] = pydantic.Field(default_factory=list)
540
541 allow_failure: Optional[list[bool | int | list[int]]] = None
542
543 timeout: Optional[str] = None
544 "Allows you to configure a timeout for a specific job (e.g. `1 minute`, `1h 30m 12s`)."
545 "https://docs.gitlab.com/ci/yaml/#timeout"
546
547 when: Optional[JobWhen] = None
548 "Describes the conditions for when to run the job. Defaults to 'on_success'."
549 "https://docs.gitlab.com/ci/yaml/#when"
550
551 start_in: Optional[str] = None
552 "Used in conjunction with 'when: delayed' to set how long to delay before starting a job. e.g. '5', 5 seconds, 30 minutes, 1 week, etc."
553 "https://docs.gitlab.com/ci/jobs/job_control/#run-a-job-after-a-delay"
554
555 manual_confirmation: Optional[str] = None
556 "Describes the Custom confirmation message for a manual job."
557 "https://docs.gitlab.com/ci/yaml/#when"
558
559 dependencies: Optional[list[str]] = None
560 "Specify a list of job names from earlier stages from which artifacts should be loaded."
561 "By default, all previous artifacts are passed."
562 "Use an empty array to skip downloading artifacts."
563
564 artifacts: Artifacts = pydantic.Field(default_factory=Artifacts)
565
566 environment: Optional[Any] = None
567
568 release: Optional[Any] = None
569
570 coverage: Optional[str] = None
571 "Must be a regular expression, optionally but recommended to be quoted, and must be surrounded with '/'."
572 "Example: '/Code coverage: \\d+\\.\\d+/'"
573
574 retry: Optional[Any] = None
575
576 parallel: Optional[Parallel | int] = None
577
578 interruptible: Optional[bool] = None
579 "Interruptible is used to indicate that a job should be canceled if made redundant by a newer pipeline run."
580 "https://docs.gitlab.com/ci/yaml/#interruptible)."
581
582 resource_group: Optional[str] = None
583 "Limit job concurrency. Can be used to ensure that the Runner will not run certain jobs simultaneously."
584
585 trigger: Optional[Trigger] = None
586
587 inherit: Optional[Any] = None
588
589 publish: Optional[str] = None
590 "Deprecated. Use `pages.publish` instead. A path to a directory that contains the files to be published with Pages."
591
592 pages: Optional[Any] = None
593
594
[docs]
595class Job(JobTemplate):
596 name: str = ""
597
598
599class WorkflowRule(Rule):
600 when: Optional[WorkflowWhen] = None
601
602 auto_cancel: Optional[WorkflowAutoCancel] = None
603
604
[docs]
605class Workflow(BaseModel):
606 name: Optional[str] = None
607 "Defines the pipeline name."
608 "https://docs.gitlab.com/ci/yaml/#workflowname"
609
610 auto_cancel: Optional[WorkflowAutoCancel] = None
611 "Define the rules for when pipeline should be automatically cancelled."
612
613 rules: Optional[list[WorkflowRule]] = None
614
615
[docs]
616class Default(BaseModel):
617 model_config = pydantic.ConfigDict(populate_by_name=True)
618
619 after_script: Optional[list[str] | str] = None
620
621 artifacts: Optional[Artifacts] = None
622
623 before_script: Optional[list[str] | str] = None
624
625 hooks: Optional[Any] = None
626
627 cache: Optional[Any] = None
628
629 image: Optional[Image] = None
630
631 interruptible: Optional[bool] = None
632 "Interruptible is used to indicate that a job should be canceled if made redundant by a newer pipeline run."
633 "https://docs.gitlab.com/ci/yaml/#interruptible"
634
635 id_tokens: Optional[IdTokens] = None
636 "Defines JWTs to be injected as environment variables."
637
638 identity: Optional[Identity] = None
639 "Sets a workload identity (experimental), allowing automatic authentication with the external system."
640 "https://docs.gitlab.com/ci/yaml/#identity"
641
642 retry: Optional[Any] = None
643 "Retry a job if it fails. Can be a simple integer or object definition."
644 "https://docs.gitlab.com/ci/yaml/#retry)."
645
646 services: Optional[Any] = None
647
648 tags: Optional[list[str]] = None
649
650 timeout: Optional[str] = None
651 "Allows you to configure a timeout for a specific job (e.g. `1 minute`, `1h 30m 12s`)."
652 "https://docs.gitlab.com/ci/yaml/#timeout"
653
654 # reference_: Optional[str] = pydantic.Field(
655 # serialization_alias="!reference", validation_alias="!reference", default=None
656 # )
657
658
[docs]
659class Pipeline(BaseModel):
660 model_config = pydantic.ConfigDict(
661 arbitrary_types_allowed=True,
662 )
663
664 jobs: list["Job | JobBuilderImpl"] = pydantic.Field(default_factory=lambda: [])
665
666 workflow: Optional[Workflow] = Workflow()
667
668 stages: Optional[list[Stage | str]] = None
669 "Groups jobs into stages. All jobs in one stage must complete before next stage is executed."
670 "https://docs.gitlab.com/ci/yaml/#stages"
671
672 include: list[IncludeComponent] = pydantic.Field(default_factory=lambda: [])
673
674 default: Optional[Default] = None
675
676 variables: Optional[dict[str, GlobalVariables]] = {}
677
[docs]
678 @staticmethod
679 def load_pipeline_default_schema() -> dict[str, Any]:
680 with open("gcip2/ci.json", "r") as f:
681 return json.loads(f.read())
682
[docs]
683 def validate_pipeline(self: Self) -> None:
684 try:
685 jsonschema.validate(
686 instance=self.dump(),
687 schema=self.load_pipeline_default_schema(),
688 )
689 except jsonschema.ValidationError as exc:
690 raise exc
691
692
693class JobBuilder:
694 def build(self) -> Job:
695 raise NotImplementedError
696
697
698@injector.inject
699@dataclasses.dataclass
700class JobBuilderImpl(JobBuilder):
701 __pydantic_config__ = pydantic.ConfigDict(arbitrary_types_allowed=True)
702
703 _base: ClassVar[type["JobBuilderImpl"] | None] = None
704 _name: ClassVar[str | Enum | None] = None
705
706 _secret_handler = SecretsHandler
707
708 _config: ProjectConfig
709 _di: injector.Injector
710 model: Job = dataclasses.field(init=False, default_factory=Job)
711
712 def __post_init__(self) -> None:
713 self._apply_name()
714
715 def _apply_base(self) -> None:
716 if self._base is None:
717 return
718
719 base = self._di.create_object(self._base)
720 base._apply_base()
721 base.apply()
722
723 base_model = base.model.model_dump(exclude_none=True)
724 current_model = self.model.model_dump(exclude_none=True)
725
726 merged = {
727 **base_model,
728 **current_model,
729 }
730
731 self.model = Job.model_validate(merged)
732
733 def _apply_name(self) -> None:
734 if self._name:
735 self.model.name = self._name if isinstance(self._name, str) else self._name.value
736
737 def apply(self: Self) -> Self:
738 return self
739
740 def build(self: Self) -> Job:
741 self._apply_base()
742 return self.model.model_copy(deep=True)
743
744 def with_name(self: Self, name: str) -> Self:
745 self.model.name = name
746 return self
747
748 def add_to_name(self: Self, suffix: str) -> Self:
749 if not self.model.name:
750 raise ValueError("Could not add suffix to job name because job name is not set.")
751 self.model.name += suffix
752 return self
753
754 def with_script(self: Self, script: str | list[str]) -> Self:
755 self.model.script = script
756 return self
757
758 def with_before_script(self: Self, script: str | list[str]) -> Self:
759 self.model.before_script = script
760 return self
761
762 def with_after_script(self: Self, script: str | list[str]) -> Self:
763 self.model.after_script = script
764 return self
765
766 def add_to_script(self: Self, script: str | list[str]) -> Self:
767 if isinstance(self.model.script, list):
768 if isinstance(script, str):
769 self.model.script.extend([script])
770 else:
771 self.model.script.extend(script)
772 return self
773
774 def with_image(self, image: str, entrypoint: list[str] | None = None) -> Self:
775 self.model.image = Image(
776 name=image,
777 entrypoint=entrypoint or [],
778 )
779 return self
780
781 def with_compose_image(self, image_name: str, entrypoint: list[str] | None = None) -> Self:
782 image_data = self._config.compose.images.get(image_name)
783 if not image_data:
784 raise ValueError(f"image_data not found in {self._config.compose.path.name}")
785
786 self.model.image = Image(
787 name=image_data.image,
788 entrypoint=entrypoint or [],
789 )
790 return self
791
792 def with_tags(self, tags: list[str]) -> Self:
793 self.model.tags = tags
794 return self
795
796 def with_stage(self, stage: str) -> Self:
797 self.model.stage = stage
798 return self
799
800 def with_when(self: Self, when: JobWhen) -> Self:
801 self.model.when = when
802 return self
803
804 def with_artifacts(
805 self,
806 paths: Optional[list[str]] = None,
807 exclude: Optional[list[str]] = None,
808 expire_in: Optional[str] = None,
809 expose_as: Optional[str] = None,
810 name: Optional[str] = None,
811 public: Optional[bool] = None,
812 access: Optional[ArtifactsAccess] = None,
813 reports: Optional[ArtifactsReports] = None,
814 untracked: Optional[bool] = None,
815 when: Optional[ArtifactsWhen] = None,
816 ) -> Self:
817 if public and access:
818 raise ValueError("Could not be set both parameters: public and access.")
819
820 self.model.artifacts = Artifacts(
821 paths=paths,
822 exclude=exclude,
823 expose_as=expose_as,
824 expire_in=expire_in,
825 name=name,
826 public=public,
827 access=access,
828 reports=reports,
829 untracked=untracked,
830 when=when,
831 )
832 return self
833
834 def with_needs(self, needs: list[Needs | str]) -> Self:
835 self.model.needs = needs
836 return self
837
838 def with_dependencies(self, dependencies: list[str]) -> Self:
839 self.model.dependencies = dependencies
840 return self
841
842 def update_variables(self, variables: dict[str, str | JobVariables]) -> Self:
843 if self.model.variables:
844 self.model.variables.update(variables)
845 else:
846 self.model.variables = variables
847 return self