El año pasado en Proton, migro de una arquitectura polyrepo a una arquitectura monorepo para facilitar la administración de los paquetes que forman parte de nuestra pila de aplicaciones web front-end. Hacía tiempo que teníamos problemas y, después de considerar nuestras opciones, decidimos que un monorepo sería la solución más adecuada. Este artículo explica los problemas que enfrentamos con nuestra configuración de polyrepo, explora los beneficios de una configuración de monorepo y describe nuestro viaje de polyrepo a monorepo.

Antes de continuar, cuando decimos “polyrepo” y “monorepo”, esto es lo que quiero decir:

  • Polyrepo: un sistema de módulos de código fuente que tienen dependencias entre sí pero son instancias de repositorio de control de versiones separadas.
  • Monorepo: un sistema de módulos de código fuente que tienen dependencias entre sí, pero todos viven bajo una única instancia de repositorio de control de versiones.

Voy a decir «repositorios de Git» o simplemente «repositorios» en lugar de «repositorios de control de versiones» de aquí en adelante. Y, para ser claros, Git no es un requisito previo de la arquitectura monorepo.

El principio

Proton comenzó con un cliente de correo electrónico, Proton Mail, como su única aplicación, pero desde entonces ha evolucionado hasta convertirse en un proveedor de privacidad que ofrece una amplia gama de productos, incluidas aplicaciones web para Proton Mail, Proton Calendar, Proton Drive y la cuenta de Proton que los vincula a todos. Agregar nuevas aplicaciones a nuestra pila ha provocado que la cantidad de repositorios Git que mantenemos crezca proporcionalmente, con un repositorio por aplicación. Sin embargo, creamos repositorios más allá de los necesarios para nuestras aplicaciones. Como puedes imaginar, nuestras aplicaciones deben compartir la misma funcionalidad, apariencia y funcionamiento, incluso si son productos diferentes. De ello se deduce que usamos repositorios para el código que se compartió entre productos.

Como ejemplo, solíamos tener un repositorio separado para los componentes de React compartidos. Este fue el resultado de una evolución natural de nuestros sistemas existentes. Sin embargo, compartir código entre bases de código se volvió cada vez más complejo a medida que añadíamos más aplicaciones y productos, lo que dificultaba la administración de paquetes bajo esta estructura de múltiples repositorios. Hay varias razones por las que este sistema no escaló bien.

Nuestro principal problema con nuestro polyrepo

Durante y después de nuestra transición a un monorepo, comenzamos a ver cómo podíamos beneficiarnos de su arquitectura. Sin embargo, un problema en particular, la replicación innecesaria y derrochadora de tareas administrativas, nos llevó a considerar esta opción monorepo en primer lugar. Cada vez que la implementación de una función requería cambios en varios proyectos para completarse (por ejemplo, agregar un componente React para una nueva función dentro de la aplicación Proton Mail), las tareas administrativas en Git eran muy poco prácticas de ejecutar. Para preparar una función única, tuvimos que reflejar las operaciones de Git (ramificación, compromiso, apertura de solicitudes de fusión, revisión, reorganización, etc.) en muchos repositorios.

Luego nos encontramos con la idea de los «cambios atómicos», que resonó con nosotros, incluso si representaba un cambio en nuestra filosofía. No hay razón para dividir los cambios que afectan intrínsecamente a nuestros componentes de interfaz de usuario compartidos y (por ejemplo) a la aplicación Proton Mail si todos abordan el mismo problema. Dichos cambios semánticamente conectados deberían ser:

  • Agrupados bajo el mismo cambio, diferencia y confirmación.
  • Revisable simultáneamente (no en dos solicitudes de combinación separadas).
  • Reversible como una sola unidad.

Un monorepo nos permite lograr esto, ya que naturalmente admite cambios atómicos en forma de compromisos de Git.

En el polyrepo, probar el código antes de aceptarlo y fusionarlo con la rama principal también resultó ser un desafío, especialmente desde el punto de vista de la automatización CI/CD. Las compilaciones tenían que incluir versiones de dependencias que no estuvieran en la rama principal de su repositorio respectivo. No obstante, con algunos trucos y trucos de CI/CD, pudimos hacer el trabajo y fue posible enviar características a través del ciclo de vida de desarrollo con éxito.

Tampoco estábamos usando semver y alojamiento de registros para crear versiones de nuestros paquetes (y todavía no lo hacemos), lo que habría sido una forma de abordar algunos de estos problemas. Sin embargo, semver habría estado lejos de ser una bala de plata para nuestras necesidades, y viene con su propio equipaje, como la complejidad en la administración de paquetes alojados, su publicación y su versión según el consumo.

La arquitectura del repositorio de Polyrepo tiene muchas otras peculiaridades menores e inconvenientes dadas nuestras necesidades. Hablaré más de los problemas que enfrentamos mientras discutimos las ventajas de nuestro monorepo. Para obtener más contexto, nuestra arquitectura polyrepo presentó problemas además de la experiencia del desarrollador, incluidos problemas técnicos inherentes. Un ejemplo tangible fue que no podíamos realizar reversiones a versiones anteriores en un repositorio cruzado. Si se fusionaba una nueva característica que afectaba a varios repositorios y luego resultaba que tenía un problema, era un desafío realizar reversiones automáticamente, ya que ninguna operación única podía realizar una reversión en distintos historiales de Git simultáneamente.

Estos problemas se acumulaban lentamente y se hizo evidente que necesitábamos una solución. Después de algunas consideraciones, esa solución resultó ser migrar a una arquitectura monorepo.

Sopesando nuestras opciones

Con la decisión de migrar bloqueada, tuvimos que idear un plan.

En ese momento, teníamos alrededor de 15 desarrolladores en el equipo de front-end trabajando en nuestra pila de aplicaciones web. Además, muchas personas de otros equipos, como Crypto o Back-end, también contribuirían con frecuencia a nuestros repositorios. Tener mucha gente trabajando activamente en estos repositorios significaba que la migración física tendría que ocurrir rápido, y la implementación tendría que ser sólida una vez que estuviéramos del otro lado. De lo contrario, corremos el riesgo de bloquear el trabajo de nuestros colegas durante un período prolongado de tiempo.

Para garantizar una implementación sólida, dedicamos bastante tiempo a investigar diferentes herramientas y experimentar con pruebas de concepto. Veríamos cómo se sentía una opción o si podíamos hacer que se comportara como queríamos. Exploramos diferentes administradores de paquetes (específicamente, npm, yarn, pnpm), versiones semánticas con un registro alojado, diferentes tipos de instalaciones de dependencia, administración de archivos de bloqueo y más.

Al final, decidimos ir muy básico. Elegimos Yarn (Berry) y Yarn Workspaces, un solo archivo de bloqueo en la raíz del monorepo, sin versiones semánticas y sin instalaciones cero. Llegamos a estas decisiones porque queríamos la menor sobrecarga posible, herramientas maduras y que nuestro equipo ya estuviera familiarizado con dichas herramientas.

Todos los beneficios potenciales de un monorepo

Un momento clave durante nuestra investigación sobre monorepos fue darnos cuenta de que, si bien esta arquitectura ciertamente resolvería los problemas a los que nos enfrentábamos, estos sistemas ofrecían mucho más. Monorepos proporcionó muchos beneficios que no necesariamente habíamos considerado, la mayoría en torno a la colaboración de los desarrolladores.

Argumentamos que la arquitectura monorepo incentivaría a las personas a colaborar en proyectos que no necesariamente son de su propiedad al hacer visible todo el código, lo que permitiría a los desarrolladores implementar soluciones simples. En lugar de verse obligado a buscar ayuda porque está mirando una caja negra, es posible que pueda implementar un cambio necesario usted mismo, ya que se podrá acceder fácilmente a todo el código.

Es probable que Monorepos también haga posible la refactorización a gran escala, ya que podríamos cambiar grandes partes de diferentes proyectos con compromisos unificados. Dado que todo el código fuente interdependiente ahora estaría alojado en el mismo repositorio de Git, la disponibilidad y la ubicación del sistema de archivos de cualquier pieza de código serían predecibles. Eso permitiría proporcionar utilidades para realizar cualquier acción necesaria para trabajar con el monorepo localmente o en integración continua (CI), por ejemplo, configuración del entorno, servidores de desarrollo, compilaciones, comprobaciones, enlace simbólico automatizado, administración de archivos de bloqueo y más. . Estábamos bastante emocionados al respecto, por decir lo menos.

Después de llegar a un modelo de monorepo con el que estábamos contentos, preparamos una presentación para el resto del equipo, presentamos nuestros hallazgos y la prueba de concepto, recopilamos comentarios e iteramos sobre ellos. Queríamos asegurarnos de que no crearíamos una configuración con la que alguien no pudiera o no estuviera feliz de trabajar. Fue bien recibido y decidimos seguir adelante.

La migración física

Mientras nos preparábamos para migrar, nuestro principal objetivo era evitar interrumpir el trabajo en curso. Escribimos un script que tomaría todos los repositorios existentes de nuestra configuración de polyrepo, fusionaría sus historias de Git en una sola historia y llenaría los espacios necesarios para realizar el monorepo completo. Este script podía generar nuestro monorepo completo con la ejecución de un comando, lo que significaba que podíamos crear el monorepo en cualquier instante, sin importar en qué estado se encontraba actualmente el polyrepo. Esto era mucho mejor que tener que cerrar el desarrollo mientras construíamos manualmente el monorepo del polirepo.

La implementación completa también vio una reescritura completa de nuestro CI para todas las comprobaciones e implementaciones de aplicaciones y paquetes, que fue una parte importante de la transición. La exploración de cómo ajustar y escribir CI para un monorepo se tratará en su propio artículo en una fecha posterior.

Una vez que todo estuvo listo y configurado, fijamos una fecha para la migración: un sábado. Elegimos un día de fin de semana para que la gente pudiera irse a casa, dejar su trabajo un viernes, luego regresar el lunes siguiente y encontrar en lo que habían estado trabajando ahora dentro del monorepo.

En este punto, consideramos el polyrepo obsoleto porque no queríamos mantener varias historias de Git en conflicto continuamente. Para asegurarnos de que no se perdiera ningún trabajo, compilamos una lista de todas las ramas activas que la gente quería recuperar y transferir (agregamos soporte para esto en nuestro script de creación de monorepo).

Por otro lado

Tan irrealmente ambicioso como suena el plan en el papel, ¡funcionó para nosotros sin problemas! Durante la primera semana después de la migración, algunas canalizaciones fallaron y algunos fragmentos de código incompletos quedaron en la configuración de polyrepo y tuvieron que transferirse manualmente después de la transición. Aparte de estos y algunos otros contratiempos menores, todo salió bien. A nadie se le impidió seriamente continuar con su trabajo, y ahora que la migración se completó, nadie miró hacia atrás.

Hemos descubierto que el monorepo ofrece incluso más beneficios de los previstos desde la migración. Ahora es mucho más fácil incorporar personas a nuestro código base, gracias a la configuración de tipo de un solo clic en un entorno de desarrollo local. Se ha desarrollado una pequeña comunidad interna a su alrededor, y no son solo miembros del equipo de Proton Front-end. Incluye a cualquier persona interesada en la arquitectura monorepo y cualquiera que trabaje con la nuestra. En esta comunidad hablamos de:

  • Monorepos en general (y nuestros WebClients monorepo en particular).
  • Lidiar con problemas relacionados con monorepo cuando las personas necesitan ayuda.
  • Proponiendo mejoras en el flujo de trabajo de nuestro monorepo.

Lo que es más importante, ahora todos hablamos el mismo idioma en lo que respecta al flujo de trabajo y la administración de Git. Dado que ahora todo es un solo repositorio de Git, también normalizamos las pautas para Git en diferentes equipos de características de front-end y configuramos universalmente las reglas de nuestra herramienta de alojamiento de Git que abarca todo el monorepo (por ejemplo, reglas de combinación).

Conclusión

En retrospectiva, esta implementación de monorepo ha superado nuestras expectativas. Es una buena solución dadas nuestras necesidades, ¡y estamos contentos de haberla elegido! La mejora en la experiencia del desarrollador condujo a un aumento notable en la productividad. Todavía no es una bala de plata, y hay muchos desafíos que conlleva, pero para nosotros, estos desafíos son superados en gran medida por los beneficios que ha brindado. Esperamos que esta arquitectura de paquete de referencia se mantenga y nos permita escalar y agregar cualquier otro paquete requerido con facilidad en el futuro previsible.

El repositorio de Git que se analiza en este artículo es de código abierto y se puede encontrar en https://github.com/ProtonMail/WebClients.