Le monorepo est une stratégie de versionning qui consiste à n'avoir qu'un seul repository pour les bases de code de plusieurs projets. Ces projets ont généralement leur propre cycle de vie, mais aussi leurs propres technologies. La construction du livrable de chaque projet peut alors être soumise à des contraintes différentes. Il est dès lors compliqué de mettre en place une chaîne de CI (continuous integration) qui puisse fonctionner pour l'ensemble.
Sortis en janvier 2020, les parent-child pipelines sont la réponse apportée par Gitlab aux problématiques de CI liées à l'exploitation des monorepos.
Voyons cela de plus près !
Un cas concret
Imaginons une banque dont les services sont accessibles depuis un client web en Angular, une application Android, une application iOS, le tout reposant sur les API d’un backend. Nous aurions alors classiquement 4 projets, mais que nous aimerions garder au sein d’un seul et unique repository.
Nous avons alors un repository my-awesome-bank
dans lequel seraient versionnés les projets suivants :
- web-client (l’application web Angular)
- android-app (l’application Android en Kotlin)
- ios-app (l’application iOS en Swift)
- backend (un backend monolithique en Java, parce que je n’ai pas eu le courage de mettre d'autres trucs :p)
Le projet est accessible depuis mon gitlab. Finalement, je vous ai un peu menti en disant que le cas était concret. Nous ne ferons joujou qu'avec GitLab CI sans nous intéresser aux projets qui resteront vides. ¯\_(ツ)_/¯
Créer le pipeline parent
Dans notre exemple, le pipeline parent pourrait se résumer à un seul et unique stage : celui responsable du déclenchement des pipelines de chacun des projets. Appelons-le trigger-child-pipelines
...
# .gitlab-ci.yml
stages:
- trigger-child-pipelines
... et associons-y un job qui déclenche le pipeline du web-client
# .gitlab-ci.yml
web-client:
stage: trigger-child-pipeline
trigger:
include: web-client/.gitlab-ci.yml
strategy: depend
only:
changes:
- web-client/**/*
Ce job déclenchera le pipeline décrit dans ./web-client/.gitlab-ci.yml
.
La stratégie depend
permet de dire au job que son statut (success, failed, cancelled, pending) dépend de celui du pipeline qu'il a déclenché. Cela permet d'attendre la fin du pipeline enfant afin de passer à la suite du pipeline parent. Le job attendra alors la fin de l’exécution du pipeline qu’il a déclenché.
On notera la présence du only:changes
sur le répertoire web-client
qui permet de dire au job de ne se déclencher qu'en cas de modification apportée au projet web-client
. Cela évite de déclencher inutilement un pipeline enfant lors de la réception d'un commit qui ne le concerne pas.
La logique sera la même pour les jobs qui déclencheront les pipelines des projets android-app
, ios-app
et backend
.
A chacun son pipeline !
Il reste à définir les pipelines de chacun des projets. Ceux-ci se configurent de la même manière qu'un pipeline classique.
Il y a cependant une différence fondamentale. Le pipeline enfant est exécuté à la racine du repository.
Par exemple, si le pipeline de backend
a été déclenché, son exécution ne se fera pas automatiquement au sein du répertoire backend
. Ainsi, les commandes telles que mvn test
ne fonctionneront pas à cause de l'absence de pom.xml
(à la racine du projet).
Un simple cd backend
dans chacun des jobs fera l'affaire.
# backend/.gitlab-ci.yml
.backend-setup:
before_script:
- cd backend
test:
extends: .backend-setup
stage: test
script:
- mvn test
Les valeurs de artifact:paths
et cache:paths
ne sont, elles non plus, pas évaluées depuis le répertoire des projets. Il sera nécessaire de donner les chemins depuis la racine du repository.
# web-client/.gitlab-ci.yml
cache:
paths:
- web-client/node_modules
...
build:
extends: .web-client-setup
stage: build
script:
- yarn build
artifacts:
paths:
- web-client/dist/*
Des artifacts difficilement partageables entre pipelines.
Il existe des cas où l'on souhaiterait fournir aux pipelines enfants les artifacts du parent et vice versa.
On pourrait, par exemple, fusionner les rapports d'exécution de tests Cucumber afin de les exposer au sein d'une living doc.
L'issue #202093 a été ouverte à ce sujet et des solutions de contournement ont été proposées.
Des pipelines enfants qui ne devraient pas s'exécuter
Lors de l'ouverture d'une branche, l'ensemble des pipelines enfants sont exécutés, malgré la présence des contraintes on:changes
. Cela peut vite être un problème si le monorepo contient beaucoup de projets. Le nombre de jobs peut être conséquent, et la facture salée si vous hébergez vous-mêmes vos runners.
L'issue #11427 traitant de ce sujet est ouverte depuis maintenant 1 an et ne semble toujours pas résolue.
NOTE : Ce souci est aussi présent dès lors que le pipeline est exécuté manuellement.
Au final
Les parent-child pipelines proposent une vraie solution aux équipes qui souhaitent partir sur des monorepos. Leur usage est identique à celui des pipelines proposés par Gitlab-ci à l'exception des quelques ajustements que nous avons vus.
Il faudra cependant être vigilant aux limitations encore existantes, notamment celles induites par l'issue #11427, et décider de les utiliser ou non en connaissance de cause.
Ces limitations ne seront pas problématiques sur des monorepos avec peu de projets (ex: un back et un front).