Articles

Pasar de múltiples repositorios a un repositorio mono de lerna-js

At mitter.io, tenemos un par de paquetes npm de cara al público que necesitamos publicar, y recientemente nos mudamos a una estructura mono-repo administrada por Lerna de tener repositorios separados para cada uno de ellos. Hoy, me gustaría compartir nuestra experiencia de esta migración y nuestra configuración con la nueva estructura monorrepo. Todos nuestros paquetes son objetivos SDK o son dependencias para nuestros objetivos SDK:

  • @mitter-io/core – La funcionalidad principal de mitter.sdk io
  • @mitter-io/models – Los modelos typescript (clases, alias de tipo, interfaces, etc.) para que el Sdk
  • @mitter-io/web – La web SDK
  • @mitter-io/react-native – El Reaccionar Native SDK
  • @mitter-io/node – El nodo.SDK de js (utilizado para node.motores js)
  • @mitter-io/react-scl – La biblioteca de componentes estándar para aplicaciones ReactJS

Todos nuestros paquetes están escritos en TypeScript y las tipografías se incluyen con los paquetes en sí, y no distribuimos paquetes de escritura separados (los que normalmente se ven comenzando con @types/). Usamos rollup para agrupar estos paquetes en los formatos de módulo UMD y ES5. Además, utilizamos TypeDoc para generar nuestra documentación, que luego se publica en un bucket público en AWS S3.

Antes de usar Lerna, teníamos un repositorio separado para cada uno de nuestros paquetes y funcionaba bien, mientras que solo teníamos el SDK web. A medida que avanzábamos y había más desarrolladores trabajando en el SDK, comenzamos a enfrentar algunos problemas con nuestra configuración:

  1. Dado que la mayor parte de la lógica del SDK reside en @mitter-io/core, casi todos los cambios que se produjeron en el paquete core y todos los demás paquetes tuvieron que actualizarse para apuntar a la nueva versión. Por lo tanto, incluso si hubiera un error que se corrigiera para, por ejemplo, React Native, el cambio iría en core, pero la actualización debía reflejarse ahora en todos los demás destinos, es decir, webnode y react-native. Era bastante común que un desarrollador fallara un objetivo.
  2. Casi todos los cambios en el SDK resultarían en cambios en al menos 3 de los 5 paquetes.
  3. Vimos un gran beneficio en mantener la misma versión en todos los paquetes (hace que sea más fácil para los desarrolladores adivinar cuál sería la última versión de target), pero el seguimiento manual de esto se estaba volviendo engorroso.
  4. npm link(o yarn link si lo prefiere) tenía su propio conjunto de problemas para asegurarse de que todas las dependencias estuvieran vinculadas, luego desvinculadas para usar la correcta desde npm y volver al enlace local para el desarrollo.
  5. Era bastante común ejecutar scripts entre paquetes (p.ej., para publicar los documentos de typescript), y estábamos utilizando un andamiaje frágil de enlaces simbólicos y scripts bash para administrar el mismo.

En ese momento, nos encontramos con Lerna y parecía ser el ajuste perfecto para nuestros requisitos.

Decidimos seguir la ruta más simple que existe, tratando de usar los valores predeterminados tanto como sea posible. Por lo que experimentamos, migrar a Lerna fue una brisa. Comience creando un nuevo repositorio de Lerna:

mkdir my-new-monorepo && cd my-new-monorepo
git init .
lerna init

Responda un par de preguntas simples (donde siempre recurrimos a la predeterminada) y estará listo. Mover nuestros paquetes antiguos de sus repositorios al nuevo (que temíamos ya que pensábamos que sería un gran dolor) fue mucho más fácil de lo esperado:

lerna import ~/projects/my-single-repo-package-1 --flatten

TENGA en cuenta que --flatten puede ser necesario o no, pero nos enfrentamos a problemas sin él.

Lo sorprendente de Lerna es que trae todas las confirmaciones de git junto con él (es posible que pierda algo de historial con --flatten), de modo que para el nuevo repositorio, el historial parece que el desarrollo siempre ha estado sucediendo en este monorepo. Esto es absolutamente esencial porque va a necesitar git blame alguien para un error que descubrió después de mudarse al monorrepo.

Configuración actual

Con Lerna, ahora administramos un único repositorio para todos nuestros paquetes, con una estructura de directorios que se parece a esta:

packages/
core/
models/
node/
react-native/
web/
lerna.json
package.json

Para publicar los paquetes modificados, ahora simplemente tenemos que:

lerna boostrap
lerna publish

No tiene que hacer lerna bootstrap cada vez; solo si es la primera vez que está revisando el repositorio. Lo que hace es simplemente instalar todas las dependencias de cada uno de los paquetes bajo este repositorio.

Al mismo tiempo, también decidimos agilizar un poco nuestro proceso y agregar todas las tareas de empaquetado dentro del ciclo de vida npm. Ten en cuenta que esto no tiene nada que ver con Lerna; esto es algo que idealmente debería estar presente en cualquier paquete npm, independientemente de la estructura del repositorio. Para cada uno de los paquetes, los siguientes scripts están presentes en el pacakge.json:

"scripts": {
...
"prepare": "yarn run build",
"prepublishOnly": "./../../ci-scripts/publish-tsdocs.sh"
...
}

Esto construye el paquete con el compilador typescript, lo empaqueta con rollup y genera documentos con typedoc:

"scripts": {
...
"build": "tsc --module commonjs && rollup -c rollup.config.ts && typedoc --out docs --target es6 --theme minimal --mode file src"
...
}

Tener una única estructura de repositorio también le permite mantener scripts comunes en un solo lugar para que los cambios se apliquen a todos los paquetes (también deberíamos mover el script de compilación a un script separado, dado que ahora se ha convertido en un comando bash bastante complejo).

El flujo de desarrollador

El flujo de desarrollador, aparte de las versiones, no cambia. Un desarrollador crea un problema en GitLab (o se le asigna uno), crea una nueva rama para el problema y, a continuación, fusiona los cambios en maestro después de revisar el código. El ciclo de vida de la versión ahora sigue un proceso extremadamente estructurado:

  1. Cuando se completa un hito y estamos planeando hacer una nueva versión, uno de los desarrolladores (a cargo de esa versión en particular) crea una nueva versión ejecutando lerna version.
  2. Lerna proporciona un mensaje extremadamente útil y fácil de usar para averiguar la siguiente versión
(master) mitter-js-sdk ツ lerna version --force-publish
lerna notice cli v3.8.1
lerna info current version 0.6.2
lerna info Looking for changed packages since v0.6.2
? Select a new version (currently 0.6.2) (Use arrow keys)
❯ Patch (0.6.3)
Minor (0.7.0)
Major (1.0.0)
Prepatch (0.6.3-alpha.0)
Preminor (0.7.0-alpha.0)
Premajor (1.0.0-alpha.0)
Custom Prerelease
Custom Version

Una vez que se selecciona una nueva versión, Lerna cambia las versiones de los paquetes, crea una etiqueta en el repositorio remoto y envía los cambios a nuestra instancia de GitLab. Más allá de esto, los desarrolladores no están obligados a hacer nada más. Nuestro CI está configurado para construir todas las etiquetas que tengan un nombre similar a un número versionado semántico.

NOTA se ejecute lerna version--force-publish porque queremos que todos los paquetes tienen el mismo linaje de versiones. Así que a veces tendremos paquetes que no difieren entre las diferentes versiones. Dependiendo de su preferencia, puede optar por no hacerlo.

La configuración del CI

Utilizamos el CI integrado de GitLab para crear, probar y publicar en todos nuestros proyectos (JS y Java). Para el nuevo monorepo de JS, tenemos dos etapas:

  1. Build
  2. Publish

La fase de compilación es extremadamente simple y ejecuta los siguientes dos scripts:

lerna bootstrap
lerna run build

Esta fase se ejecuta en cada commit para validar esencialmente la cordura del paquete. La fase de publicación, por otro lado, ejecuta lo siguiente:

git checkout master
lerna bootstrap
git reset --hard
lerna publish from-package --yes

Nos dimos cuenta de que teníamos que hacer un git checkout master y un git reset --hard porque GitLab clona (o obtiene, dependiendo de la configuración) el repositorio, y luego comprueba el commit que se va a construir. Esto establece el directorio de trabajo en modo ‘CABEZA separada’, es decir, el ref HEAD no apunta a ninguna parte. Lerna usa HEAD para averiguar la versión actual del paquete y los errores en el estado de encabezado separado.

también Tenemos que ejecutar lerna publish from-package frente a lerna publish, como la ejecución de un simple lerna publish habría Lerna quejándose de que la versión actual ya está publicado, como los metadatos se actualizan cuando el promotor de la ran lerna version localmente. El argumentofrom-package le dice a Lerna que publique todas las versiones que no están actualmente presentes en npm para un paquete dado. Esto también ayuda si una publicación falla por alguna razón y estás volviendo a intentar la canalización.

La fase de publicación está configurada para ejecutarse solo en etiquetas que coincidan con el siguiente crédito de expresiones regulares:

^v(0|\d*)\.(0|\d*)\.(0|\d*)(-(0|\d*|\d**)(\.(0|\d*|\d**))*)?(\++(\.+)*)?$

Esto es un poco sofisticado, y para la mayoría de los equipos y para la mayoría de los propósitos, simplemente ^v*$ debería funcionar. 🙂

NOTA Aunque todavía no lo hemos hecho, ya que somos un equipo pequeño, también se podría marcar cualquier etiqueta que siga la expresión regular anterior como protegida en GitLab para restringir quién puede publicar paquetes a npm.

Puede consultar nuestro monorrepo en https://github.com/mitterio/js-sdk (Esto se refleja en nuestro repositorio interno de GitLab).

Notas rápidas

Cuando se ejecutan scripts comunes (como lo hacemos para publicar documentos de typescript), es bastante útil conocer los detalles del paquete que ejecuta el script. Esto se aplica a los scripts en el ciclo de vida de npm, así como a los scripts que se pueden ejecutar con lerna run o lerna exec. Para un paquete dado en npm, npm hace que todo package.json esté disponible para un script utilizando variables de entorno. Por lo tanto, para un paquete dado con el siguiente package.json:

{
"name": "@mitter-io/core",
"version": "0.6.28",
"repository": {
"type": "git"
}
}

Las siguientes variables estarán disponibles mientras se ejecuta cualquier script de ciclo de vida:

npm_package_name=@mitter-io/core
npm_package_version=0.6.28
npm_package_repository_type=git

Peculiaridades/problemas

Un par de cosas en las que todavía estamos trabajando con el nueva configuración (algunos de ellos son problemas, mientras que algunos probablemente no conozcamos mejor):

  • No estoy seguro de si es posible, pero nos gustaría poder tener scripts de ciclo de vida comunes para todos nuestros paquetes. Declarar estas en la raíz package.json no funciona.
  • Es extremadamente difícil probar su configuración de Lerna completamente sin publicar algo en npm. No estoy seguro de si hay un --dry-run en algún lugar.
  • Lerna tiene una forma de mantener un bloque de configuración común para devDependencies de modo que todos los devDependencies sean de la misma versión para cada uno de los subpaquetes. Esta es una característica bastante genial, pero nos llevaría algún tiempo eliminar todas las comunes.
  • Lo mismo podría aplicarse a otras dependencias, así que aunque no queremos un bloque de configuración dependencies, tener una forma de expresar variables disponibles en los proyectos sería bueno. Por ejemplo, en nuestra Java/Kotlin monorepo, utilizamos la etiqueta gradle.properties para contener variables como springBootVersionspringCoreVersion, etc., que luego son utilizados por los scripts de gradle individuales.

Nuestros pensamientos sobre monorepos
Ha sido un debate bastante acalorado recientemente con monorepos y si estamos viendo a un gran número saltar al carro de nuevo, que recuerda bastante a la época en que los microservicios estaban de moda.

La estructura que seguimos aquí tiene múltiples monorepos, y esta no es la primera vez que administramos monorepos. Toda nuestra plataforma y backend es un monorrepo que contiene código privado, desplegable y múltiples paquetes de cara pública que se publican en bintray. También tenemos nuestro sitio web principal funcionando con un backend de resorte, con el frontend incluido con el paquete web que admite la recarga en caliente (webpack watch), etc. Nunca decidimos ir con un solo repositorio único en toda la organización porque las herramientas simplemente no estaban allí.

Tener la mayor parte de nuestro código Java en un único repositorio funciona muy bien porquegradle proporciona todas las herramientas necesarias para el monorrepo de Java ylerna y el ciclo de vida de npm proporciona las herramientas para el monorrepo del SDK de JS. Así que, en pocas palabras, los monorepos son geniales una vez que identificas la cobertura de los cambios que van en tu repositorio. Para nuestro backend Java, vimos múltiples MRs en varios proyectos para una sola característica, lo que nos inclinó a pasar a un monorrepo solo para este proyecto en particular, con todo el resto de nuestro código aún en repositorios separados. Y una vez que vimos surgir un patrón similar para nuestros SDK de JS, nos mudamos a Lerna.

Tenga en cuenta que somos un equipo pequeño de aproximadamente 9 ingenieros, por lo que lo que funciona para nosotros podría no funcionar para equipos de diferentes tamaños. Lo que más nos gustaría señalar es que la adopción de cualquier solución no tiene que ser binaria, en la que o lo hacemos según lo prescrito o no lo hacemos en absoluto.

Algunas de las motivaciones que vimos para un monorrepo definitivamente se aplicaron a nosotros y muchas de ellas no. Por ejemplo, simplemente no podemos dedicar tiempo a construir las herramientas si todo nuestro código base se trasladó a un único repositorio, independientemente del beneficio que podamos experimentar o no. Así que el debate en realidad no se trata de tener un «repositorio único», por sí solo, no es más que una nueva estructura de directorios. La prescripción de ellos es aliviar ciertos problemas y, como con toda «bala de plata», hay advertencias.

El debate es sobre los problemas comunes que se enfrentan en la industria del software y qué soluciones se han tomado comúnmente; «común» es la palabra clave. El área en la que te desvías de la aplicación «común» es donde puedes innovar, hacer cambios y construir un poco.