pipeforge 1.0.0__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.
- pipeforge/__init__.py +1201 -0
- pipeforge/__main__.py +208 -0
- pipeforge/_internal/__init__.py +0 -0
- pipeforge/_internal/core_loop.py +700 -0
- pipeforge/_internal/database.py +515 -0
- pipeforge/_internal/drawing.py +544 -0
- pipeforge/_internal/file_loader.py +654 -0
- pipeforge/_internal/inspector.py +555 -0
- pipeforge/_internal/params.py +148 -0
- pipeforge/_internal/pipeline.py +212 -0
- pipeforge/_internal/script.py +1004 -0
- pipeforge/_internal/utils.py +122 -0
- pipeforge/examples/hello_jenkins.toml +16 -0
- pipeforge/examples/hello_world.toml +67 -0
- pipeforge/examples/merge_pull_request.toml +240 -0
- pipeforge/examples/multi_pipeline.toml +93 -0
- pipeforge/examples/multi_pipeline_2.toml +54 -0
- pipeforge/examples/nightly.toml +19 -0
- pipeforge/examples/retries.toml +43 -0
- pipeforge/examples/scripts/hello_world__get_purpose_in_life.py +34 -0
- pipeforge/examples/scripts/hello_world__print_summary.py +27 -0
- pipeforge/examples/scripts/hello_world__repeat_purpose.py +33 -0
- pipeforge/examples/scripts/retry.py +25 -0
- pipeforge/examples/scripts/timeout.py +25 -0
- pipeforge/examples/timeout.toml +37 -0
- pipeforge-1.0.0.dist-info/METADATA +198 -0
- pipeforge-1.0.0.dist-info/RECORD +29 -0
- pipeforge-1.0.0.dist-info/WHEEL +4 -0
- pipeforge-1.0.0.dist-info/entry_points.txt +2 -0
pipeforge/__init__.py
ADDED
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A pipeline jobs orchestrator.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
================================================================================
|
|
7
|
+
1. OVERVIEW
|
|
8
|
+
================================================================================
|
|
9
|
+
|
|
10
|
+
The purpose of the "pipeforge" module is to be used as part of a continuous
|
|
11
|
+
integration / continuous delivery (CI/CD) system in two ways:
|
|
12
|
+
|
|
13
|
+
1. As the engine/orchestrator in charge of running each of the "jobs" that
|
|
14
|
+
make up each of the possible "processes" (aka. "pipelines") that can be
|
|
15
|
+
run in the CI/CD.
|
|
16
|
+
|
|
17
|
+
2. As a library those "jobs" must use to communicate with the orchestrator.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
21
|
+
1.1. "pipeforge" as an orchestrator
|
|
22
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
23
|
+
|
|
24
|
+
Let's say that one of these pipelines is the one triggered when a developer is
|
|
25
|
+
ready to merge his changes into the main repository branch. Let's say that this
|
|
26
|
+
pipeline is made up of the following jobs:
|
|
27
|
+
|
|
28
|
+
1. Statically analyze the source code
|
|
29
|
+
2. Build software package for Windows
|
|
30
|
+
3. Build software package for Linux
|
|
31
|
+
4. Run tests on Windows
|
|
32
|
+
5. Run tests on Linux
|
|
33
|
+
6. Send report email
|
|
34
|
+
|
|
35
|
+
Some of these jobs can be done in parallel, thus the pipeline looks more like
|
|
36
|
+
this:
|
|
37
|
+
|
|
38
|
+
Build for Windows Build for Linux Statically analyze
|
|
39
|
+
| | the source code
|
|
40
|
+
| | |
|
|
41
|
+
V V |
|
|
42
|
+
Test on Windows Test on Linux |
|
|
43
|
+
| | |
|
|
44
|
+
| | |
|
|
45
|
+
'---------------------+---+----------------'
|
|
46
|
+
|
|
|
47
|
+
V
|
|
48
|
+
Send report email
|
|
49
|
+
|
|
50
|
+
The "pipeforge" package includes an orchestrator that can help you take care of
|
|
51
|
+
what to run and when if you define the inputs, outputs and dependencies of the
|
|
52
|
+
different jobs using a *.toml file.
|
|
53
|
+
|
|
54
|
+
NOTE: A *.toml file can always be converted into a *.json file (and the
|
|
55
|
+
other way around). They are equivalent. The conversion is trivial once you
|
|
56
|
+
understand the syntax. You can also use an online tool to perform it
|
|
57
|
+
(example: https://pseitz.github.io/toml-to-json-online-converter/)
|
|
58
|
+
|
|
59
|
+
The only difference is that comments (which start with "#") are allowed in
|
|
60
|
+
*.toml files and not in *.json files. That's why I will be showing examples
|
|
61
|
+
in toml syntax, but be aware that pipeforge accepts both formats.
|
|
62
|
+
|
|
63
|
+
Following with the example, the *.toml could be something like this:
|
|
64
|
+
|
|
65
|
+
[[pipelines]]
|
|
66
|
+
name = "main"
|
|
67
|
+
|
|
68
|
+
[[pipelines.jobs]]
|
|
69
|
+
name = "Statically analyze the source code"
|
|
70
|
+
script = "static_analysis.py"
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
[pipelines.jobs.input]
|
|
74
|
+
developer_branch = "origin/update_dependencies_to_latest_version"
|
|
75
|
+
|
|
76
|
+
[pipelines.jobs.output]
|
|
77
|
+
report = "?"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
[[pipelines.jobs]]
|
|
81
|
+
name = "Build software package for Windows"
|
|
82
|
+
script = "build_windows.py"
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
[pipelines.jobs.input]
|
|
86
|
+
developer_branch = "origin/update_dependencies_to_latest_version"
|
|
87
|
+
|
|
88
|
+
[pipelines.jobs.output]
|
|
89
|
+
output_exe = "?"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
[[pipelines.jobs]]
|
|
93
|
+
name = "Build software package for Linux"
|
|
94
|
+
script = "build_linux.py"
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
[pipelines.jobs.input]
|
|
98
|
+
developer_branch = "origin/update_dependencies_to_latest_version"
|
|
99
|
+
|
|
100
|
+
[pipelines.jobs.output]
|
|
101
|
+
output_exe = "?"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
[[pipelines.jobs]]
|
|
105
|
+
name = "Run tests on Windows"
|
|
106
|
+
script = "test_windows.py"
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
[pipelines.jobs.input]
|
|
110
|
+
package = "@{Build software package for Windows::output_exe}"
|
|
111
|
+
|
|
112
|
+
[pipelines.jobs.output]
|
|
113
|
+
report = "?"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
[[pipelines.jobs]]
|
|
117
|
+
name = "Run tests on Linux"
|
|
118
|
+
script = "test_linux.py"
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
[pipelines.jobs.input]
|
|
122
|
+
package = "@{Build software package for Linux::output_exe}"
|
|
123
|
+
|
|
124
|
+
[pipelines.jobs.output]
|
|
125
|
+
report = "?"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
[[pipelines.jobs]]
|
|
129
|
+
name = "Send report email"
|
|
130
|
+
script = "send_email.py"
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
[pipelines.jobs.input]
|
|
134
|
+
static_analysis_report = "@{Statically analyze the source code::report}"
|
|
135
|
+
windows_test_report = "@{Run tests on Windows::report}"
|
|
136
|
+
linux_test_report = "@{Run tests on Linux::report}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
If you save that into a file called "merge_pull_request.toml" you could then use
|
|
140
|
+
"pipeforge" to automatically take care of the dependencies, figure out in which
|
|
141
|
+
order each job has to be executed (by analyzing which jobs have input parameters
|
|
142
|
+
that depend on other jobs output parameters) and then actually execute them:
|
|
143
|
+
|
|
144
|
+
import pipeforge
|
|
145
|
+
|
|
146
|
+
pipeforge.log_configure(...)
|
|
147
|
+
|
|
148
|
+
p = pipeline.Pipeline(..., "merge_pull_request.toml")
|
|
149
|
+
p.run(...)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
153
|
+
1.2. "pipeforge" as a library to access job parameters
|
|
154
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
155
|
+
|
|
156
|
+
Each job is associated to a script (for example, the "Send report email" job
|
|
157
|
+
will run the "send_email.py" script). When the orchestrator runs each of these
|
|
158
|
+
jobs, it will provide them a "token" that they can use to read the input
|
|
159
|
+
parameters (defined in the *.toml file) and write the output parameters (also
|
|
160
|
+
defined in the *.toml file) using a different "pipeforge" API:
|
|
161
|
+
|
|
162
|
+
import pipeforge
|
|
163
|
+
|
|
164
|
+
params = pipeforge.JobParams(token)
|
|
165
|
+
|
|
166
|
+
# Read input parameters
|
|
167
|
+
#
|
|
168
|
+
input_params = params.get_input_parameters_and_values()
|
|
169
|
+
|
|
170
|
+
# Do whatever the script is meant to do
|
|
171
|
+
#
|
|
172
|
+
do_something(input_params['number_of_fingers'])
|
|
173
|
+
...
|
|
174
|
+
|
|
175
|
+
# Write output parameters
|
|
176
|
+
#
|
|
177
|
+
output_params = {}
|
|
178
|
+
for x in params.get_output_parameters():
|
|
179
|
+
output_params[x] = ...
|
|
180
|
+
|
|
181
|
+
params.set_output_parameters_and_values(output_params)
|
|
182
|
+
|
|
183
|
+
The way this "token" is received depends on the mechanism the orchestrator uses
|
|
184
|
+
to run the scripts (more on this later, in the SCRIPT MANAGER section)
|
|
185
|
+
|
|
186
|
+
In summary: "pipeforge" helps you run scripts whose inputs and outputs form a
|
|
187
|
+
network of inter-dependencies if you first declare them as jobs using a special
|
|
188
|
+
*.toml file.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
================================================================================
|
|
193
|
+
2. SUPPORTING DATABASE
|
|
194
|
+
================================================================================
|
|
195
|
+
|
|
196
|
+
The way the pipeline runner (ie. the "orchestrator") communicates with the
|
|
197
|
+
script that is run on each job is through an external database.
|
|
198
|
+
|
|
199
|
+
This is how it works:
|
|
200
|
+
|
|
201
|
+
1. When the orchestrator is about to run a new job, it checks the list of
|
|
202
|
+
input parameters declared in the *.toml file.
|
|
203
|
+
|
|
204
|
+
2. If any of those parameters contains a reference ("@") to the output of
|
|
205
|
+
a previously executed job, the orchestrator will resolve them.
|
|
206
|
+
|
|
207
|
+
Example: "@{Run test on Linux::report"} could be resolved into
|
|
208
|
+
"/mnt/network/reports/test_14452.json"
|
|
209
|
+
|
|
210
|
+
3. The orchestrator saves all input parameters _names and values_ of the job
|
|
211
|
+
that is going to be run into an external database.
|
|
212
|
+
|
|
213
|
+
4. The orchestrator saves all output parameters _names_ of the job that is
|
|
214
|
+
going to be run into that same external database.
|
|
215
|
+
|
|
216
|
+
5. The orchestrator "somehow" runs the script associated to the job, and
|
|
217
|
+
provides it with a "token" that the script can later use to access the
|
|
218
|
+
external database information.
|
|
219
|
+
|
|
220
|
+
The way the script is run and the way the "token" is provided to the
|
|
221
|
+
script is up to the user of "pipeforge" by providing a special class that
|
|
222
|
+
inherits from "pipeforge.Script()" (more details on this later, in the
|
|
223
|
+
SCRIPT MANAGER section).
|
|
224
|
+
|
|
225
|
+
Some possibilities are:
|
|
226
|
+
|
|
227
|
+
- The script is run in the local machine as a background process and the
|
|
228
|
+
"token" is provided as an environment variable.
|
|
229
|
+
|
|
230
|
+
- The script is run remotely in a Jenkins instance that we trigger using
|
|
231
|
+
a REST API and the "token" is provided as one of the parameters in this
|
|
232
|
+
REST API call.
|
|
233
|
+
|
|
234
|
+
- Etc...
|
|
235
|
+
|
|
236
|
+
6. The script uses the provided "token" and the "pipeforge" API (more
|
|
237
|
+
specifically the "JobParams()" class) to read all input parameters'
|
|
238
|
+
values, read the requested list of output parameters and provide a value
|
|
239
|
+
for each of them.
|
|
240
|
+
|
|
241
|
+
This external database, as of today, must be a MongoDB instance whose URL is
|
|
242
|
+
provided when you create the "Pipeline" object:
|
|
243
|
+
|
|
244
|
+
import pipeforge
|
|
245
|
+
|
|
246
|
+
p = pipeline.Pipeline("mongodb://localhost:27017/pipelines", "merge_pull_request.toml")
|
|
247
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
248
|
+
You can easily deploy such a MongoDB instance using, for example, docker
|
|
249
|
+
(instructions here: https://hub.docker.com/_/mongo)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
================================================================================
|
|
254
|
+
3. TOML FILE FORMAT
|
|
255
|
+
================================================================================
|
|
256
|
+
|
|
257
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
258
|
+
3.1. Format description
|
|
259
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
260
|
+
|
|
261
|
+
The *.toml file that defines a flow contains one or more pipelines. Example:
|
|
262
|
+
|
|
263
|
+
[[pipelines]]
|
|
264
|
+
name = "main"
|
|
265
|
+
|
|
266
|
+
[[pipelines]]
|
|
267
|
+
name = "clean up"
|
|
268
|
+
|
|
269
|
+
[[pipelines]]
|
|
270
|
+
name = "recovery"
|
|
271
|
+
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
"name" is the name of the pipeline and can contain spaces. One of the pipelines
|
|
275
|
+
*must* be named "main" (this is the pipeline that will be executed when no
|
|
276
|
+
specific pipeline name is given). The others can be called whatever you want.
|
|
277
|
+
|
|
278
|
+
Each of these pipeline entries must contain one or more job definitions. Each of
|
|
279
|
+
them looks like this:
|
|
280
|
+
|
|
281
|
+
[[pipelines.jobs]]
|
|
282
|
+
name = <string>
|
|
283
|
+
script = <string>
|
|
284
|
+
runner = <string>
|
|
285
|
+
detached = <string>
|
|
286
|
+
timeout = <string>
|
|
287
|
+
retries = <string>
|
|
288
|
+
on_failure = <string>
|
|
289
|
+
on_input_err = <string>
|
|
290
|
+
|
|
291
|
+
[pipelines.jobs.input]
|
|
292
|
+
... = <string>
|
|
293
|
+
... = <string>
|
|
294
|
+
...
|
|
295
|
+
|
|
296
|
+
[pipelines.jobs.ouput]
|
|
297
|
+
... = "?"
|
|
298
|
+
... = "?"
|
|
299
|
+
...
|
|
300
|
+
|
|
301
|
+
"name" is just the name of the job, as you want it to appear in logs and
|
|
302
|
+
reports. It can contain spaces. You should make it descriptive but not very
|
|
303
|
+
long. It must be *unique* among all the jobs defined in the same pipeline.
|
|
304
|
+
|
|
305
|
+
Examples:
|
|
306
|
+
|
|
307
|
+
name = "Set globals"
|
|
308
|
+
name = "Take repository snapshot"
|
|
309
|
+
name = "Build FW"
|
|
310
|
+
name = "Static analysis of FW"
|
|
311
|
+
|
|
312
|
+
"script" is the name that "represents" the script that will run to perform the
|
|
313
|
+
expected job at each step. Depending on the "running mode" (more on this later,
|
|
314
|
+
in the SCRIPT MANAGER section), this can be the patch to a script in the local
|
|
315
|
+
system, the URL of a remote Jenkins REST API endpoint, a key of a dictionary
|
|
316
|
+
containing predefined scripts, etc...
|
|
317
|
+
|
|
318
|
+
Examples:
|
|
319
|
+
|
|
320
|
+
script = "/usr/local/bin/build_firmware.py"
|
|
321
|
+
script = "http://my.jenkins.local/jobs/build_firmware/run"
|
|
322
|
+
script = "build fw"
|
|
323
|
+
|
|
324
|
+
"runner" is a string that tells the orchestrator something about where (or how)
|
|
325
|
+
the script should be run. Depending on the "running mode" (more on this later,
|
|
326
|
+
in the SCRIPT MANAGER section), this can be a name of a specific remote machine,
|
|
327
|
+
a set of "tags" a Jenkins agent must contain, the name of a local container
|
|
328
|
+
image, etc...
|
|
329
|
+
|
|
330
|
+
Examples:
|
|
331
|
+
|
|
332
|
+
runner = "<unused>"
|
|
333
|
+
runner = "Linux machine"
|
|
334
|
+
runner = "LINUX+FAST+EUROPE+BIG_RAM"
|
|
335
|
+
runner = "
|
|
336
|
+
|
|
337
|
+
"detached" can be either "true" or "false". If "false" (which is the usual
|
|
338
|
+
case), the job will be part of the pipeline (ie. the job *must* have finished in
|
|
339
|
+
order for the pipeline to finish). If "true", once the job is triggered the
|
|
340
|
+
orchestrator will forget about it... in particular:
|
|
341
|
+
|
|
342
|
+
- A detached job can fail and the orchestrator will ignore it
|
|
343
|
+
- A detached job can still be running when the orchestrator decides to end
|
|
344
|
+
- Other jobs cannot depend on output parameters from a detached job
|
|
345
|
+
- A detached job does not have a "timeout" property
|
|
346
|
+
- A detached job does not have a "retries" property
|
|
347
|
+
- A detached job does not have a "on_failure" property
|
|
348
|
+
|
|
349
|
+
Examples:
|
|
350
|
+
|
|
351
|
+
detached = "true"
|
|
352
|
+
detached = "false"
|
|
353
|
+
|
|
354
|
+
"timeout" is how much time the job can be running before the orchestrator
|
|
355
|
+
decides to kill it (and return an error).
|
|
356
|
+
|
|
357
|
+
Examples:
|
|
358
|
+
|
|
359
|
+
timeout = "20 minutes"
|
|
360
|
+
timeout = "1 hour"
|
|
361
|
+
timeout = "N/A" <------ mandatory value when detached = "true"
|
|
362
|
+
|
|
363
|
+
"retries" is the number of times a job should be re-attempted in case it fails.
|
|
364
|
+
Ideally we would always want to set it to "0" but there are some times when we
|
|
365
|
+
know a particular job is prone fail due to external factors (network issues, low
|
|
366
|
+
hard disk space, etc...).
|
|
367
|
+
|
|
368
|
+
Examples:
|
|
369
|
+
|
|
370
|
+
retries = "0"
|
|
371
|
+
retries = "1"
|
|
372
|
+
retries = "9"
|
|
373
|
+
retries = "N/A" <------ mandatory value when detached = "true"
|
|
374
|
+
|
|
375
|
+
"on_failure" is a string that indicates what happens after all retries (if any)
|
|
376
|
+
have been exhausted and the job is still failing. It can take one of these
|
|
377
|
+
values:
|
|
378
|
+
|
|
379
|
+
- "continue"
|
|
380
|
+
|
|
381
|
+
Keep running the pipeline as if nothing had happened. Note that when this
|
|
382
|
+
happens output parameters might not have been set and later jobs that
|
|
383
|
+
depend on them need to take this possibility into consideration (ie. they
|
|
384
|
+
will receive "?" as the parameter's value, and they need how to handle
|
|
385
|
+
that case)
|
|
386
|
+
|
|
387
|
+
- "stop pipeline"
|
|
388
|
+
|
|
389
|
+
All pending jobs are canceled and the pipeline is immediately stopped
|
|
390
|
+
returning ERROR
|
|
391
|
+
|
|
392
|
+
- "trigger pipeline : <pipeline name>"
|
|
393
|
+
|
|
394
|
+
All pending jobs are canceled and pipeline <pipeline name> is triggered
|
|
395
|
+
(note that it can be the same pipeline that was executing).
|
|
396
|
+
|
|
397
|
+
<pipeline name> must match the "name" property of one of the pipelines
|
|
398
|
+
defined in the *.toml file.
|
|
399
|
+
|
|
400
|
+
- "N/A"
|
|
401
|
+
|
|
402
|
+
You *must* use this value when parameter "detached" is set to "true".
|
|
403
|
+
This makes sense as detached jobs are not monitored and we cannot take
|
|
404
|
+
any action once it finishes (failing or not)
|
|
405
|
+
|
|
406
|
+
Examples:
|
|
407
|
+
|
|
408
|
+
on_failure = "continue"
|
|
409
|
+
on_failure = "stop pipeline"
|
|
410
|
+
on_failure = "trigger pipeline : clean up"
|
|
411
|
+
on_failure = "N/A" <------ mandatory value when detached = "true"
|
|
412
|
+
|
|
413
|
+
"on_input_err" is a string that indicates what happens when one or more of the
|
|
414
|
+
input parameters are set to "?" (this is better explained later on). It can take
|
|
415
|
+
one of these values:
|
|
416
|
+
|
|
417
|
+
- "run"
|
|
418
|
+
|
|
419
|
+
Run the job normally. It will be the job responsibility to figure out what
|
|
420
|
+
to do when reading the input parameter that returns "?" (for example, it
|
|
421
|
+
might decide that the parameter is critical and immediately fail or, in
|
|
422
|
+
the case of not-so-critical parameters, provide an alternative default
|
|
423
|
+
value)
|
|
424
|
+
|
|
425
|
+
- "fail"
|
|
426
|
+
|
|
427
|
+
Act as if the first instruction of the job was "exit -1" and set the
|
|
428
|
+
status of the job to "FAILURE".
|
|
429
|
+
Note what when this happens output parameters of this job will not be set
|
|
430
|
+
(ie. they will be passed down the pipeline with a value of "?")
|
|
431
|
+
|
|
432
|
+
- "succeed"
|
|
433
|
+
|
|
434
|
+
Act as if the first instruction of the job was "exit 0" and set the status
|
|
435
|
+
of the job to "SUCCESS".
|
|
436
|
+
Note what when this happens output parameters of this job will not be set
|
|
437
|
+
(ie. they will be passed down the pipeline with a value of "?")
|
|
438
|
+
|
|
439
|
+
- "skip"
|
|
440
|
+
|
|
441
|
+
Act as if the first instruction of the job was "exit 0" and set the
|
|
442
|
+
status of the job to "SKIPPED".
|
|
443
|
+
Note what when this happens output parameters of this job will not be set
|
|
444
|
+
(ie. they will be passed down the pipeline with a value of "?")
|
|
445
|
+
|
|
446
|
+
Each job *can* (ie. it's optional) define a set of input parameters. They can be
|
|
447
|
+
one of these:
|
|
448
|
+
|
|
449
|
+
- A "fixed" string (ex: "developer_branch" from the "Statically analyze the
|
|
450
|
+
source code" job). Some more examples:
|
|
451
|
+
|
|
452
|
+
[pipelines.jobs.input]
|
|
453
|
+
name = "Peter"
|
|
454
|
+
surname = "La Anguila"
|
|
455
|
+
age = "39"
|
|
456
|
+
|
|
457
|
+
NOTE: If the input parameter name contains one or more dots ("."), you
|
|
458
|
+
will need to enclose it in double quotes, like this:
|
|
459
|
+
|
|
460
|
+
[pipelines.jobs.input]
|
|
461
|
+
name = "Peter"
|
|
462
|
+
surname = "La Anguila"
|
|
463
|
+
"my.age" = "39"
|
|
464
|
+
|
|
465
|
+
NOTE: If you decide to use JSON instead of TOML this is not something you
|
|
466
|
+
need to worry about, as in JSON *all* parameter names must be enclosed in
|
|
467
|
+
double quotes anyway.
|
|
468
|
+
|
|
469
|
+
- A string that contains one or more references to the output parameters of
|
|
470
|
+
other jobs (ex: "package" from "Run tests in Linux" contains a reference to
|
|
471
|
+
parameter "output_exe" from the "Build software package for Linux" job).
|
|
472
|
+
In this case the orchestrator will first "resolve" all these references so
|
|
473
|
+
that what is saved into the database (for the job to later query) is a
|
|
474
|
+
"fix" string at the end.
|
|
475
|
+
|
|
476
|
+
The format of a reference is this one:
|
|
477
|
+
|
|
478
|
+
@{<job name>::<output parameter name>}
|
|
479
|
+
|
|
480
|
+
Some more examples:
|
|
481
|
+
|
|
482
|
+
[pipelines.jobs.input]
|
|
483
|
+
test_machine = "@{Select machine::selection}"
|
|
484
|
+
binary_to_install = "@{Build binary::package_path}/windows/msword.exe"
|
|
485
|
+
report_receivers = "@{Build binary::author},@{Static analysis::author}"
|
|
486
|
+
|
|
487
|
+
Note that if a job wants to wait for another one to finish but does not
|
|
488
|
+
depend on any specific output parameter from it, you can omit the
|
|
489
|
+
"::<output_param_name>" part from the end. Example:
|
|
490
|
+
|
|
491
|
+
[pipelines.jobs.input]
|
|
492
|
+
wait_for_builds = "@{Build firmware}, @{Build platform}"
|
|
493
|
+
|
|
494
|
+
In this case, the value of @{...} will be expanded into the exit status of
|
|
495
|
+
the referenced job, which can be either "SUCCESS" or "FAILURE". This means
|
|
496
|
+
that if you were to read the value of input parameter "wait_for_builds"
|
|
497
|
+
from the previous example (but, why would you want to do that?), you could
|
|
498
|
+
get something like this:
|
|
499
|
+
|
|
500
|
+
"SUCCESS, SUCCESS"
|
|
501
|
+
|
|
502
|
+
In addition to all input parameters explicitly defined in the *.toml file, a job
|
|
503
|
+
will always receive a set of "hidden" input parameters that start with "__"
|
|
504
|
+
(example: "__pipeline_id", which tells the job the ID of the pipeline that the
|
|
505
|
+
job is part of). For more details, check the documentation of
|
|
506
|
+
pipeforge._internal.params.JobParams.get_input_parameters_and_values.
|
|
507
|
+
|
|
508
|
+
Note that the "on_input_err" condition will be triggered when:
|
|
509
|
+
|
|
510
|
+
- One or more of the input parameters are set to exactly "?"
|
|
511
|
+
|
|
512
|
+
- One or more of the input parameters are referencing one or more output
|
|
513
|
+
parameters from other jobs and one of the resolved values is "?", even if
|
|
514
|
+
that resolved value is just a piece of the full input string. In other
|
|
515
|
+
words, if an input parameter is defined like this:
|
|
516
|
+
|
|
517
|
+
number_of_cows = "Total: @{Cow counter::output}"
|
|
518
|
+
|
|
519
|
+
...then, if "@{Cow counter::output}" turns out to be "?", the
|
|
520
|
+
"on_input_err" condition will be triggered even though the actual input
|
|
521
|
+
parameter is "Total: ?" and not just "?"
|
|
522
|
+
|
|
523
|
+
- One or more of the input parameters are referencing another job directly
|
|
524
|
+
(ex: "@{Cow counter}") instead of a job output parameter (ex:
|
|
525
|
+
"@{Cow counter::output}") and that job exit status is different from
|
|
526
|
+
SUCCESS.
|
|
527
|
+
|
|
528
|
+
Each job *can* (ie. it's optional) define a set of output parameters. They must
|
|
529
|
+
always set to "?" in the *.toml file and the associated job must always set it
|
|
530
|
+
to some value or else the orchestrator will complain. This is by design: all
|
|
531
|
+
output parameters are mandatory.
|
|
532
|
+
|
|
533
|
+
Examples:
|
|
534
|
+
|
|
535
|
+
[pipelines.jobs.output]
|
|
536
|
+
report_path = "?"
|
|
537
|
+
result = "?"
|
|
538
|
+
|
|
539
|
+
NOTE: As it was the case with input parameters, if your output parameters
|
|
540
|
+
contain one or more dots (".") in their name, you need to enclosure them in
|
|
541
|
+
double quotes.
|
|
542
|
+
|
|
543
|
+
There is also another section in the *.toml file called [config] which is
|
|
544
|
+
*optional* and is currently only used to contain the "run_always" parameter
|
|
545
|
+
described in the next section.
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
549
|
+
3.2. Additional considerations: syntactic sugar
|
|
550
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
551
|
+
|
|
552
|
+
The "strict" format of the TOML file is what we just described in the previous
|
|
553
|
+
section. What comes next is just "syntactic sugar", which means it will get
|
|
554
|
+
"expanded" into the "strict" format before pipeforge processes it.
|
|
555
|
+
|
|
556
|
+
"Syntactic sugar" exists for the user convenience, to make the file less
|
|
557
|
+
verbose.
|
|
558
|
+
|
|
559
|
+
* Environment variables expansion
|
|
560
|
+
|
|
561
|
+
For convenience, before processing the *.toml file, the orchestrator will
|
|
562
|
+
search for all references to variables named ${PIPEFORGE__...} and replace
|
|
563
|
+
them by the value of the equally named environment variable.
|
|
564
|
+
|
|
565
|
+
Example:
|
|
566
|
+
|
|
567
|
+
[[pipelines.jobs]]
|
|
568
|
+
...
|
|
569
|
+
[pipelines.job.input]
|
|
570
|
+
development_branch = "${PIPEFORGE__PR_BRANCH_NAME}"
|
|
571
|
+
|
|
572
|
+
* Single pipeline file
|
|
573
|
+
|
|
574
|
+
If your file is only going to define one pipeline, you don't need to
|
|
575
|
+
create the "pipelines" list of entries: It will be automatically be
|
|
576
|
+
created for you and named "main".
|
|
577
|
+
|
|
578
|
+
In other words, a file which contains this...
|
|
579
|
+
|
|
580
|
+
[[pipelines]]
|
|
581
|
+
name = "main"
|
|
582
|
+
|
|
583
|
+
[[pipelines.jobs]]
|
|
584
|
+
...
|
|
585
|
+
[[pipelines.jobs]]
|
|
586
|
+
...
|
|
587
|
+
[[pipelines.jobs]]
|
|
588
|
+
...
|
|
589
|
+
[[pipelines.jobs]]
|
|
590
|
+
...
|
|
591
|
+
|
|
592
|
+
...is equivalent to another file that only contains this:
|
|
593
|
+
|
|
594
|
+
[[jobs]]
|
|
595
|
+
...
|
|
596
|
+
[[jobs]]
|
|
597
|
+
...
|
|
598
|
+
[[jobs]]
|
|
599
|
+
...
|
|
600
|
+
[[jobs]]
|
|
601
|
+
...
|
|
602
|
+
|
|
603
|
+
* Global parameters
|
|
604
|
+
|
|
605
|
+
Each job must always define all the expected parameters ("name", "script",
|
|
606
|
+
"runner", etc...). There are no default values if you forget one!
|
|
607
|
+
Pipeforge will fail if it detects a missing parameter.
|
|
608
|
+
|
|
609
|
+
In order to be able to work around this fact, the [global] section was
|
|
610
|
+
introduced. It works like this: whatever you place inside the [global]
|
|
611
|
+
section will be "injected" into each of the [[pipelines.jobs]] on each of
|
|
612
|
+
the [[pipelines]] under the hood.
|
|
613
|
+
|
|
614
|
+
For example, instead of writing this...
|
|
615
|
+
|
|
616
|
+
[[pipelines]]
|
|
617
|
+
name = "main"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
[[pipelines.jobs]]
|
|
621
|
+
name = "Compile product A"
|
|
622
|
+
script = "compile_a.py"
|
|
623
|
+
runner = "linux"
|
|
624
|
+
detached = "false"
|
|
625
|
+
timeout = "1 minutes"
|
|
626
|
+
retries = "0"
|
|
627
|
+
on_failure = "stop pipeline"
|
|
628
|
+
on_input_err = "fail"
|
|
629
|
+
|
|
630
|
+
[pipelines.jobs.input]
|
|
631
|
+
version = "v1.0"
|
|
632
|
+
flavor = "debug"
|
|
633
|
+
|
|
634
|
+
[pipelines.jobs.output]
|
|
635
|
+
binaries_path = "?"
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
[[pipelines.jobs]]
|
|
639
|
+
name = "Compile product B"
|
|
640
|
+
script = "compile_b.py"
|
|
641
|
+
runner = "windows"
|
|
642
|
+
detached = "false"
|
|
643
|
+
timeout = "1 minutes"
|
|
644
|
+
retries = "0"
|
|
645
|
+
on_failure = "stop pipeline"
|
|
646
|
+
on_input_err = "fail"
|
|
647
|
+
|
|
648
|
+
[pipelines.jobs.input]
|
|
649
|
+
version = "v1.0"
|
|
650
|
+
flavor = "release"
|
|
651
|
+
|
|
652
|
+
[pipelines.jobs.output]
|
|
653
|
+
binaries_path = "?"
|
|
654
|
+
unit_tests_results = "?"
|
|
655
|
+
|
|
656
|
+
...we could write this (which is 100% equivalent and shorter):
|
|
657
|
+
|
|
658
|
+
[global]
|
|
659
|
+
runner = "linux"
|
|
660
|
+
detached = "false"
|
|
661
|
+
timeout = "1 minutes"
|
|
662
|
+
retries = "0"
|
|
663
|
+
on_failure = "stop pipeline"
|
|
664
|
+
on_input_err = "fail"
|
|
665
|
+
|
|
666
|
+
[global.input]
|
|
667
|
+
version = "v1.0"
|
|
668
|
+
flavor = "debug"
|
|
669
|
+
|
|
670
|
+
[global.output]
|
|
671
|
+
binaries_path = "?"
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
[[pipelines]]
|
|
675
|
+
name = "main"
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
[[pipelines.jobs]]
|
|
679
|
+
name = "Compile product A"
|
|
680
|
+
script = "compile_a.py"
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
[[pipelines.jobs]]
|
|
684
|
+
name = "Compile product B"
|
|
685
|
+
script = "compile_b.py"
|
|
686
|
+
runner = "windows"
|
|
687
|
+
|
|
688
|
+
[pipelines.jobs.input]
|
|
689
|
+
flavor = "release"
|
|
690
|
+
|
|
691
|
+
[pipelines.jobs.output]
|
|
692
|
+
unit_tests_results = "?"
|
|
693
|
+
|
|
694
|
+
Notice how the "global" section parameters are only "injected" into a
|
|
695
|
+
given [[pipelines.job]] if it does not already define a value for it (ie.
|
|
696
|
+
whatever we put in [[pipelines.job]] will always have precedence)
|
|
697
|
+
|
|
698
|
+
* Implicit pipelines
|
|
699
|
+
|
|
700
|
+
If you use special value "retrigger without : ..." in the "on_failure" job
|
|
701
|
+
parameter, this is what will happen:
|
|
702
|
+
|
|
703
|
+
1. A new pipeline will automatically be created for you which contains
|
|
704
|
+
the same jobs as the current pipeline *except* for the ones listed
|
|
705
|
+
after the ":" (example: "retrigger without : @{Test 1}, @{Test 2}")
|
|
706
|
+
|
|
707
|
+
2. The original job "on_failure" parameter value will be replaced by
|
|
708
|
+
"trigger pipeline : <name_of_the_new_pipeline>"
|
|
709
|
+
|
|
710
|
+
In other words, instead of writing this...
|
|
711
|
+
|
|
712
|
+
[[pipelines]]
|
|
713
|
+
name = "main"
|
|
714
|
+
|
|
715
|
+
[[pipelines.jobs]]
|
|
716
|
+
name = "Compile"
|
|
717
|
+
script = "compile.py"
|
|
718
|
+
...
|
|
719
|
+
|
|
720
|
+
[[pipelines.jobs]]
|
|
721
|
+
name = "Test 1"
|
|
722
|
+
script = "test_1.py"
|
|
723
|
+
...
|
|
724
|
+
|
|
725
|
+
[[pipelines.jobs]]
|
|
726
|
+
name = "Test 2"
|
|
727
|
+
script = "test_2.py"
|
|
728
|
+
...
|
|
729
|
+
|
|
730
|
+
[[pipelines.jobs]]
|
|
731
|
+
name = "Merge"
|
|
732
|
+
script = "merge.py"
|
|
733
|
+
on_failure = "trigger pipeline : auto_pipeline_1"
|
|
734
|
+
...
|
|
735
|
+
|
|
736
|
+
[pipelines.jobs.input]
|
|
737
|
+
results_1 = "@{Test 1::results}"
|
|
738
|
+
results_2 = "@{Test 2::results}"
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
[[pipelines]]
|
|
742
|
+
name = "auto_pipeline_1"
|
|
743
|
+
|
|
744
|
+
[[pipelines.jobs]]
|
|
745
|
+
name = "Compile"
|
|
746
|
+
script = "compile.py"
|
|
747
|
+
...
|
|
748
|
+
|
|
749
|
+
[[pipelines.jobs]]
|
|
750
|
+
name = "Merge"
|
|
751
|
+
script = "merge.py"
|
|
752
|
+
on_failure = "trigger pipeline : auto_pipeline_1"
|
|
753
|
+
...
|
|
754
|
+
|
|
755
|
+
[pipelines.jobs.input]
|
|
756
|
+
results_1 = "!!@{Compile}!!"
|
|
757
|
+
results_2 = "!!@{Compile}!!"
|
|
758
|
+
|
|
759
|
+
...we could write this (which is 100% equivalent and shorter):
|
|
760
|
+
|
|
761
|
+
[[pipelines]]
|
|
762
|
+
name = "main"
|
|
763
|
+
|
|
764
|
+
[[pipelines.jobs]]
|
|
765
|
+
name = "Compile"
|
|
766
|
+
script = "compile.py"
|
|
767
|
+
...
|
|
768
|
+
|
|
769
|
+
[[pipelines.jobs]]
|
|
770
|
+
name = "Test 1"
|
|
771
|
+
script = "test_1.py"
|
|
772
|
+
...
|
|
773
|
+
|
|
774
|
+
[[pipelines.jobs]]
|
|
775
|
+
name = "Test 2"
|
|
776
|
+
script = "test_2.py"
|
|
777
|
+
...
|
|
778
|
+
|
|
779
|
+
[[pipelines.jobs]]
|
|
780
|
+
name = "Merge"
|
|
781
|
+
script = "merge.py"
|
|
782
|
+
on_failure = "retrigger without : @{Test 1}, @{Test 2}"
|
|
783
|
+
...
|
|
784
|
+
|
|
785
|
+
[pipelines.jobs.input]
|
|
786
|
+
results_1 = "@{Test 1::results}"
|
|
787
|
+
results_2 = "@{Test 2::results}"
|
|
788
|
+
|
|
789
|
+
...or, taking advantage of what was previously explained regarding single
|
|
790
|
+
pipeline files, this (which is even shorter):
|
|
791
|
+
|
|
792
|
+
[[jobs]]
|
|
793
|
+
name = "Compile"
|
|
794
|
+
script = "compile.py"
|
|
795
|
+
...
|
|
796
|
+
|
|
797
|
+
[[jobs]]
|
|
798
|
+
name = "Test 1"
|
|
799
|
+
script = "test_1.py"
|
|
800
|
+
...
|
|
801
|
+
|
|
802
|
+
[[jobs]]
|
|
803
|
+
name = "Test 2"
|
|
804
|
+
script = "test_2.py"
|
|
805
|
+
...
|
|
806
|
+
|
|
807
|
+
[[jobs]]
|
|
808
|
+
name = "Merge"
|
|
809
|
+
script = "merge.py"
|
|
810
|
+
on_failure = "retrigger without : @{Test 1}, @{Test 2}"
|
|
811
|
+
...
|
|
812
|
+
|
|
813
|
+
[jobs.input]
|
|
814
|
+
results_1 = "@{Test 1::results}"
|
|
815
|
+
results_2 = "@{Test 2::results}"
|
|
816
|
+
|
|
817
|
+
Note that when jobs are "removed" this way, dependencies are re-adjusted
|
|
818
|
+
in a way that "makes sense". In the example above, the "Merge" job in the
|
|
819
|
+
new "auto_pipeline_1" pipeline depends on "Compile":
|
|
820
|
+
|
|
821
|
+
[[jobs]]
|
|
822
|
+
name = "Merge"
|
|
823
|
+
script = "merge.py"
|
|
824
|
+
on_failure = "trigger pipeline : auto_pipeline_1"
|
|
825
|
+
...
|
|
826
|
+
|
|
827
|
+
[jobs.input]
|
|
828
|
+
results_1 = "!!@{Compile}!!" # <----- HERE!
|
|
829
|
+
results_2 = "!!@{Compile}!!" # <----- HERE!
|
|
830
|
+
|
|
831
|
+
This means they will receive the execution status of "Compile" (which is
|
|
832
|
+
probably useless in this context) but, this way, we make sure "Merge" will
|
|
833
|
+
not be executed until "Compile" has finished (as it was the case in the
|
|
834
|
+
original "main" pipeline). Some extra considerations:
|
|
835
|
+
|
|
836
|
+
- The replacement is the status of all dependencies of the job that is
|
|
837
|
+
being removed (concatenated by a "+" sign).
|
|
838
|
+
In the example there was just one dependency ("Compile") but in other
|
|
839
|
+
scenarios you could end up with something like this (dependencies
|
|
840
|
+
status concatenated by "+"):
|
|
841
|
+
|
|
842
|
+
results_1 = "!!@{Prepare}+@{Compile}!!"
|
|
843
|
+
|
|
844
|
+
- The replaced string will be surrounded by a pair of "!!".
|
|
845
|
+
This is to let you know that whatever value you were expecting here
|
|
846
|
+
has been replaced by the dependencies status implicit pipeline
|
|
847
|
+
mechanism.
|
|
848
|
+
|
|
849
|
+
* [meta] section
|
|
850
|
+
|
|
851
|
+
You can add a [meta] section at the top of the file and whatever is found
|
|
852
|
+
inside will be ignored. This can be used to document the pipeline. Why
|
|
853
|
+
not just use comments? For two reasons:
|
|
854
|
+
|
|
855
|
+
1. While TOML files accept comments (line that start with a "#"), JSON
|
|
856
|
+
files do not!
|
|
857
|
+
|
|
858
|
+
3. Using a dedicated section allows you to have structured data which
|
|
859
|
+
is easier to "extract" by other tools.
|
|
860
|
+
|
|
861
|
+
Example:
|
|
862
|
+
|
|
863
|
+
[meta]
|
|
864
|
+
author = "John Cobra"
|
|
865
|
+
version = "1.7"
|
|
866
|
+
description = "Continuous integration pipeline for making pancakes"
|
|
867
|
+
|
|
868
|
+
* Shortcut to always run a given job
|
|
869
|
+
|
|
870
|
+
If a parameter called "run_always" is found in the optional [config]
|
|
871
|
+
section, then all job names it contains will "run always". Each job
|
|
872
|
+
reference in this string must be provided using the "@{<job_name>}"
|
|
873
|
+
syntax. Example: if we want to always run jobs "Job M" and "Job S" we
|
|
874
|
+
would use this:
|
|
875
|
+
|
|
876
|
+
[config]
|
|
877
|
+
run_always = "@{Job M}, @{Job S}"
|
|
878
|
+
|
|
879
|
+
In practice this means that if a pipeline contains at least one job listed
|
|
880
|
+
in "run_always", this is what happens under the hood:
|
|
881
|
+
|
|
882
|
+
1. The "on_failure" property of all jobs in that pipeline is changed
|
|
883
|
+
to "continue", overwriting the previous value.
|
|
884
|
+
|
|
885
|
+
2. The "on_input_err" property of all jobs in that pipeline (except
|
|
886
|
+
for those in the "run_always" list) is changed to "skip".
|
|
887
|
+
|
|
888
|
+
3. The "on_input_err" property of all jobs in that pipeline that are
|
|
889
|
+
listed in the "run_always" list is changed to "run".
|
|
890
|
+
|
|
891
|
+
This is better understood with an example. Let's say we have this
|
|
892
|
+
pipeline:
|
|
893
|
+
|
|
894
|
+
A ---> B ---.
|
|
895
|
+
+ ---> E
|
|
896
|
+
C ---> D ---.
|
|
897
|
+
|
|
898
|
+
...defined like this:
|
|
899
|
+
|
|
900
|
+
[[pipelines.jobs]]
|
|
901
|
+
name = "A"
|
|
902
|
+
on_failure = "stop pipeline"
|
|
903
|
+
on_input_err = "fail"
|
|
904
|
+
...
|
|
905
|
+
|
|
906
|
+
[[pipelines.jobs]]
|
|
907
|
+
name = "B"
|
|
908
|
+
on_failure = "stop pipeline"
|
|
909
|
+
on_input_err = "fail"
|
|
910
|
+
...
|
|
911
|
+
|
|
912
|
+
[[pipelines.jobs]]
|
|
913
|
+
name = "C"
|
|
914
|
+
on_failure = "stop pipeline"
|
|
915
|
+
on_input_err = "fail"
|
|
916
|
+
...
|
|
917
|
+
|
|
918
|
+
[[pipelines.jobs]]
|
|
919
|
+
name = "D"
|
|
920
|
+
on_failure = "stop pipeline"
|
|
921
|
+
on_input_err = "fail"
|
|
922
|
+
...
|
|
923
|
+
|
|
924
|
+
[[pipelines.jobs]]
|
|
925
|
+
name = "E"
|
|
926
|
+
on_failure = "stop pipeline"
|
|
927
|
+
on_input_err = "fail"
|
|
928
|
+
...
|
|
929
|
+
|
|
930
|
+
...but we want "E" (which could be, for example, a stats collecting job)
|
|
931
|
+
to always run even if any of the previous jobs fails.
|
|
932
|
+
|
|
933
|
+
In order to achieve this we could add this at the top of the file:
|
|
934
|
+
|
|
935
|
+
[config]
|
|
936
|
+
run_always = "@{E}"
|
|
937
|
+
|
|
938
|
+
...which effectively "converts" the original pipeline definition file into
|
|
939
|
+
this:
|
|
940
|
+
|
|
941
|
+
[[pipelines.jobs]]
|
|
942
|
+
name = "A"
|
|
943
|
+
on_failure = "continue"
|
|
944
|
+
on_input_err = "skip"
|
|
945
|
+
...
|
|
946
|
+
|
|
947
|
+
[[pipelines.jobs]]
|
|
948
|
+
name = "B"
|
|
949
|
+
on_failure = "continue"
|
|
950
|
+
on_input_err = "skip"
|
|
951
|
+
...
|
|
952
|
+
|
|
953
|
+
[[pipelines.jobs]]
|
|
954
|
+
name = "C"
|
|
955
|
+
on_failure = "continue"
|
|
956
|
+
on_input_err = "skip"
|
|
957
|
+
...
|
|
958
|
+
|
|
959
|
+
[[pipelines.jobs]]
|
|
960
|
+
name = "D"
|
|
961
|
+
on_failure = "continue"
|
|
962
|
+
on_input_err = "skip"
|
|
963
|
+
...
|
|
964
|
+
|
|
965
|
+
[[pipelines.jobs]]
|
|
966
|
+
name = "E"
|
|
967
|
+
on_failure = "continue"
|
|
968
|
+
on_input_err = "run"
|
|
969
|
+
...
|
|
970
|
+
|
|
971
|
+
Notes:
|
|
972
|
+
|
|
973
|
+
- When using "run_always" you won't be able to take advantage of
|
|
974
|
+
"on_failure" and "on_input_err", as these properties will be
|
|
975
|
+
overwritten in all pipeline jobs.
|
|
976
|
+
|
|
977
|
+
- In particular, all your "on_failure = trigger pipeline : ..." entries
|
|
978
|
+
are automatically discarded.
|
|
979
|
+
|
|
980
|
+
- That's why, if you want a finer control over your pipeline, it is
|
|
981
|
+
recommended not to use "run_always" and, instead, individually set the
|
|
982
|
+
"on_failure" and "on_input_err" property of each job manually.
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
================================================================================
|
|
987
|
+
4. SCRIPT MANAGER
|
|
988
|
+
================================================================================
|
|
989
|
+
|
|
990
|
+
Remember how you call the orchestrator:
|
|
991
|
+
|
|
992
|
+
import pipeforge
|
|
993
|
+
|
|
994
|
+
pipeforge.log_configure(...)
|
|
995
|
+
|
|
996
|
+
p = pipeline.Pipeline(..., "merge_pull_request.toml")
|
|
997
|
+
p.run(...)
|
|
998
|
+
|
|
999
|
+
"run()" takes a parameter called "script_manager", which is what the
|
|
1000
|
+
orchestrator uses to start, query and stop the script associated to each job.
|
|
1001
|
+
|
|
1002
|
+
This "script_manager" object is built like this:
|
|
1003
|
+
|
|
1004
|
+
1. Create a specialized python subclass that inherits from
|
|
1005
|
+
"pipeforge.ScriptManager"
|
|
1006
|
+
|
|
1007
|
+
2. Overwrite the class interface functions with your own implementation.
|
|
1008
|
+
In short you need to implement three functions:
|
|
1009
|
+
|
|
1010
|
+
- run() to start execution a script
|
|
1011
|
+
- query() to return the current execution state
|
|
1012
|
+
- stop() to terminate the script execution
|
|
1013
|
+
|
|
1014
|
+
Check "pipeforge.ScriptManager" documentation to see which parameters each
|
|
1015
|
+
of them take and what are they supposed to do in more detail.
|
|
1016
|
+
|
|
1017
|
+
3. Create an instance of this new specialized subclass.
|
|
1018
|
+
|
|
1019
|
+
4. Call "pipeline.Pipeline(script_manager=<instance_of_your_specialized_subclass>)
|
|
1020
|
+
|
|
1021
|
+
Example:
|
|
1022
|
+
|
|
1023
|
+
class MyScriptManager(pipeforge.ScriptManager):
|
|
1024
|
+
|
|
1025
|
+
def run(...):
|
|
1026
|
+
...
|
|
1027
|
+
def query(...):
|
|
1028
|
+
...
|
|
1029
|
+
def stop(...):
|
|
1030
|
+
...
|
|
1031
|
+
|
|
1032
|
+
x = MyScriptManager(...)
|
|
1033
|
+
|
|
1034
|
+
p.run(script_manager=x, ...)
|
|
1035
|
+
|
|
1036
|
+
In this way, depending on how you want to run your jobs, you can create
|
|
1037
|
+
specialized subclasses for...
|
|
1038
|
+
|
|
1039
|
+
- Executing scripts on remote Jenkins instance
|
|
1040
|
+
- Executing scripts directly in a local or remote computer over SSH
|
|
1041
|
+
- Executing scripts inside a docker container
|
|
1042
|
+
- Etc...
|
|
1043
|
+
|
|
1044
|
+
In short, the responsibilities of each specialized subclass are:
|
|
1045
|
+
|
|
1046
|
+
- Use the "script" and "runner" fields of the job definition to figure out
|
|
1047
|
+
how and where to run the script associated to a job.
|
|
1048
|
+
|
|
1049
|
+
- Somehow pass the script being run a "token" which the orchestrator
|
|
1050
|
+
provides (this will later be needed by each script to read its input
|
|
1051
|
+
parameters and write its output parameters through "pipeforge.JobParams")
|
|
1052
|
+
|
|
1053
|
+
- Be able to return the state of the script at any time ("RUNNING",
|
|
1054
|
+
"SUCCESS", etc...)
|
|
1055
|
+
|
|
1056
|
+
- Be able to stop a running script.
|
|
1057
|
+
|
|
1058
|
+
Again, you will find more details in the documentation of
|
|
1059
|
+
"pipeforge.ScriptManager". Just follow what is described there and you will be
|
|
1060
|
+
ready to go.
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
================================================================================
|
|
1065
|
+
5. LOGGING
|
|
1066
|
+
================================================================================
|
|
1067
|
+
|
|
1068
|
+
When importing the "pipeforge" module, by default no log messages will be
|
|
1069
|
+
printed to stdout. If you want to change that you must use
|
|
1070
|
+
"pipeforge.log_configure()".
|
|
1071
|
+
|
|
1072
|
+
Through this function you can set the verbosity level and even a custom callback
|
|
1073
|
+
function which will be called every time a new log message is ready so that you
|
|
1074
|
+
can print it however you want (to stdout, to a file, etc...)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
================================================================================
|
|
1079
|
+
6. STAND-ALONE APPLICATION
|
|
1080
|
+
================================================================================
|
|
1081
|
+
|
|
1082
|
+
The "pipeforge" module can also be used directly as an application, from the
|
|
1083
|
+
shell, like this:
|
|
1084
|
+
|
|
1085
|
+
$ python -m pipeforge <command> ...
|
|
1086
|
+
|
|
1087
|
+
For a list of valid commands and options, run this:
|
|
1088
|
+
|
|
1089
|
+
$ python -m pipeforge --help
|
|
1090
|
+
|
|
1091
|
+
Note that, depending on the "pip" version you used when installing pipeforge, it
|
|
1092
|
+
might be directly available as a script:
|
|
1093
|
+
|
|
1094
|
+
$ pipeforge --help
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
================================================================================
|
|
1099
|
+
7. JENKINS INTEGRATION
|
|
1100
|
+
================================================================================
|
|
1101
|
+
|
|
1102
|
+
Jenkins can be used to run "pipeforge" jobs. All you need to do is this:
|
|
1103
|
+
|
|
1104
|
+
1. Deploy a Jenkins instance.
|
|
1105
|
+
|
|
1106
|
+
2. Create a new Jenkins job following the documentation in
|
|
1107
|
+
pipeforge._internal.script.JenkinsScriptManager. Let's call it, for
|
|
1108
|
+
example, "Job".
|
|
1109
|
+
|
|
1110
|
+
3. In your PC, run this:
|
|
1111
|
+
|
|
1112
|
+
$ export PIPEFORGESCRIPT_JENKINS_ENDPOINT=<Jenkins instance URL>:::<username>:::<password>:::<job_name>
|
|
1113
|
+
$ python -m pipeforge run <database_url> <path_to_toml_file> JenkinsScriptManager
|
|
1114
|
+
|
|
1115
|
+
Example:
|
|
1116
|
+
|
|
1117
|
+
$ export PIPEFORGESCRIPT_JENKINS_ENDPOINT='https://jenkins.example.com:::john:::cobra123:::Job'
|
|
1118
|
+
$ python -m pipeforge run mongodb://mongodb.example.com:27017/pipelines pull_request.toml JenkinsScriptManager
|
|
1119
|
+
|
|
1120
|
+
That's all: you are now running all the steps of your pipeline in remote slaves
|
|
1121
|
+
managed by Jenkins.
|
|
1122
|
+
|
|
1123
|
+
There is one extra thing we can do to further integrate "pipeforge" with
|
|
1124
|
+
Jenkins, and that is to *also* run the "pipeforge" engine itself in Jenkins
|
|
1125
|
+
(instead of in your PC). For this follow these instructions:
|
|
1126
|
+
|
|
1127
|
+
1. Create a new Jenkins job in your instance called, "Pipeline" (now you
|
|
1128
|
+
have two jobs: one called "Job", to run pipeline jobs/steps and this new
|
|
1129
|
+
one called "Pipeline" to run the pipeline engine that schedules jobs)
|
|
1130
|
+
|
|
1131
|
+
2. In the "Configure" tab, select the "This project is parameterized"
|
|
1132
|
+
checkbox and add this entry:
|
|
1133
|
+
|
|
1134
|
+
- Type: File Parameter
|
|
1135
|
+
Name: pipeline.toml
|
|
1136
|
+
Description:
|
|
1137
|
+
*.toml file containing the "pipeforge" pileline definition (as
|
|
1138
|
+
decribed in [1]) you want to run. See [2] for examples.
|
|
1139
|
+
|
|
1140
|
+
[1] help(pipeforge.__init__)
|
|
1141
|
+
[2] pipeforge/examples
|
|
1142
|
+
|
|
1143
|
+
3. Select the "Execute concurrent builds if necessary" checkbox.
|
|
1144
|
+
|
|
1145
|
+
4. In "Build Steps" add this entry:
|
|
1146
|
+
|
|
1147
|
+
- Type: Execute shell
|
|
1148
|
+
- Command:
|
|
1149
|
+
|
|
1150
|
+
export PIPEFORGESCRIPT_JENKINS_ENDPOINT='localhost:::<username>:::<password>:::Job'
|
|
1151
|
+
python -m pipeforge run <database_url> pipeline.toml JenkinsScriptManager
|
|
1152
|
+
|
|
1153
|
+
Example:
|
|
1154
|
+
|
|
1155
|
+
export PIPEFORGESCRIPT_JENKINS_ENDPOINT='localhost:::john:::cobra123:::Job'
|
|
1156
|
+
python -m pipeforge run mongodb://mongodb.example.com:27017/pipelines pipeline.toml JenkinsScriptManager
|
|
1157
|
+
|
|
1158
|
+
That's all!
|
|
1159
|
+
Now, whenever you want to run a pipeline, simply go to your Jenkins instance and
|
|
1160
|
+
trigger the "Pipeline" job. It will ask you for a TOML file. Upload it and go!
|
|
1161
|
+
|
|
1162
|
+
Note: when you do this (ie. running the "pipeforge" engine as a Jenkins job),
|
|
1163
|
+
all jobs executed by "pipeforge" will receive an additional environment variable
|
|
1164
|
+
("JENKINS_PARENT") containing the URL of the Jenkins job running the "pipeforge"
|
|
1165
|
+
engine. This is for convenience, in case you want to include this link in your
|
|
1166
|
+
job's output.
|
|
1167
|
+
|
|
1168
|
+
Finally, if you want to connect all of this to a source code management (SCM)
|
|
1169
|
+
system (ie. GitHub, BitBucket, ...) so that a pipeline is triggered on specific
|
|
1170
|
+
actions (when a pull request is opened, when the developer presses a button,
|
|
1171
|
+
etc...) you just need to do this:
|
|
1172
|
+
|
|
1173
|
+
1. Install a hook in your SCM that is triggered on action X.
|
|
1174
|
+
|
|
1175
|
+
2. The hook will forge a TOML file describing what we want to do.
|
|
1176
|
+
In this TOML file, input parameters to the first job will probably be
|
|
1177
|
+
information regarding the repository, such as the branch name, the target
|
|
1178
|
+
branch, etc...
|
|
1179
|
+
|
|
1180
|
+
3. The hook will call our Jenkins REST API to send the TOML file and trigger
|
|
1181
|
+
the "Pipeline" job.
|
|
1182
|
+
|
|
1183
|
+
4. (Optionally) One of the latest jobs in the pipeline is responsible for
|
|
1184
|
+
posting a message/comment back into the SCM including the pipeline
|
|
1185
|
+
result.
|
|
1186
|
+
"""
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
# These are the internal symbols exported to users of this module:
|
|
1190
|
+
|
|
1191
|
+
# To users that want to run pipelines:
|
|
1192
|
+
#
|
|
1193
|
+
from ._internal.utils import log_configure # To configure module's logging
|
|
1194
|
+
from ._internal.script import ScriptManager # To customize the script runner
|
|
1195
|
+
from ._internal.pipeline import Pipeline # To run pipelines
|
|
1196
|
+
|
|
1197
|
+
# To users that want to create scripts that run in a pipeline
|
|
1198
|
+
#
|
|
1199
|
+
from ._internal.params import JobParams # To access job parameters
|
|
1200
|
+
|
|
1201
|
+
|