wandres.dev
CHANGESETS · versionado en monorepo

changeset version: consumir, versionar y generar el changelog

El comando changeset version es el corazón mecánico del sistema: lee todos los changesets pendientes, calcula la nueva versión de cada paquete tomando el bump más alto, actualiza package.json, escribe el CHANGELOG, propaga los cambios a las dependencias internas y borra los changesets consumidos. Todo en local, sin tocar el registro.

⏱ 16 min

Hasta ahora los changesets solo se acumulaban: intenciones dormidas en la carpeta .changeset/, esperando. El comando changeset version es el que las despierta y las consume. Toma el montón entero de changesets pendientes, calcula qué versión merece cada paquete, reescribe los package.json, redacta las entradas del CHANGELOG.md, propaga los ascensos por el grafo de dependencias internas y, finalmente, borra los changesets que acaba de gastar. Es una operación puramente local —no publica nada, no habla con npm— y su resultado es un conjunto de archivos modificados que puedes revisar como cualquier otro diff. Esa es su elegancia: convierte una decisión de release en un cambio de código auditable.

🎯 Al terminar esta lección sabrás
  • Describir con precisión qué hace y qué no hace changeset version sobre el árbol de archivos.
  • Entender la aritmética de agregación: cuando varios changesets tocan un paquete, gana el bump más alto.
  • Ver qué archivos toca: package.json, CHANGELOG.md y las dependencias internas.
  • Situar el PR de release como el diff revisable que produce este comando, no como magia opaca.

Qué hace exactamente el comando

changeset version es un transformador de archivos, no un publicador. Cuando lo ejecutas, recorre .changeset/, lee cada changeset pendiente y ejecuta una secuencia determinista de operaciones locales. Es importante fijar la frontera de lo que hace, porque la mitad de la confusión con Changesets nace de creer que este comando publica: no lo hace.

# Consumir todos los changesets pendientes y aplicar las versiones
pnpm changeset version

# Efecto: package.json y CHANGELOG.md modificados, .changeset/*.md borrados.
# NO publica, NO crea tags de git, NO hace commit por defecto.

Lo que hace, en orden: calcula la versión final de cada paquete afectado, la escribe en su package.json, genera o amplía su CHANGELOG.md con los resúmenes agrupados por severidad, actualiza los rangos de dependencia entre paquetes internos y elimina los archivos de changeset que ha consumido. Lo que no hace es igual de definitorio: no publica al registro, no crea etiquetas de git y no confirma el resultado en un commit —salvo que actives commit: true en la configuración—. Deja el trabajo hecho pero sin cometer, para que un humano o una Action lo revisen antes de sellarlo.

Esa separación es deliberada. Al dejar los cambios sin confirmar ni publicar, changeset version produce un estado intermedio inspeccionable: puedes abrir el diff, ver que la versión saltó de 1.2.4 a 1.3.0, leer las entradas nuevas del changelog y confirmar que las dependencias internas se ajustaron. Solo cuando ese diff te convence pasas al siguiente acto, que es publicar. Rara vez ejecutarás este comando a mano sobre main: lo normal es que lo corra la GitHub Action dentro del PR de release, y que tú solo revises su resultado.

En una sola pasada, changeset version deja el árbol de archivos así:

  • Sube la versión de cada paquete afectado en su package.json.
  • Crea o amplía el CHANGELOG.md de cada paquete con los resúmenes agrupados por severidad.
  • Reescribe los rangos de las dependencias internas para que apunten a las versiones nuevas.
  • Elimina de .changeset/ los archivos de changeset que acaba de consumir.
  • No confirma, no etiqueta y no publica: deja el trabajo listo para revisión.

La aritmética: el bump más alto gana

El detalle más sutil de changeset version es cómo agrega múltiples changesets que afectan al mismo paquete. Entre release y release, un paquete popular puede acumular diez changesets: siete patch, dos minor y uno major. La pregunta es qué versión sale, y la respuesta es una regla limpia: el bump más alto gana. El paquete sube una sola vez, al nivel más severo declarado, no una vez por cada changeset.

flowchart TB
pend[Changesets pendientes] --> ver[changeset version]
ver --> calc[Toma el bump mas alto por paquete]
ver --> pj[Sube la version en package json]
ver --> log[Escribe el changelog]
ver --> dep[Actualiza dependencias internas]
ver --> del[Borra los changesets consumidos]
style ver fill:#a6e3a1,color:#11111b
style log fill:#89b4fa,color:#11111b

El razonamiento es semántico, no aritmético. Si en este ciclo hay aunque sea un cambio que rompe compatibilidad, la release entera rompe compatibilidad, y eso es un major, sin importar cuántos patches lo acompañen. La severidad es una cota superior, no una suma: diez patches no equivalen a un minor, porque diez correcciones siguen sin añadir superficie nueva. Por eso el sistema colapsa el conjunto al máximo y no al total.

  • Diez changesets de patch sobre un paquete en 1.4.2 producen 1.4.3: una sola subida de patch.
  • Si entre ellos hay un minor, el resultado es 1.5.0: el minor domina a todos los patches.
  • Si además hay un major, sale 2.0.0: el major se impone sobre minor y patch por igual.
# Cinco changesets sobre un paquete en 1.4.2, y su version final
patch + patch + patch  ->  1.4.3   colapsan en un solo patch
patch + minor          ->  1.5.0   el minor domina a los patches
patch + minor + major  ->  2.0.0   el major se impone a todo

Aunque el número de versión colapse al máximo, ningún resumen se pierde. Los diez cuerpos de los diez changesets aparecen en el changelog, ordenados bajo sus encabezados de severidad. La versión se agrega; la narración se conserva entera. Así el usuario ve un único salto de versión pero la lista completa de lo que cambió.

Lo que toca: package.json, CHANGELOG y dependientes

El artefacto más visible que produce el comando es el CHANGELOG.md. Changesets lo construye por paquete, agrupando los resúmenes bajo tres encabezados —cambios mayores, menores y de parche— y añadiendo, cuando procede, una sección automática de dependencias actualizadas.

# @acme/botones

## 1.3.0

### Minor Changes

- a1b2c3d: Añade la variante fantasma al componente Botón con estados accesibles.

### Patch Changes

- e4f5g6h: Corrige el foco visible del botón en Safari.
- Updated dependencies [a1b2c3d]
  - @acme/tokens@2.1.0

Esa última entrada, la de dependencias actualizadas, revela la parte más inteligente del comando: la propagación por el grafo interno. Si el paquete @acme/tokens sube de versión y @acme/botones depende de él, changeset version no se limita a versionar tokens: también asciende botones —al menos un patch— y actualiza el rango con que botones declara su dependencia de tokens. La cota mínima de ese ascenso la fija la opción updateInternalDependencies, que veremos en detalle en la lección de monorepo.

🔢

Calcula y sube

Resuelve la versión final de cada paquete —el bump más alto gana— y la escribe en su package.json. Un salto por paquete, no uno por changeset.

📜

Redacta el changelog

Vuelca cada resumen al CHANGELOG.md, agrupado por severidad. El número se agrega, pero ninguna narración se pierde por el camino.

🕸️

Propaga y limpia

Actualiza los rangos de las dependencias internas por todo el grafo y borra los changesets consumidos, dejando .changeset/ vacía.

Versiones efímeras: snapshot y prerelease

Más allá de la release estable, changeset version sabe fabricar versiones de usar y tirar sin ensuciar tu línea principal. El modo snapshot genera un número único y descartable —del estilo 0.0.0-canary-20260726103000— pensado para publicar un adelanto por cada commit y probarlo en un proyecto real antes de cortar la versión de verdad.

# Publica un adelanto canary por commit, sin gastar los changesets reales
pnpm changeset version --snapshot canary
pnpm changeset publish --tag canary

Para una serie de prelanzamientos con continuidad —betas, release candidates— está el modo pre. La secuencia changeset pre enter next entra en modo prerelease; a partir de ahí cada changeset version produce números como 2.0.0-next.0 y 2.0.0-next.1, hasta que changeset pre exit cierra la serie y la primera release estable consolida en 2.0.0. Ambos modos conservan la propiedad esencial del comando: la versión sigue siendo una función determinista de los changesets, solo que la etiqueta la declara provisional.

  • El snapshot no consume tus changesets: el lote sigue intacto para la release estable.
  • La etiqueta de dist —canary, next— aísla estas versiones para que un npm install normal nunca las coja.
  • Al salir de pre, todos los prereleases acumulados colapsan en una única versión estable.
ℹ️
El comando es idempotente sobre su entrada, no sobre el árbol

Ejecutar changeset version una segunda vez sin nuevos changesets no vuelve a subir las versiones: al haber borrado los changesets consumidos, no queda entrada que procesar y el comando no tiene nada que hacer. La aritmética depende exclusivamente de los changesets presentes en .changeset/, no del estado actual de los package.json. Esta es la razón de que el flujo sea reproducible: dado el mismo conjunto de changesets sobre el mismo punto de partida, la salida es siempre idéntica.

Versionar es una función pura de tus intenciones, y por eso puede ser revisada como código

Lo verdaderamente profundo de changeset version es que transforma el versionado, tradicionalmente un acto manual y opaco, en una función determinista de una entrada explícita. Dale el mismo conjunto de changesets y el mismo estado de partida, y producirá exactamente el mismo resultado, siempre: las mismas versiones, el mismo changelog, la misma propagación por el grafo. Esta pureza no es un tecnicismo, es lo que hace posible todo lo demás. Porque si versionar es una función pura, su salida es un diff, y un diff se puede revisar, discutir, aprobar o rechazar en un Pull Request como cualquier otro cambio de código. El PR de release deja de ser un misterio —¿por qué esta versión, por qué este changelog?— y se vuelve una consecuencia trazable de decisiones que el equipo ya tomó y ya revisó, changeset a changeset. Fíjate en el desplazamiento conceptual, porque es el mismo que subyace a la infraestructura como código, a las migraciones de base de datos y a los builds reproducibles: coges un acto humano propenso al error —configurar un servidor a mano, alterar un esquema en producción, decidir una versión a ojo— y lo reexpresas como la aplicación determinista de una entrada declarativa y versionada. Ganas reproducibilidad, ganas revisión, ganas auditoría, y sobre todo pierdes el miedo, porque nada de lo que produce el comando es irreversible ni sorpresivo: es solo el diff que se deduce de lo que ya habías escrito. Cuando entiendas que la meta de estas herramientas es convertir decisiones en funciones puras sobre datos declarados, reconocerás el mismo patrón en la mitad de las herramientas serias que uses el resto de tu carrera.

⚔️ Ejecuta, inspecciona y comprende la agregación
  1. Con dos changesets pendientes sobre un paquete —uno patch y uno minor—, ejecuta changeset version y confirma que la versión sube una sola vez, al minor.
  2. Abre el diff resultante y localiza los tres cambios: la versión en package.json, las entradas nuevas en CHANGELOG.md y los changesets borrados.
  3. Provoca una propagación: haz que un paquete dependa de otro, versiona el de abajo y observa cómo el de arriba recibe su patch y su rango actualizado.
  4. Ejecuta el comando una segunda vez sin nuevos changesets y verifica que no vuelve a alterar las versiones.
  5. Explica en dos frases por qué que changeset version sea una función determinista permite revisar la release como un diff.