Lección 3 de 313 min
Versionado y automatización de releases
Conventional Commits, Semantic Release, Changesets y Release Please, y hooks de Git con Husky y lint-staged.
Actualizas una librería de la 2.3 a la 2.4 y tu aplicación deja de compilar. Si la librería sigue el versionado semántico, el fallo es suyo: un cambio en la cifra del medio promete que nada se rompe. Pero esa promesa solo se cumple si alguien decide bien el número de cada versión, y las personas se equivocan, se olvidan de actualizar el changelog o publican a medias.
En esta lección verás cómo hacer que la versión, el changelog y la publicación salgan solos a partir de los mensajes de commit, con Conventional Commits y herramientas como Semantic Release, Changesets o Release Please. Y cómo Husky y lint-staged frenan los errores antes incluso de que exista el commit.
Mensajes que deciden la versión
Versionado semántico (SemVer)
MAJOR.MINOR.PATCH: tres números que prometen qué puede romperse al actualizar.
- Especificación:
- SemVer 2.0.0
- Autor:
- Tom Preston-Werner
El versionado semántico da significado a cada número de MAJOR.MINOR.PATCH. Sube PATCH (1.4.2 → 1.4.3) cuando corriges errores sin cambiar cómo se usa; MINOR (1.4.2 → 1.5.0) cuando añades funciones compatibles con lo anterior; y MAJOR (1.4.2 → 2.0.0) cuando rompes la compatibilidad. Al subir un número, los de su derecha vuelven a cero.
Ya lo viste en la lección de gestores de paquetes: gracias a esas promesas, un rango como ^1.4.2 acepta cualquier 1.x posterior, pero nunca la 2.0.0. También existen versiones previas, como 2.0.0-beta.1, que se ordenan antes de la versión final.
Las versiones 0.x son especiales: indican desarrollo inicial, en el que cualquier cosa puede cambiar. La 1.0.0 marca el momento en que la API pública se considera estable.
Úsalo cuando
En todo lo que usan otras personas o programas: librerías, APIs, aplicaciones con versiones publicadas.
Ten en cuenta
SemVer es una promesa, no una garantía: depende de que quien publica clasifique bien cada cambio. Por eso conviene que la versión se deduzca de los commits y no de la memoria de nadie.
1.4.2 → 1.4.3 corrige un error (PATCH)
1.4.2 → 1.5.0 añade una función (MINOR)
1.4.2 → 2.0.0 elimina o cambia una API (MAJOR)
2.0.0-beta.1 versión previa a la 2.0.0
0.8.1 desarrollo inicial: puede cambiar cualquier cosaConventional Commits
Una convención para los mensajes de commit que las máquinas pueden interpretar.
- Especificación:
- 1.0.0
- Inspirada en:
- la convención de Angular
Conventional Commits fija el formato de la primera línea: tipo(ámbito): descripción. El tipo dice qué clase de cambio es: feat para una función nueva, fix para una corrección y otros como docs, refactor, test, perf, ci o chore. El ámbito (scope), opcional, indica la parte afectada, como en fix(carrito): ….
Un cambio incompatible se marca con ! tras el tipo (feat(api)!: …) o con un pie BREAKING CHANGE: al final del mensaje. Así, cada commit se traduce en una subida de versión: fix es un PATCH, feat un MINOR y cualquier cambio incompatible, un MAJOR.
Para que nadie se salte la convención, commitlint revisa cada mensaje. Con su configuración convencional (@commitlint/config-conventional) rechaza tipos desconocidos, el tipo en mayúsculas, descripciones vacías o terminadas en punto y también las que empiezan en mayúscula.
Úsalo cuando
Cuando quieres generar versiones y changelogs automáticamente, o simplemente una historia de Git que se entienda de un vistazo.
Ten en cuenta
Solo funciona si el tipo es honesto: un cambio incompatible etiquetado como fix publicará un PATCH que rompe a tus usuarios. Si fusionas con squash, el título del merge request se convierte en el mensaje del commit, así que revísalo también.
feat(carrito): añade cupones de descuento
fix: corrige el redondeo del IVA
docs: explica cómo ejecutar el proyecto en local
feat(api)!: elimina el endpoint /v1/pedidos
# Un mensaje completo, con cuerpo y pie:
fix(api): envía las fechas en formato ISO 8601
Antes se enviaba la marca de tiempo en milisegundos.
BREAKING CHANGE: el campo «fecha» ya no es un número.Interactivo
De los commits a la versión
Escribe la versión actual y los commits desde la última release. El simulador valida cada mensaje como commitlint y calcula la versión siguiente con las reglas de semantic-release (preset conventionalcommits).
→1.5.0minor
feat(carrito): añade cupones de descuento
minorfix: corrige el redondeo del IVA
patchdocs: explica cómo ejecutar el proyecto en local
sin releaseArreglado el login
rechazadoformatoFalta el tipo seguido de dos puntos, como en «fix: corrige…».
O añade uno de ejemplo
CHANGELOG.md generado
## 1.5.0
### Nuevas funciones
* **carrito:** añade cupones de descuento
### Correcciones
* corrige el redondeo del IVANo aparecen en el changelog: docs.
El cambio de más nivel es un feat: sube la MINOR y la PATCH vuelve a cero. Las correcciones viajan en la misma versión. commitlint habría rechazado 1 mensaje; semantic-release no los tiene en cuenta.
Automatizar las releases
Las tres herramientas producen lo mismo (un número de versión, un changelog y una publicación), pero difieren en quién decide la versión y cuándo se publica.
Semantic Release
Versión, changelog y publicación decididos por completo por los commits, en el pipeline.
- Se ejecuta en:
- el pipeline de CI
- Decide la versión:
- los commits
Semantic Release se ejecuta en el pipeline cada vez que algo llega a la rama de release. Busca la última etiqueta de Git (por ejemplo, v1.4.2), analiza los commits posteriores, calcula la versión siguiente, genera las notas de la release, crea la etiqueta v1.5.0 y publica: en npm, en GitLab, en GitHub o donde indiquen sus plugins.
No hay ningún paso humano: si se fusiona un feat, sale una versión nueva. Todo se configura con plugins (commit-analyzer, release-notes-generator, npm, gitlab…) en un archivo como .releaserc.json, y en el pipeline basta con ejecutar npx semantic-release.
Úsalo cuando
En librerías y paquetes de un solo proyecto que quieren publicar cada cambio en cuanto se fusiona, sin que nadie tenga que acordarse.
Ten en cuenta
La configuración por defecto sigue la convención de Angular; si tu equipo marca los cambios incompatibles con !, usa el preset conventionalcommits, que lo reconoce. No está pensado para monorepos y, como nadie revisa el número antes de publicar, un tipo mal puesto sale tal cual.
{
"branches": ["main"],
"plugins": [
["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }],
["@semantic-release/release-notes-generator", { "preset": "conventionalcommits" }],
"@semantic-release/npm",
"@semantic-release/gitlab"
]
}Changesets
Cada cambio declara su propia subida de versión en un pequeño archivo Markdown.
- Ideal para:
- monorepos
- Decide la versión:
- una persona
Con Changesets, la versión no se deduce de los commits: la decide quien hace el cambio. Al preparar su rama ejecuta npx changeset, elige qué paquetes cambian, si es major, minor o patch, y escribe un resumen. El resultado es un archivo en la carpeta .changeset/ que viaja en el merge request y se revisa como el resto del código.
Cuando toca publicar, changeset version consume todos los changesets pendientes, sube las versiones en cada package.json, actualiza los CHANGELOG.md y ajusta las dependencias entre paquetes; después, changeset publish los publica. Brilla en monorepos como los que viste con Turborepo, porque cada paquete se versiona por separado.
Úsalo cuando
En monorepos con varios paquetes publicables, o cuando prefieres que una persona decida y redacte lo que entra en el changelog.
Ten en cuenta
Si alguien olvida añadir el changeset, su cambio no genera ninguna versión. La acción oficial que abre y mantiene el pull request de versión es para GitHub; en GitLab necesitas herramientas de la comunidad o tus propios scripts.
---
"@tienda/carrito": minor
"@tienda/ui": patch
---
Añade cupones de descuento al carrito y ajusta el estilo del campo de código.Release Please
Lee tus Conventional Commits y mantiene un PR de release que fusionas cuando quieras.
- Creado por:
- Plataforma:
- GitHub
Release Please, creado por Google, también calcula la versión a partir de Conventional Commits, pero no publica por su cuenta. En su lugar, abre y mantiene un pull request de release con la versión nueva y el CHANGELOG.md actualizado. Cada commit que llega a main lo actualiza.
Cuando decides publicar, fusionas ese pull request: Release Please crea la etiqueta y la release en GitHub, y los pasos siguientes de tu pipeline publican el paquete o despliegan. Admite muchos lenguajes (Node, Python, Java, Go, Rust…) y monorepos mediante un archivo de manifiesto.
Úsalo cuando
Si quieres versiones automáticas a partir de los commits pero prefieres elegir el momento de publicar, y tu código está en GitHub.
Ten en cuenta
Está pensado para GitHub, cuya API usa para el pull request y las releases, así que con GitLab tendrás que elegir otra herramienta. Y la publicación en npm o el despliegue los añades tú después de que cree la release.
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: nodeHooks de Git: frenar errores antes del commit
Git puede ejecutar scripts en ciertos momentos, como justo antes de crear un commit o al escribir su mensaje: son los hooks. El problema es que viven en .git/hooks, una carpeta que no se versiona, así que cada persona tendría que instalarlos a mano.
Husky
Hooks de Git guardados en el repositorio e instalados para todo el equipo.
- Carpeta:
- .husky/
- Hooks típicos:
- pre-commit, commit-msg
Husky guarda los hooks en una carpeta .husky/ del repositorio y configura Git para que los use. Al ejecutar npx husky init, añade al package.json un script prepare que los activa automáticamente en cuanto alguien hace npm install.
Los dos hooks más útiles son pre-commit, que se ejecuta antes de crear el commit (ideal para pasar el linter), y commit-msg, que recibe el mensaje y puede rechazarlo (ideal para commitlint). Si un hook termina con error, el commit no se crea.
Úsalo cuando
Para que todo el equipo pase las mismas comprobaciones rápidas antes de cada commit, sin depender de que cada cual lo configure.
Ten en cuenta
Cualquiera puede saltárselos con git commit --no-verify, así que el pipeline de CI debe repetir las comprobaciones. Y no metas en el pre-commit tareas lentas, como toda la batería de pruebas: el equipo acabará desactivándolo.
npm install --save-dev husky
npx husky init # crea .husky/pre-commit y el script «prepare»
# .husky/pre-commit: se ejecuta antes de cada commit
npx lint-staged
# .husky/commit-msg: valida el mensaje con commitlint
npx --no -- commitlint --edit $1lint-staged
Ejecuta linters y formateadores solo sobre los archivos que vas a commitear.
Pasar ESLint y Prettier por todo el proyecto en cada commit sería lento. lint-staged mira qué archivos están en la staging area y ejecuta sobre ellos los comandos que configures según su extensión. Si un comando corrige algo, como prettier --write, la corrección entra en el commit.
Combinado con el hook pre-commit de Husky, garantiza que todo lo que entra en el repositorio pasa el linter y tiene el formato correcto, y tarda segundos aunque el proyecto tenga miles de archivos. Verás ESLint y Prettier a fondo en el módulo de testing y calidad.
Úsalo cuando
Siempre que uses Husky con linters o formateadores: es la pieza que hace que el hook sea rápido.
Ten en cuenta
Solo revisa los archivos que cambias, así que un error en otro archivo puede seguir ahí. El análisis completo, y el tsc que necesita ver todo el proyecto, déjalos para el pipeline.
{
"lint-staged": {
"*.{js,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,md,json}": "prettier --write"
}
}Pruébalo
Con Husky, lint-staged y commitlint, cada commit pasa tres filtros: el pre-commit corrige el formato de los archivos preparados, el linter bloquea los errores y el commit-msg rechaza los mensajes que no siguen Conventional Commits. Vuelve al simulador de arriba y comprueba qué mensajes habría dejado pasar commitlint.
Interactivo
Semantic Release, Changesets y Release Please
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Semantic Release | Changesets | Release Please |
|---|---|---|---|
| Quién decide la versión | Los commits, de forma automática | Quien hace el cambio, en un archivo de changeset | Los commits, pero tú eliges cuándo publicar |
| Necesita Conventional Commits | Sí | No | Sí |
| Cuándo se publica | En cada merge a la rama de release, sin pasos manuales | Al ejecutar changeset publish, a menudo tras fusionar un PR de versión | Cuando fusionas el PR de release que mantiene |
| Monorepos | No de serie; hay plugins de la comunidad | Sí, es su punto fuerte | Sí, con un archivo de manifiesto |
| Plataforma | Cualquier CI, con plugins para npm, GitLab y GitHub | Cualquier CI; acción oficial para GitHub | GitHub |
| Changelog | Notas de la release; CHANGELOG.md con un plugin | Un CHANGELOG.md por paquete, con los resúmenes | CHANGELOG.md actualizado en el PR de release |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Mantienes una librería de utilidades en GitLab y quieres que cada merge a main con un feat o un fix se publique en npm al momento, sin que nadie toque nada.
Para recordar
- SemVer da significado a cada número: PATCH corrige, MINOR añade sin romper y MAJOR rompe la compatibilidad.
- Con Conventional Commits, el tipo de cada commit (
fix,feat,!) decide la subida de versión, y commitlint rechaza los mensajes que no siguen el formato. - Semantic Release publica solo en cada merge; Release Please prepara un PR de release que fusionas cuando quieres; Changesets deja que cada persona declare su cambio, ideal en monorepos.
- Husky versiona los hooks de Git y lint-staged los hace rápidos, pero el pipeline debe repetir las comprobaciones porque los hooks se pueden saltar.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.Tu librería está en la 1.4.2. Desde la última release hay un
fix, undocsy unfeat. ¿Qué versión calcula Semantic Release?2.¿Cuál de estos mensajes rechazaría commitlint con la configuración convencional?
3.¿Cuál es la diferencia principal entre Changesets y Semantic Release?
4.Un compañero hace
git commit --no-verifyy se salta los hooks de Husky. ¿Qué evita que su error llegue amain?5.Tu paquete está en la 0.9.3 y fusionas
feat!: cambia la firma de crear(). Con el presetconventionalcommits, ¿qué publica Semantic Release?