Construire la CI d'un monorepo: les parent-child pipelines de Gitlab-ci

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).