Source code for gcip2.pipeline_core

  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
[docs] 86class BaseInput(BaseModel): 87 type: Optional[BaseInputType] = BaseInputType.STRING 88 "Input type. Defaults to 'string' when not specified." 89 90 description: Optional[str] = None 91 "Human-readable explanation of the parameter." 92 93 options: Optional[list[BaseInputType]] = None 94 "List of allowed values for this input." 95 96 regex: Optional[str] = None 97 "Regular expression that string values must match." 98 99 default: Optional[Any] = None 100 "Default value for this input."
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