learning-front

Difícil A · Ingeniería a escala: monorepos y publicación

Changesets y versionado a escala

Publicar varios paquetes sin volverte loco: versionado semántico aplicado en equipo, changelogs automáticos y releases coordinadas con Changesets.

Semver cuando publicas#

Ya conoces semver como consumidor: el ^ del package.json que permite que el gestor de paquetes actualice automáticamente dentro del mismo major. Ahora te toca el otro lado: eres quien publica y decides qué número poner.

La regla de semver es mecánica pero su aplicación requiere criterio:

  • Patch (1.2.3 → 1.2.4): arreglas un bug sin cambiar la API pública. El comportamiento que prometiste antes sigue prometido, solo que más correcto.
  • Minor (1.2.3 → 1.3.0): añades funcionalidad nueva que no rompe lo que ya existía. Quien tenía ^1.2.3 recibirá tu 1.3.0 automáticamente: si rompe algo, es tu fallo, no el suyo.
  • Major (1.2.3 → 2.0.0): cambias la API de forma que el código existente deja de funcionar. Eliminas una exportación, cambias la firma de una función, renombras un tipo.

El punto delicado es el breaking change real. No todo lo que parece rompedor lo es:

  • Añadir un parámetro opcional a una función → minor (el código existente sigue llamándola igual).
  • Cambiar el tipo de retorno de string a string | null → major (quien hacía .trim() al resultado ahora tiene un error de TypeScript o un crash en runtime).
  • Añadir una nueva exportación con nombre → minor (no colisiona con nada).
  • Renombrar una exportación existente → major.

El test práctico: ¿puede alguien que depende de mi paquete con ^ actualizar a esta versión sin tocar nada de su código? Si la respuesta es no, es major.

Los rangos ^ y ~ en el package.json de tus consumidores reflejan cuánto confían en tu versionado. Si publicas un breaking change como minor, estás traicionando esa confianza: su pnpm install instalará tu versión nueva y su código romperá en silencio, sin ningún error visible hasta que lo ejecuten.

El dolor de versionar a mano#

Con un solo paquete, versionar es fácil: editas el package.json, escribes el CHANGELOG.md a mano, haces la etiqueta de git y publicas.

Con un monorepo de cinco o diez paquetes, el problema escala:

  1. @team-builder/core corrige un bug de cálculo de winrate.
  2. @team-builder/ui consume core con workspace:*.
  3. ¿Tienes que subir ui también? ¿Con qué bump?
  4. ¿Quién actualiza el CHANGELOG.md de core? ¿Y el de ui?
  5. Hay cuatro PRs en paralelo. ¿Qué versión toca a cada uno?

Coordinar todo esto a mano entre varios developers es una fuente constante de errores: changelogs que no coinciden con los commits, versiones que se saltan, dependencias internas que apuntan a versiones antiguas. Changesets resuelve esto con un flujo en dos tiempos.

Changesets: ficheros de intención y flujo en dos tiempos#

La idea central de Changesets es separar el momento en que describes un cambio del momento en que aplicas el bump de versión.

Los ficheros .changeset/*.md#

Cada vez que terminas un cambio publicable, ejecutas pnpm changeset (o pnpm changeset add). El CLI interactivo te pregunta:

  • ¿Qué paquetes han cambiado?
  • ¿Qué tipo de bump? (major / minor / patch)
  • ¿Cuál es el resumen del cambio?

Y crea un fichero en .changeset/ con un nombre aleatorio:

---
"@team-builder/ui": minor
---

Añade el componente `HeroCard` que muestra nombre, rol y estadísticas de un héroe.
No rompe la API existente: exportación nueva, sin cambios en las exportaciones actuales.

El fichero tiene un frontmatter YAML con el paquete y el bump, y una descripción en Markdown que irá directamente al CHANGELOG.md. Este fichero se commitea junto con el código del cambio: es la intención documentada.

changeset version — versionar#

Cuando el equipo decide que es hora de publicar (normalmente en CI, al hacer merge a main), se ejecuta:

# Consume todos los ficheros .changeset/*.md:
# - Sube las versiones en package.json
# - Genera o actualiza CHANGELOG.md en cada paquete afectado
# - Reescribe las dependencias internas de workspace:* a la version concreta
# - Borra los ficheros .changeset/*.md consumidos
pnpm changeset version

El resultado es un commit con los package.json actualizados, los CHANGELOG.md generados y los ficheros .changeset/ eliminados. Este commit es el punto de no retorno: a partir de aquí, las versiones están decididas.

changeset publish — publicar#

# Publica al registry (npm u otro) todos los paquetes
# cuya version en package.json no coincide con la del registry.
# Crea también las etiquetas de git para cada version publicada.
pnpm changeset publish

La separación version / publish es deliberada: puedes revisar el commit de version (y los changelogs generados) antes de publicar. En CI es habitual que publish solo corra en la rama main tras el merge.

Fixed, linked y prereleases#

Fixed y linked#

Por defecto, cada paquete versiona de forma independiente. Changesets ofrece dos modos de coordinación en su config.json:

{
"$schema": "https://unpkg.com/@changesets/config/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [["@team-builder/core", "@team-builder/ui"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
  • fixed: todos los paquetes del grupo siempre tienen el mismo número de versión. Si core sube a 2.0.0, ui también sube a 2.0.0 aunque no haya cambiado nada en ui. Es el modelo de Babel o Jest: una única “versión del proyecto”.
  • linked: los paquetes coordinan el bump hacia arriba. Si core tiene un bump major y ui tiene un bump minor, ui también sube con el major, porque el bump de core “arrastra” al grupo. Cada paquete puede tener su propio historial de versiones, pero nunca quedan desfasados hacia abajo.

La opción updateInternalDependencies: "patch" controla qué bump recibe un paquete que depende de uno que cambió pero no tiene un changeset propio: por defecto un patch para que la dep interna quede fijada a la versión exacta publicada.

Prereleases y snapshots#

Para probar una versión antes de publicar en el canal estable:

# Entra en modo prerelease con la etiqueta "beta"
pnpm changeset pre enter beta

# Ahora changeset version genera versiones como 1.2.0-beta.0
pnpm changeset version

# Publica en el canal beta (no afecta a las instalaciones con latest)
pnpm changeset publish --tag beta

# Sal del modo prerelease cuando estés listo para la version estable
pnpm changeset pre exit

Los snapshots son más ligeros: generan una versión puntual sin consumir los changesets definitivamente, útil en CI de pull requests para que otros equipos puedan probar tu rama antes de que se merge.

# Publica una version de snapshot sin consumir los changesets.
# La version tendra un formato como: 0.0.0-pr-42-20240315
pnpm changeset version --snapshot
pnpm changeset publish --tag snapshot

En CI: el PR automático “Version Packages”#

El flujo recomendado en proyectos reales usa el bot oficial de Changesets (una GitHub Action) que automatiza todo el proceso:

  1. Un developer termina su trabajo, crea el .changeset/*.md y abre un PR.
  2. El bot comenta en el PR avisando de que hay un changeset pendiente.
  3. Cuando el PR se merge a main, la Action comprueba si hay changesets sin consumir.
  4. Si los hay, abre automáticamente un PR llamado “Version Packages” con el resultado de changeset version: versiones actualizadas, changelogs generados y ficheros .changeset/ eliminados.
  5. El equipo revisa ese PR, lo aprueba y hace merge.
  6. Al merge de “Version Packages”, otra Action ejecuta changeset publish y publica.

Este flujo separa la decisión (qué bump y qué resumen, en el PR de la feature) de la ejecución (el merge de “Version Packages”). El changelog se escribe cuando el developer tiene el contexto del cambio fresco, no cuando toca publicar semanas después.

Todo eso cabe en un solo .github/workflows/release.yml. Es el fichero que un junior hereda el primer día en un equipo que publica paquetes:

# .github/workflows/release.yml — versiona y publica con Changesets
name: Release

# Corre en cada push a main (es decir, tras el merge de cualquier PR).
on:
push:
  branches: [main]

# Permisos del job: escribir en el repo (abrir el PR "Version Packages"),
# gestionar PRs, y id-token para firmar la procedencia al publicar en npm.
permissions:
contents: write
pull-requests: write
id-token: write

jobs:
release:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - uses: pnpm/action-setup@v4

    - uses: actions/setup-node@v4
      with:
        node-version: 22
        cache: pnpm

    - run: pnpm install --frozen-lockfile

    # El corazón del flujo. La Action mira si hay changesets sin consumir en main:
    #   - si LOS HAY, ejecuta 'version' y abre/actualiza el PR "Version Packages".
    #   - si NO los hay (acabas de mergear ese PR), ejecuta 'publish' y sube al registry.
    - uses: changesets/action@v1
      with:
        version: pnpm changeset version
        publish: pnpm changeset publish
      env:
        # Token de GitHub (lo inyecta GitHub solo) para abrir/actualizar el PR.
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        # Token de automatización de npm, para publicar al registry.
        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        # Activa la procedencia (provenance): npm firma DE DÓNDE salió el paquete.
        NPM_CONFIG_PROVENANCE: true

Dos detalles que en 2026 son estándar, no extra:

  • NPM_TOKEN es un token de automatización de npm (no tu contraseña): se guarda como secret del repo y solo lo ve este workflow. Publicar desde CI, nunca desde tu portátil, es lo que evita que una versión salga de una máquina sin auditar.
  • Procedencia (provenance) — con id-token: write y NPM_CONFIG_PROVENANCE: true, npm usa el OIDC de GitHub para registrar un comprobante firmado de que ese paquete se construyó en ESE workflow, en ESE commit del repo público. Quien lo instala puede verificar que el .tgz publicado corresponde de verdad al código del repo, no a algo que alguien coló a mano. Es la defensa directa contra un paquete suplantado en la cadena de suministro.

Comprueba lo que sabes#

Pregunta 1 de 8

Un paquete está en 2.4.1. Añades una función nueva que no rompe nada. ¿Qué versión corresponde?

Tu turno#

El ejercicio versiona @team-builder/ui en el monorepo del Team Builder con Changesets: declaras el cambio con pnpm changeset, eliges el bump correcto, ejecutas pnpm changeset version y observas el resultado en el CHANGELOG.md y en los package.json. Las soluciones muestran el fichero .changeset y el CHANGELOG de cada tier con sus comentarios de por qué cada nivel mejora al anterior.

Ejercicio · hazlo en local

Versionar @team-builder/ui con Changesets

Sobre el monorepo del Team Builder (packages/core + packages/ui), declara un cambio en @team-builder/ui con pnpm changeset, elige el bump correcto, ejecuta changeset version y observa el resultado en el CHANGELOG.md y en los package.json. Las soluciones muestran el fichero .changeset y el CHANGELOG de cada tier con sus comentarios.

Paso 1: Crea el changeset y versiona

  • El fichero .changeset/<nombre>.md existe tras pnpm changeset.
  • pnpm changeset version se ejecuta sin errores.
  • La versión de @team-builder/ui en su package.json ha subido.

Cómo hacerlo en local

Clona el repositorio del curso y entra en la carpeta del ejercicio. Sigue el README para preparar el monorepo con Changesets y ejecuta los comandos paso a paso. Compara el resultado con las soluciones de cada tier.

git clone <repo>
cd exercises/dificil-escala/changesets-y-versionado
pnpm changeset
pnpm changeset version
# revisa el bump de version en packages/ui/package.json
# revisa el CHANGELOG.md generado en packages/ui/
Ver soluciones
No hay soluciones disponibles para este ejercicio todavía.