L’année dernière chez Proton, nous sommes passés d’une architecture polyrepo à une architecture monorepo pour faciliter la gestion des packages faisant partie de notre pile d’applications web frontales. Nous rencontrions des problèmes depuis un certain temps et après avoir envisagé nos options, nous avons décidé qu’un monorepo serait la solution la plus adaptée. Cet article explique les problèmes que nous avons rencontrés avec notre configuration polyrepo, explore les avantages d’une configuration monorepo et décrit notre parcours du polyrepo au monorepo.
Avant d’aller plus loin, lorsque je dis « polyrepo » et « monorepo », voici ce que je veux dire :
- Polyrepo : Un système de modules de code source qui ont des dépendances entre eux mais sont des instances de dépôt de contrôle de version séparées.
- Monorepo : Un système de modules de code source qui ont des dépendances entre eux mais qui se trouvent tous sous une instance de dépôt de contrôle de version unique.
Je vais dire « dépôts Git » ou simplement « dépôts » au lieu de « dépôts de contrôle de version » à partir de maintenant. Et, pour être clair, Git n’est pas une condition préalable à l’architecture monorepo.
Le début
Proton a commencé avec un client de messagerie, Proton Mail(nouvelle fenêtre), comme seule application mais s’est depuis développé en tant que fournisseur de confidentialité offrant une large gamme de produits, y compris des applications web pour Proton Mail, Proton Calendar(nouvelle fenêtre), Proton Drive(nouvelle fenêtre), et le Compte Proton qui les relie tous. L’ajout de nouvelles applications à notre pile a fait croître proportionnellement le nombre de dépôts Git que nous maintenons, avec un dépôt par application. Cependant, nous avons créé des dépôts au-delà de ceux requis pour nos applications. Comme vous pouvez l’imaginer, nos applications doivent partager la même fonctionnalité, apparence et sensation, même si ce sont des produits différents. Il s’ensuit que nous utilisions des dépôts pour le code partagé entre les produits.
Par exemple, nous avions un dépôt séparé pour les composants React partagés. C’était le résultat d’une évolution naturelle de nos systèmes existants. Cependant, partager du code entre les bases de code est devenu de plus en plus complexe au fur et à mesure que nous ajoutions plus d’applications et de produits, rendant difficile la gestion des packages sous cette structure multi-dépôts. Il y a plusieurs raisons pour lesquelles ce système n’était pas évolutif.
Notre principal problème avec notre polyrepo
Pendant et après notre transition vers un monorepo, nous avons commencé à voir comment nous pourrions bénéficier de son architecture. Cependant, un problème en particulier — la réplication inutile et superflue des tâches administratives — nous a poussés à envisager cette option de monorepo en premier lieu. Lorsqu’une mise en œuvre de fonctionnalité nécessitait des modifications dans plusieurs projets pour être complétée (par exemple, l’ajout d’un composant React pour une nouvelle fonctionnalité dans l’application Proton Mail), les tâches administratives dans Git étaient très peu pratiques à exécuter. Pour préparer une seule fonctionnalité, nous devions miroiter les opérations Git — création de branches, commits, ouverture de demandes de fusion, révisions, rebasage, etc. — à travers de nombreux dépôts.
Nous avons alors découvert l’idée de « changements atomiques », qui nous a interpellés, même si cela représentait un changement dans notre philosophie. L’idée principale derrière les changements atomiques est que, au lieu d’avoir des changements limités à une préoccupation technique de votre ou vos projets, vous limitez les changements à leur groupe sémantique en tant que blocs de modification de la fonctionnalité de votre produit. Il n’y a aucune raison de diviser les modifications qui affectent intrinsèquement nos composants d’interface utilisateur partagés et (par exemple) l’application Proton Mail si elles abordent toutes la même préoccupation. De telles modifications sémantiquement connectées devraient être :
- Regroupés sous le même changement, diff et commit
- Révisables simultanément (et non dans deux demandes de fusion distinctes)
- Réversibles en tant qu’unité unique.
Un monorepo nous permet d’atteindre cet objectif car il prend naturellement en charge les modifications atomiques sous forme de commits Git.
Dans le polyrepo, tester le code avant de l’accepter et de le fusionner à la branche principale s’est également révélé difficile, notamment du point de vue de l’automatisation CI/CD. Les builds devaient inclure des versions des dépendances qui n’étaient pas sur la branche principale de leur dépôt respectif. Néanmoins, avec quelques astuces et manipulations CI/CD, nous avons pu faire le travail et il était possible de faire passer les fonctionnalités à travers le cycle de développement avec succès.
Nous n’utilisions pas non plus semver et l’hébergement de registre pour versionner nos packages (et nous ne le faisons toujours pas), ce qui aurait été une manière de résoudre certains de ces problèmes. Cependant, semver aurait été loin d’être une solution miracle pour nos besoins, et cela vient avec son propre fardeau, comme la complexité de gérer les packages hébergés, de les publier et de les versionner lors de la consommation.
L’architecture de dépôt polyrepo présente de nombreux autres petits inconvénients mineurs étant donné nos besoins. Je parlerai davantage des problèmes auxquels nous avons été confrontés en discutant des avantages de notre monorepo. Pour plus de contexte, notre architecture polyrepo présentait des problèmes au-delà de l’expérience développeur, y compris des problèmes techniques inhérents. Un exemple concret était que nous ne pouvions pas effectuer de retours à des versions précédentes sur une base inter-dépôts. Si une nouvelle fonctionnalité qui affectait plusieurs dépôts était fusionnée puis s’avérait avoir un problème, il était difficile d’effectuer des retours automatiques car aucune opération unique ne pouvait réaliser un retour sur des historiques Git séparés simultanément.
Ces problèmes s’accumulaient progressivement, et il est devenu évident que nous avions besoin d’une solution. Après mûre réflexion, cette solution s’est avérée être la migration vers une architecture monorepo.
Évaluation de nos options
Une fois la décision de migrer prise, nous devions élaborer un plan.
À ce moment-là, notre équipe Front-end comptait environ 15 développeurs travaillant sur notre pile d’applications web. De plus, de nombreuses personnes d’autres équipes, telles que Crypto ou Back-end, contribuaient également fréquemment à nos dépôts. Avoir de nombreuses personnes travaillant activement sur ces dépôts signifiait que la migration physique devait se faire rapidement et que la mise en œuvre devait être robuste une fois de l’autre côté. Sinon, nous risquions de bloquer le travail de nos collègues pendant une période prolongée.
Pour garantir une mise en œuvre robuste, nous avons passé pas mal de temps à rechercher différents outils et à expérimenter avec des preuves de concept. Nous vérifiions comment une option se comportait ou si nous pouvions l’amener à se comporter comme nous le souhaitions. Nous avons exploré différents gestionnaires de packages (en particulier, npm, yarn, pnpm), la version sémantique avec un registre hébergé, différents types d’installations de dépendances, la gestion des fichiers de verrouillage et plus encore.
Au final, nous avons décidé de rester très basiques. Nous avons choisi Yarn (Berry) et Yarn Workspaces, un seul fichier de verrouillage à la racine du monorepo, pas de version sémantique et pas d’installations zéro. Nous sommes arrivés à ces décisions car nous voulions le moins de surcharge possible, des outils matures et que notre équipe soit déjà familière avec ces outils.
Tous les avantages potentiels d’un monorepo
Un moment clé lors de nos recherches sur les monorepos a été de réaliser que, si cette architecture allait certainement résoudre les problèmes auxquels nous étions confrontés, ces systèmes offraient bien plus encore. Les monorepos ont offert de nombreux avantages auxquels nous n’avions pas nécessairement pensé, la plupart étant liés à la collaboration entre développeurs.
Nous avons soutenu que l’architecture monorepo inciterait les personnes à collaborer sur des projets dont ils ne sont pas nécessairement propriétaires en rendant tout le code visible, donnant ainsi aux développeurs le pouvoir d’apporter des corrections simples. Au lieu d’être contraint de chercher de l’aide parce que vous faites face à une boîte noire, vous pourriez être en mesure d’apporter le changement nécessaire vous-même puisque tout le code serait facilement accessible.
Les monorepos rendraient également possible une refonte à grande échelle, car nous pourrions modifier de grandes parties de différents projets avec des commits unifiés. Puisque tout le code source interdépendant serait désormais hébergé dans le même dépôt Git, la disponibilité et l’emplacement dans le système de fichiers de n’importe quel morceau de code serait prévisible. Cela rendrait possible de fournir des utilitaires pour effectuer toute action nécessaire pour travailler avec le monorepo localement ou dans l’intégration continue (CI), par exemple la configuration de l’environnement, les serveurs de développement, les constructions, les vérifications, la création automatique de liens symboliques, la gestion des fichiers de verrouillage et plus encore. Nous étions franchement emballés, c’est le moins qu’on puisse dire.
Après avoir conçu un plan de monorepo qui nous satisfaisait, nous avons préparé une présentation pour le reste de l’équipe, partagé nos découvertes et notre preuve de concept, recueilli des retours et l’avons peaufiné. Nous voulions nous assurer de ne pas créer une configuration avec laquelle quelqu’un serait incapable ou mécontent de travailler. Cela a été bien accueilli, et nous avons décidé d’aller de l’avant.
La migration physique
Alors que nous nous préparions à migrer, notre principal objectif était d’éviter de perturber le travail en cours. Nous avons écrit un script qui prendrait tous les dépôts existants de notre configuration polyrepo, fusionnerait leurs historiques Git en un seul historique et comblerait les lacunes nécessaires pour réaliser le monorepo complet. Ce script pouvait générer notre monorepo entier à l’exécution d’une commande, ce qui signifiait que nous pouvions créer le monorepo à tout moment, quel que soit l’état actuel du polyrepo. C’était bien mieux que de devoir arrêter le développement pendant que nous construisions manuellement le monorepo à partir du polyrepo.
La mise en œuvre complète a également vu une réécriture complète de notre CI pour tous les contrôles et déploiements des applications et des paquets, ce qui représentait une grande partie de la transition. Explorer comment ajuster et écrire le CI pour un monorepo fera l’objet d’un article à part entière à une date ultérieure.
Une fois que tout était prêt et en place, nous avons fixé une date pour la migration : un samedi. Nous avons choisi un jour de week-end afin que les gens puissent rentrer chez eux, laisser leur travail derrière eux le vendredi, puis revenir le lundi suivant et retrouver ce sur quoi ils travaillaient à présent dans le monorepo.
À ce stade, nous considérions le polyrepo obsolète car nous ne voulions pas maintenir plusieurs historiques Git conflictuels en continu. Pour garantir qu’aucun travail ne soit perdu, nous avons compilé une liste de toutes les branches actives que les gens souhaitaient sauvegarder et transférer (nous avons ajouté une prise en charge pour cela dans notre script de création de monorepo).
De l’autre côté
Aussi irréaliste que le plan puisse paraître sur le papier, cela a très bien fonctionné pour nous ! Durant la première semaine après la migration, quelques pipelines ont échoué, et quelques morceaux de code incomplets sont restés dans la configuration polyrepo et ont dû être transférés manuellement après la transition. Mis à part ces quelques petits incidents et d’autres mineurs, tout s’est bien passé. Personne n’a été sérieusement empêché de continuer son travail, et maintenant que la migration est terminée, personne ne s’est retourné.
Nous avons découvert que le monorepo offre encore plus d’avantages que prévu depuis la migration. Il est désormais beaucoup plus facile d’intégrer de nouvelles personnes à notre base de code, grâce à la configuration en un clic pour un environnement de développement local. Une petite communauté interne s’est développée autour de cela, et ce ne sont pas seulement les membres de l’équipe Front-end de Proton. Elle comprend toute personne intéressée par l’architecture monorepo et toute personne qui travaille avec la nôtre. Dans cette communauté, nous parlons de :
- Des monorepos en général (et de notre monorepo WebClients(nouvelle fenêtre) en particulier)
- Gérer les problèmes liés aux monorepos lorsque les gens ont besoin d’aide
- Proposer des améliorations à notre flux de travail monorepo.
Plus important encore, nous parlons désormais tous le même langage en ce qui concerne le flux de travail et l’administration Git. Puisqu’il s’agit désormais d’un seul dépôt Git, nous avons également normalisé les directives pour Git à travers différentes équipes de développement front-end et configuré universellement les règles de notre outil d’hébergement Git qui couvre l’ensemble du monorepo (par exemple, les règles de fusion).
Conclusion
Avec le recul, cette mise en œuvre du monorepo a dépassé nos attentes. C’est une bonne solution compte tenu de nos besoins, et nous sommes ravis de l’avoir adoptée ! L’amélioration de l’expérience des développeurs a conduit à une augmentation notable de la productivité. Ce n’est toujours pas une solution miracle, et il existe de nombreux défis qui l’accompagnent, mais pour nous, ces défis sont largement compensés par les avantages qu’elle a apportés. Nous espérons que cette architecture de package de base tiendra le coup et nous permettra de monter en échelle et d’ajouter tout autre package requis facilement pour l’avenir prévisible.
Le dépôt Git discuté dans cet article est open source et peut être trouvé à https://github.com/ProtonMail/WebClients(nouvelle fenêtre).