Pipelines¶
A pipeline is defined in Python and rendered to YAML at build time. A project normally
has two entry points in ci.py:
Builder |
Produces |
|---|---|
|
|
|
|
Jobs¶
A job inherits from JobBuilderImpl and configures the underlying Job model in
apply():
class PreCommit(JobBuilderImpl):
_name = "pre-commit"
_base = base.BaseLinux
def apply(self):
self.model.script = ["pre-commit run -av"]
return self.with_stage(Stages.PRE_COMMIT)
_name sets the job name in the generated YAML. _base inherits configuration from
another builder — here BaseLinux, which supplies the shared before_script and
after_script. Values set by the job itself win over those from its base.
Jobs are created by self.job(), never by calling the class directly:
self.job(PreCommit).apply()
That is what gives a job self._config, the script assets, and everything else supplied
by the container. See Dependency injection.
Since the configuration is available, jobs can be generated from it:
class UnitTests(JobBuilderImpl):
_base = base.BasePython
def apply(self):
for module in self._config.extra.get("tests", {}).get("unit-tests", []):
...
Images are referenced by name from the project’s compose file rather than hard-coded:
self.with_compose_image("base_python")
Building a pipeline¶
class Stages(str, Enum):
PRE_COMMIT = "pre-commit"
class Pipeline(PipelineBuilderImpl):
def apply(self):
self.model.stages = [Stages.PRE_COMMIT]
self.add_jobs((self.job(PreCommit).apply(),))
self.with_workflow(workflow)
return self
build() fills in anything left unset: a default image taken from the compose file, the
static-k8s tag, and a single jobs stage. Setting them explicitly overrides those
defaults.
Workflow rules are usually defined once and shared between both builders:
workflow = Workflow(
name="default",
rules=[
WorkflowRule(
if_='$CI_PIPELINE_SOURCE == "merge_request_event"',
when=WorkflowWhen.ALWAYS,
),
WorkflowRule(
if_="$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH",
when=WorkflowWhen.ALWAYS,
),
WorkflowRule(when=WorkflowWhen.NEVER),
],
)
A parent_pipeline rule is prepended automatically, so a child pipeline is always
allowed to run when triggered by its parent.
Root configuration¶
class GitlabCi(GitlabCiBuilderImpl):
def apply(self):
super().apply()
self.with_workflow(workflow)
return self
When this builder declares no jobs, two trigger jobs are added for you: one that builds the child pipeline and one that triggers it from the resulting artifact.
Generated output¶
workflow:
name: default
rules:
- if: $CI_PIPELINE_SOURCE == "parent_pipeline"
when: always
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: always
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: always
- when: never
stages:
- pre-commit
default:
image:
name: python:3.12
tags:
- static-k8s
pre-commit:
before_script:
- |
#!/usr/bin/env sh
set -o errexit -o nounset -o xtrace
uv sync
. ".venv/bin/activate"
script:
- pre-commit run -av
stage: pre-commit
interruptible: true
The before_script block comes from BaseLinux and is not written in the job
definition.
Building¶
dothat run build-gitlab-ci
dothat run build-pipeline -f pipelines/my-pipeline/ci.py
Both commands log a diff against the previous version of the file, so changes to generated configuration are visible in review.