wandres.dev
CHANGESETS · versionado en monorepo

Changesets en monorepo: fixed, linked y dependencias internas

En un monorepo los paquetes dependen unos de otros, y versionarlos bien exige coordinar el grafo entero. Changesets nació para esto: propaga los ascensos por las dependencias internas, respeta el protocolo workspace, y ofrece dos políticas de agrupación —fixed y linked— para paquetes que deben moverse juntos. Cerramos el nivel con el versionado coordinado de todo un ecosistema.

⏱ 16 min

Todo lo que hemos visto brilla de verdad en su hábitat natural: el monorepo. Cuando decenas de paquetes conviven en un repositorio y dependen unos de otros, versionar deja de ser un acto local y se vuelve un problema de grafo. Tocar los tokens de diseño obliga a repensar los botones que los consumen, y los botones arrastran a la aplicación que los monta. Changesets fue concebido precisamente para este terreno: sabe propagar un ascenso por toda la cadena de dependencias internas, respeta el protocolo workspace con que los paquetes se referencian entre sí, y te da dos políticas —fixed y linked— para los grupos de paquetes que quieres mover al unísono. Esta lección cierra el nivel ensamblando esas piezas en el versionado coordinado de un ecosistema entero.

🎯 Al terminar esta lección sabrás
  • Entender cómo Changesets propaga un ascenso por el grafo de dependencias internas de un monorepo.
  • Distinguir con precisión fixed de linked: cuándo los paquetes suben juntos y cuándo solo comparten número.
  • Configurar updateInternalDependencies y respetar el protocolo workspace en el versionado.
  • Excluir paquetes del versionado con ignore y gobernar los paquetes privados.

El monorepo cambia las reglas

En un repositorio de un solo paquete, versionar es contestar una pregunta. En un monorepo con paquetes interdependientes, versionar es resolver una reacción en cadena. Cuando un paquete sube de versión, todos los que dependen de él quedan, técnicamente, apuntando a una versión que ya no es la última, y hay que decidir si ellos también deben publicarse para consumir la novedad. Changesets automatiza esa decisión: propaga el ascenso hacia arriba por el grafo, paquete a paquete.

flowchart TB
cs[Changeset minor en tokens] --> tok[El paquete tokens sube a minor]
tok --> bot[botones depende de tokens]
bot --> botb[botones recibe un patch y ajusta el rango]
botb --> ui[ui depende de botones]
ui --> uib[ui recibe un patch en cascada]
style tok fill:#a6e3a1,color:#11111b
style uib fill:#89b4fa,color:#11111b

Sigue la cascada del diagrama. Declaras un minor en @acme/tokens. Al versionar, tokens sube a su nuevo minor; pero @acme/botones depende de tokens, así que Changesets lo asciende también —al menos un patch— y actualiza el rango con que declara esa dependencia. Y como @acme/ui depende a su vez de botones, el ascenso vuelve a propagarse: ui recibe su patch en cascada. Un solo changeset en la base del grafo termina moviendo a tres paquetes, cada uno con su entrada de changelog explicando que se actualizó por una dependencia. Nadie tuvo que rastrear esa cadena a mano; el comando la recorrió por ti.

Esta propagación es lo que hace confiable un monorepo. Sin ella, publicarías tokens y dejarías a botones referenciando en silencio una versión vieja, un desajuste que estalla semanas después en la máquina de un usuario. Con ella, el grafo interno queda siempre coherente tras cada release: cada paquete apunta a versiones que existen y que son las que se probaron juntas.

La cascada obedece a reglas fijas que conviene tener presentes:

  • Solo se propaga hacia arriba: de la dependencia a quien depende de ella, nunca al revés.
  • El ascenso heredado es como mínimo un patch, ajustable con updateInternalDependencies.
  • El rango con que el dependiente declara la dependencia se reescribe a la versión nueva.
  • La propagación es transitiva: recorre el grafo entero, nivel a nivel, hasta agotarlo.

Dentro del repositorio, esa coherencia se apoya en el protocolo workspace, que Changesets entiende y reescribe al versionar:

// En el repo: apunta al codigo local con el protocolo workspace
{ "dependencies": { "@acme/tokens": "workspace:^" } }

// Al publicar: el gestor sustituye workspace por la version real
{ "dependencies": { "@acme/tokens": "^2.1.0" } }

fixed frente a linked

Por defecto, cada paquete tiene su propia línea de versión y solo sube cuando lo tocan sus propios changesets o la cascada. Pero a veces quieres que un grupo de paquetes comparta destino. Changesets ofrece dos políticas para ello, parecidas en la superficie y opuestas en el fondo, y confundirlas es un error clásico.

# fixed frente a linked, la diferencia clave
fixed   ->  todos suben juntos a la misma version, aunque no cambien
linked  ->  comparten numero de version, pero solo sube el que cambia

Un grupo fixed se comporta como un solo paquete con varias fachadas: si uno recibe un changeset, todos los del grupo suben a la misma versión nueva, incluso los que no cambiaron nada. Es la política de las suites fuertemente acopladas que se publican como una unidad —el caso arquetípico es un compilador y sus plugins oficiales, que siempre comparten número—. La ventaja es la simplicidad mental: una sola versión para todo el grupo. El coste es el ruido: publicas versiones de paquetes que no cambiaron.

Un grupo linked es más sutil. Los paquetes comparten la línea de versión, pero solo se publican los que de verdad cambiaron; cuando varios del grupo se publican a la vez, se alinean todos al número más alto. Un paquete linked que no tiene changesets en este ciclo no se publica: se queda en su versión anterior. Así evitas el ruido de fixed conservando la coherencia de numeración cuando los cambios sí coinciden.

Un ejemplo hace tangible la diferencia. Supón un grupo con @acme/react y @acme/vue, ambos en 1.4.0, y un changeset minor solo en @acme/react:

# Mismo grupo, misma entrada, dos politicas opuestas
fixed   ->  react 1.5.0  y  vue 1.5.0   vue sube sin haber cambiado
linked  ->  react 1.5.0  y  vue 1.4.0   vue se queda porque no cambio

La elección entre uno y otro no es técnica sino editorial: le dice al usuario si tus paquetes son un producto con varias piezas —entonces fixed— o piezas hermanas que comparten linaje pero viven su propia vida —entonces linked—.

🔗

fixed

Bloque indivisible. Un changeset en cualquiera sube a todos a la misma versión, cambien o no. Para suites que se publican como una sola cosa.

🪢

linked

Numeración compartida sin lockstep. Solo sube el que cambia, y si varios coinciden se alinean al más alto. Menos ruido que fixed.

🧩

independiente

El comportamiento por defecto. Cada paquete lleva su propia versión y solo se mueve por sus changesets o por la cascada de dependencias.

workspace, updateInternalDependencies e ignore

Los paquetes de un monorepo se referencian entre sí con el protocolo workspace, escribiendo por ejemplo workspace:^ en lugar de una versión fija. Changesets entiende este protocolo: durante el versionado ajusta esos rangos internos, y en el momento de publicar es el gestor de paquetes quien reemplaza workspace: por la versión real que se está publicando. Así, dentro del repositorio las dependencias siempre apuntan al código local, pero el paquete publicado lleva rangos de versión normales.

La opción updateInternalDependencies gobierna la fuerza de la cascada. Su valor por defecto, patch, significa que cuando una dependencia interna sube, su dependiente recibe al menos un patch. Si lo subes a minor, cada dependiente ascenderá al menos un minor ante cualquier cambio en lo que consume. Y ignore es la tijera: lista los paquetes que Changesets nunca debe versionar ni publicar —la aplicación de ejemplo, la documentación, los bancos de pruebas—, que viven en el monorepo pero no salen al registro.

{
  "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
  "changelog": ["@changesets/changelog-github", { "repo": "acme/design-system" }],
  "commit": false,
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "fixed": [["@acme/core", "@acme/runtime"]],
  "linked": [["@acme/react", "@acme/vue"]],
  "ignore": ["@acme/docs", "@acme/playground"]
}

Ese config.json es el panel de mando del versionado del monorepo. Sus campos más decisivos:

  • fixed — grupos que suben siempre juntos a la misma versión, cambien o no.
  • linked — grupos que comparten numeración pero solo publican a los que cambian.
  • updateInternalDependencies — la cota mínima del ascenso en cascada: patch o minor.
  • ignore — paquetes que nunca se versionan ni publican, aunque vivan en el repositorio.
  • accesspublic para que los paquetes con scope salgan visibles al registro.
  • baseBranch — la rama contra la que changeset status compara para exigir changesets.
ℹ️
Privados no es lo mismo que ignorados

Un paquete marcado como privado en su package.json no se publica al registro, pero Changesets sí lo versiona por defecto, porque necesita mantener coherente el grafo interno. Si además quieres que ni siquiera se le calcule versión ni se le cree tag, eso se controla aparte —con ignore o con la configuración de paquetes privados—. La distinción importa: privado dice no publicar; ignorado dice ni siquiera considerar. Un banco de pruebas suele ser privado e ignorado; una librería interna compartida suele ser privada pero versionada.

El monorepo revela que una versión no describe un paquete, describe un momento del grafo entero

Cierra este nivel con la idea que reordena todo lo anterior. En un repositorio de un solo paquete es fácil creer que una versión es una etiqueta que describe ese paquete: su estado, sus features, sus bugs. El monorepo destruye esa ilusión y revela la verdad más honda: una versión no describe un paquete aislado, describe un corte coherente del grafo entero de paquetes en un instante. Cuando publicas, no lanzas piezas sueltas al vacío; lanzas una fotografía del ecosistema en la que cada paquete apunta a versiones de sus vecinos que existen, que se probaron juntas y que forman un todo consistente. Por eso Changesets no versiona paquete a paquete de forma independiente, sino que propaga ascensos por el grafo, alinea grupos con fixed y linked, y actualiza los rangos internos: todo su trabajo consiste en garantizar que ese corte sea coherente, que no publiques un botones que dependa de un tokens que no existe. Y aquí está la lección que llevarte más allá de esta herramienta, la que separa a quien gestiona librerías de quien gestiona ecosistemas: en un sistema de piezas interdependientes, la unidad de razonamiento nunca es la pieza, es la configuración coherente del conjunto. Lo verás repetirse en los lockfiles que congelan un árbol entero de dependencias, en las imágenes de contenedor que sellan un sistema completo, en las migraciones que versionan un esquema entero, en la infraestructura como código que describe una topología completa. Todos comparten el mismo principio: cuando las cosas dependen unas de otras, versionar la coherencia del todo importa más que versionar cada parte, porque el usuario no instala una parte, hereda un estado del mundo. Aprender Changesets en monorepo es, en realidad, aprender a pensar en cortes coherentes de un grafo, y ese es un músculo que usarás en cada sistema complejo que construyas.

⚔️ Coordina el grafo entero
  1. Monta un monorepo con tres paquetes en cadena —tokens, botones, ui—, declara un changeset en el de la base y observa la cascada de ascensos.
  2. Configura un grupo fixed y comprueba que un changeset en uno sube a todos a la misma versión, incluso a los que no cambiaron.
  3. Configura un grupo linked y verifica la diferencia: solo suben los que cambian, alineándose al número más alto cuando coinciden.
  4. Cambia updateInternalDependencies de patch a minor y razona cómo altera la severidad de la cascada.
  5. Añade un paquete de documentación a ignore y explica por qué privado e ignorado son decisiones distintas.