Pipeline File
pyTooling.GitLab.PipelineFile models a GitLab CI/CD configuration file - the project’s
.gitlab-ci.yml, a file it includes, or the file of a child pipeline -, not a run of it (that’s
Pipeline Run):
from pathlib import Path
from pyTooling.GitLab.PipelineFile import PipelineFile
pipelineFile = PipelineFile.FromFile(Path(".gitlab-ci.yml"))
print(f"{pipelineFile}: {pipelineFile.JobCount} jobs in stages {', '.join(pipelineFile.Stages)}")
for job in pipelineFile.IterateJobs():
print(f" {job.Name:<24} {job.Stage:<10} {job.Image or ''}")
The file is read with ruamel.yaml, a requirement of this package.
The Tree
Every element knows its Parent, and holds a reference to the configuration
file it belongs to in PipelineFile.
A file may begin with a header document, separated from the configuration by ---, which holds the spec
key - see Inputs.
Stages
DeclaredStages holds the stages key as written, or None.
Stages holds the stages the pipeline runs, in their order: the
declared ones, or build, test and deploy (DEFAULT_STAGES) for a
file without stages, always between .pre and .post:
stages: # Stages: .pre, lint, build, .post
- lint
- build
Variables
A variable of the top-level variables is a Variable. Its
Value is the text GitLab reads - a number or a boolean written in the
file is its text, as 3.14 or true. A variable declared as mapping may also carry:
Description- GitLab prefills the variable in the form running a pipeline by hand, with the description beside it;Options- the values that form offers to select from;Expand-False, if variables in the value aren’t expanded.
variables:
DEPLOY_ENVIRONMENT:
value: "staging"
description: "The deployment target."
options: ["production", "staging"]
Jobs
A top-level key is a Job, unless it is a global keyword
(RESERVED_KEYS). A job whose name starts with a dot is a hidden job:
GitLab doesn’t run it, other jobs extend it. The file keeps both apart, in file order:
pipelineFile.Jobs # the jobs, by name
pipelineFile.HiddenJobs # the hidden jobs, by name
pipelineFile.ContainsJob(".template") # False: a hidden job isn't a job
A job runs in its Stage - test, if it names none -, which
FromFile() checks against the pipeline’s stages. The keys a job’s
documentation needs are read too:
Script,BeforeScriptandAfterScript- the lines, a single line as a list of one, nested lists flattened;When- aRuleOutcome;AllowFailureandAllowedExitCodes-allow_failureas a boolean, or as a mapping ofexit_codes;Image,ServicesandEnvironment- the names, written as a string or as thenameof a mapping;Tags;Variables- the job’s variables.
Dependencies Between Jobs
A job runs after the jobs of the earlier stages, unless it has a needs key: then it runs as soon as the jobs it
needs have run, and needs: [] lets it start with the pipeline. Needs
tells the three apart - None, the entries, or an empty tuple. Each entry is a
Need, whose Kind says what it
refers to:
|
|
|---|---|
|
|
|
|
|
|
|
|
Artifacts and IsOptional
hold artifacts and optional. NeededJobs resolves the needs of the
same pipeline to the jobs of the file:
for job in pipelineFile.IterateJobs():
if job.Needs is not None:
print(f"{job.Name} needs {', '.join(needed.Name for needed in job.NeededJobs)}")
FromFile() checks that a job needs only jobs of the file - an
optional need may name a missing job - and that the jobs don’t need each other in a cycle.
Rules and Workflow
A job’s rules decide whether it is added to a pipeline, the rules of the workflow key whether a pipeline
is created. GitLab evaluates them in order, and the first matching one decides; the model keeps them as written,
without evaluating them. A Rule holds:
Condition- the expression ofif;ChangesandCompareTo- the paths ofchanges, written as a list or aschanges.paths, andchanges.compare_to;Exists- the paths ofexists, as a list or asexists.paths;When,AllowFailureandVariables- what applies, if the rule matches.
PipelineFile.Workflow holds the
Workflow - the pipeline’s Name
and its Rules -, or None for a file without workflow:
workflow:
name: "Pipeline for $CI_COMMIT_REF_NAME"
rules:
- if: $CI_COMMIT_TAG
when: never
- when: always
Includes and Defaults
Each entry of include is an Include, whose
Kind says where the files come from and whose
Location names them, as written:
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
An include’s Inputs keep the types they are written with, and its
Rules decide whether the files are included. The included files are
read only to resolve extends and !reference tags, with an
IncludeResolver - see Extends, References and Defaults.
PipelineFile.Default holds the default key as a
Default - image, services, tags and the scripts around a job’s script. The
global keywords image, services, before_script and after_script, deprecated forms of default, are
read into it too, unless default sets them.
Parallel Jobs and Triggers
A job’s parallel is a Parallel: a
Count, or a Matrix
whose entries map variables to values. Combinations holds one
combination per instance, each entry contributing the product of its lists, and
InstanceNames the names GitLab gives the instances:
deploy: # deploy: [aws, monitoring], deploy: [aws, app1], deploy: [gcp, data]
parallel:
matrix:
- PROVIDER: aws
STACK: [monitoring, app1]
- PROVIDER: gcp
STACK: data
test: # test 1/3, test 2/3, test 3/3
parallel: 3
A need’s Parallel names the matrix instances it needs, written as
needs:parallel:matrix.
A job’s trigger is a Trigger. A trigger including files starts a child
pipeline - Includes holds the files as
Includes, also the artifact a job created for a dynamic child pipeline.
A trigger naming a Project - a string, or project of a mapping,
with an optional Branch - starts that project’s pipeline.
Strategy says whether the job waits for the pipeline.
Extends, References and Defaults
A job’s keys are merged as GitLab merges them, before the job is built:
!referencetags are replaced by what they refer to - in a list, a referred list is spliced in, as inscript: [!reference [.setup, script], make]. References nest up to 10 levels (MAX_REFERENCE_DEPTH).extendsmerges the jobs a job extends, in order, and then its own keys: mappings merge deeply, other values - lists included - replace, andnullremoves a key. Jobs extend each other up to 11 levels (MAX_EXTENDS_DEPTH).Extendskeeps the names.A job gets every key of
default, and of the global keywords standing for it, which it doesn’t set - unlessinherit.defaultisfalse, or lists the keys it inherits. Hidden jobs get none.
Every key keeps the line it is written at, also when another job contributes it.
A file may extend, or refer to, jobs of the files it includes. Read with an
IncludeResolver, the local files it includes - and those they include - are
read too, and their jobs and keys resolve; a key an included file contributes gets the line of the extends key or
the reference bringing it in. The included jobs aren’t jobs of the file:
pipelineFile = PipelineFile.FromFile(Path(".gitlab-ci.yml"), IncludeResolver(Path(".")))
A missing job or key is an error, unless the file includes files that weren’t read - another project’s files, a URL,
a template, a component, or a local file without a resolver. Then the job is built from what was read, and
Unresolved names what it lacks, as extends .template or
!reference [.setup, script].
Inputs
A CI/CD component’s template, or a file included with inputs, declares its parameters in the header document’s
spec:inputs. PipelineFile.Inputs holds them as
Inputs, and
PipelineFile.Description holds spec:description:
spec:
description: "Lints a Python package."
inputs:
stage:
default: test
description: "Stage of the job."
python:
options: ["3.13", "3.14"]
default: "3.14"
version:
regex: ^v\d+$
---
lint:
stage: $[[ inputs.stage ]]
An input has a Type - string, if it doesn’t say -, a
Default with the type it is written with, a
Description, Options, a
Regex and conditional Rules.
It is Required without a default key.
FromFile() checks that a default matches the type and the options.
A job’s inputs - set when the job is run or retried by hand - are inputs too, in
Job.Inputs; each needs a default.
A value interpolating an input, as $[[ inputs.stage ]], is kept as written: it is known when the file is included
only, so a stage or a needed job written that way isn’t checked.
Building a Pipeline
ToPipeline() builds the pipeline a file defines as a
pyTooling.CI model, as pyTooling maps GitLab CI onto it. Each element links back to the job it was built
from by Definition:
Job |
Element |
|---|---|
a job running a script |
|
a job with |
|
a trigger job |
|
a hidden job |
none |
The pipeline is named by workflow:name, or else by the file’s name. A job without needs needs every element
of the nearest earlier stage holding elements; needs replaces that, and needs: [] needs nothing. A need of
some instances of a matrix needs the whole matrix.
Each rule of a job becomes a Rule of the element, in the order GitLab evaluates them: its
condition is the rule’s if, its When the rule’s when, else the job’s, else
on_success. changes, compare_to, exists, allow_failure and variables are the rule’s
key-value-pairs. A job with when, but without rules, gets one rule without condition; workflow:rules are the
pipeline’s rules, deciding always unless they say never. The result converts into a graph:
from pathlib import Path
from pyTooling.GitLab.PipelineFile import IncludeResolver, PipelineFile
pipelineFile = PipelineFile.FromFile(Path(".gitlab-ci.yml"))
pipeline = pipelineFile.ToPipeline(IncludeResolver(Path(".")))
graph = pipeline.ToGraph()
An IncludeResolver reads a trigger job’s child pipeline from the repository’s
directory, if its files are local; the workflow then holds the child pipeline’s elements, and
ChildFiles the files. depth limits how many levels of
child pipelines are expanded.
Source Lines
Every element keeps the Line it is written at, starting at 1, so a
consumer can point at the place a finding comes from. A malformed file raises
PipelineFileError, which carries the file in
Path and the line in
Line, and names both in a note:
pyTooling.GitLab.PipelineFile.PipelineFileError: Variable 'A' has unknown key 'secret'.
In '.gitlab-ci.yml:4'.