Creating a CI/CD pipeline employing an existing Makefile
Abstract. In this post I’m going to highlight a certain problem occuring when one needs to create a CI/CD pipeline based on an already existing Makefile
, and describe my solution to this problem.
GNU Make for reproducible research papers #
We support the principle of reproducibility in science. All numerical results presented in our last paper are fully reproducible and hence can be verified by the reader. In order to build the paper a series of numerical experiments is executed, their results are analysed, and finally the corresponding tables and plots are generated.
One obviously needs a build system to make it right. We opted for GNU Make. The idea to use GNU Make for reseach papers is neither new nor surprising. Make is a general purpose build system and is available in almost every Linux system.
Here is an example of a Makefile
for a paper in numerical mathematics, which one could easily extend.
MANUSCRIPT_TEX = manuscript.tex
MANUSCRIPT_PDF = $(MANUSCRIPT_TEX:.tex=.pdf)
.PHONY: manuscript plots.all tables.all numericals.all
manuscript: $(MANUSCRIPT_PDF)
$(MANUSCRIPT_PDF): \
$(MANUSCRIPT_TEX) \
plots.all \
tables.all
latexmk -pdf -silent $(MANUSCRIPT_TEX)
artifacts/numericals/solution_1.npy: \
numericals/solver_1.py
python3 numericals/solver_1.py --outfile=$@
artifacts/plots/plot_1.svg: \
plots/plot_1.py \
artifacts/numericals/solution_1.npy
mkdir -p artifacts/plots
python3 plots/plot_1.py --outfile=$@
artifacts/tables/table_1.tex: \
tables/table_1.py \
artifacts/numericals/solution_1.npy
python3 tables/table_1.py --outfile=$@
numericals.all: artifacts/numericals/solution_1.npy
plots.all: artifacts/plots/plot_1.svg
tables.all: artifacts/tables/table_1.tex
Notice that all the generated artifacts, i.e. computed numbers, generated plots and tables are located in artifacts/
, separately from the generation scripts and author’s content.
Creating a GitLab CI pipeline #
Well, the ability to build the manuscript locally on a developer’s machine is nice, but automated builds running on a dedicated computing node for every new version are even better. How do we write a CI/CD pipeline based on the Makefile
we have?
The key feature of make is dependency resolution and smart re-evaluation: the computing artifacts whose dependencies didn’t change won’t be recomputed. However, the default strategy for a GitLab CI pipeline would be to start all the computations from scratch every time. When writing a paper based on heavy computations, this is getting extremely expensive so the overhead involved outweighs the benefits of automation: a line of text added to the manuscipt will trigger re-evaluation of all the artifacts.
In order to resolve this issue we need to tell GitLab to cache our artifacts. Moreover, we need to change the GIT_STRATEGY
from it’s default clone to fetch. Otherwise the scripts which are generating the artifacts will get more recent modification times than the previously computed artifacts, which will make the latter automatically obsolete for make.
Here is a minimal example.
variables:
GIT_STRATEGY: fetch
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- articafts
manuscript:
image: myproject/image
script: make manuscript
artifacts:
paths:
- artifacts
What is wrong with it? We declared a high level task manuscript
and delegated make to do all the small intermediate steps. Is it bad? Not allways.
Going this way we need to create a big and dirty docker image which needs to contain all the relevant software. For small projects with simple computing environments this might be the best choice. However, if the computing environment is rather sophisticated and challenging to reproduce, it might be beneficial to split it into a few: one for each group of tasks, i.e. computing, process, plotting, etc.
In this case our pipeline will also become more informative. Instead of one big task manuscript
we will be able to see all the intermediate steps straight in GitLab’s interface.
So here is the improved version.
stages:
- compute
- process
- tex
variables:
GIT_STRATEGY: fetch
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- articafts
numericals:
stage: compute
image: myproject/image_numericals
script: make numericals.all
artifacts:
paths:
- articafts/numericals
plots:
stage: process
image: myproject/image_plots
script: make plots.all
artifacts:
paths:
- articafts/plots
tables:
stage: process
image: myproject/image_tables
script: make tables.all
artifacts:
paths:
- articafts/tables
tex:
stage: tex
image: myproject/image_tex
script: make preprint
artifacts:
paths:
- manuscript.pdf
Caveats #
After using a similar pipeline for some time we ran into the following issue. When the re-evaluation of some artifacts actually takes place (i.e. when their dependencies have been modified), the next task occasionally fails because the artifacts from the previous stage are still being uploaded from the corresponding GitLab runner. At the moment I have no easy fix in mind, so I simply restart the failed task if this happened.