Del commit al changelog: automatizar versiones con Conventional Commits y GitHub Actions
Cuando escribí sobre cómo escribir mensajes de commit claros, útiles y automatizables dejé una promesa a medias. Expliqué la especificación de Conventional Commits y dije que permite “automatizar procesos como generación de changelogs, control de versiones y despliegues”… y ahí lo dejé.
Este artículo es la otra mitad: el sistema que lee esos commits. Si ninguna herramienta usa la convención, se queda en un acuerdo de estilo, y esos acuerdos se abandonan en cuanto hay prisa.
Qué se automatiza exactamente
La idea es que el tipo del commit determine el salto de versión, sin que nadie decida a mano:
| Commit | Salto | Ejemplo |
|---|---|---|
fix: |
patch | 1.4.2 → 1.4.3 |
feat: |
minor | 1.4.2 → 1.5.0 |
BREAKING CHANGE: en el cuerpo, o feat!: |
major | 1.4.2 → 2.0.0 |
chore:, docs:, style:, refactor:, test: |
ninguno | 1.4.2 |
A partir de eso, la herramienta calcula la versión siguiente, agrupa los commits por tipo, escribe el CHANGELOG.md, crea la etiqueta de git y publica la release. No vuelves a tocar un número de versión a mano.
Y el changelog deja de ser una tarea pendiente. Las tareas de documentación que dependen de acordarse suelen acabar abandonadas.
Elegir herramienta
Hay dos formas de hacerlo, y conviene entender la diferencia antes de copiar un YAML.
semantic-release publica en cuanto llega el commit. Haces merge a main y el workflow calcula la versión, crea la etiqueta y publica, sin intervención. Encaja con una librería que se entrega de forma continua.
release-please abre un pull request de release. Acumula los commits desde la última versión en un PR que contiene el CHANGELOG.md y el bump de versión. Ese PR se queda esperando: publicas cuando lo mergeas.
Yo prefiero la segunda para casi todo, por una razón práctica: te deja ver el changelog antes de que exista la versión. Si un commit quedó mal descrito, lo corriges en el PR y no en una release ya publicada. Además, publicar sigue siendo una decisión de una persona, algo que en proyectos con cliente suele ser obligatorio.
El resto del artículo usa release-please, pero los problemas del final aplican a las dos.
El workflow
Un .github/workflows/release.yml mínimo:
name: release
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node
Tres cosas en las que fijarse:
El bloque permissions es obligatorio. Sin contents: write la action no puede crear etiquetas ni commits, y sin pull-requests: write no puede abrir el PR. Falla con un 403 que no dice qué falta, y es el motivo más habitual de que no funcione a la primera.
release-type define de dónde sale la versión. node la lee y la escribe en package.json. Hay tipos para otros lenguajes, y simple si solo quieres un archivo version.txt sin ecosistema detrás.
Cuando copies esto, comprueba cuál es la versión mayor actual de cada action: cambian con el tiempo, y el @v4 de hoy puede no ser el vigente cuando lo leas.
Validar los mensajes
Todo lo anterior tiene un punto débil: un commit que no sigue la convención no rompe nada, simplemente no aparece en el changelog, y nadie se entera.
Por eso hay que validar. La herramienta estándar es commitlint:
npm i -D @commitlint/cli @commitlint/config-conventional husky
npx husky init
echo 'npx --no -- commitlint --edit $1' > .husky/commit-msg
Y la configuración, en commitlint.config.js:
export default { extends: ["@commitlint/config-conventional"] };
A partir de ahí, un git commit -m "arreglos varios" se rechaza en tu máquina antes de crearse.
Eso sí, los hooks locales no son una garantía: se saltan con --no-verify y no existen para quien clone el repositorio sin instalar las dependencias. Si el proyecto tiene más de una persona, valida también en CI:
name: commitlint
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: wagoid/commitlint-github-action@v6
Ese fetch-depth: 0 es necesario: sin el historial completo, la action no sabe qué commits trae el PR y no valida nada, también sin avisar.
Tres problemas que no dan error
Cualquiera de estos te puede costar una tarde, porque ninguno da un error claro.
1. El squash merge sustituye tus commits
Si tu repositorio usa squash and merge, los commits del PR desaparecen y el mensaje que llega a main es el título del pull request. Da igual lo bien escritos que estén los diez commits de la rama: el que cuenta es ese.
Con squash activado tienes que validar el título del PR, no los commits:
name: pr-title
on:
pull_request:
types: [opened, edited, synchronize]
permissions:
pull-requests: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Es el más frecuente, y confunde porque el equipo sí está escribiendo bien los commits; ese trabajo simplemente se pierde en el merge.
2. GITHUB_TOKEN no dispara otros workflows
Este es el más desconcertante. GitHub no dispara workflows a partir de eventos creados con el GITHUB_TOKEN por defecto. Es una protección para evitar bucles infinitos, y tiene sentido.
La consecuencia es que, si tienes un workflow con on: release: [published] para desplegar o publicar en npm, no se va a ejecutar cuando release-please cree la release. El pipeline se queda a medias sin ningún error.
Hay dos soluciones:
-
Meter el despliegue en el mismo workflow, condicionado a la salida de la action. Es lo más sencillo y lo que recomiendo:
- uses: googleapis/release-please-action@v4 id: release with: release-type: node - if: ${{ steps.release.outputs.release_created }} run: npm publish -
Usar un token propio (un PAT o un token de GitHub App) en lugar del
GITHUB_TOKEN. Funciona, pero es un secreto más que renovar, con más permisos de los que suele hacer falta.
3. El bucle de CI
Si tu automatización hace commits en el repositorio y tu CI se ejecuta en cada push, tienes un bucle. Poner [skip ci] en el mensaje del commit automático lo corta; las herramientas conocidas ya lo hacen, pero si automatizas algo por tu cuenta, acuérdate.
¿Y si tu proyecto no tiene versiones?
No todo esto aplica a cualquier proyecto. Este blog, por ejemplo, no tiene número de versión: se despliega en cada push y no hay nada que publicar como release.
En proyectos así, lo útil es la otra mitad: commitlint y un changelog generado, sin versionado semántico. Sigues teniendo un historial legible y una lista de cambios, que es casi todo lo que aporta, sin inventarte un 2.4.0 que no significa nada.
La pregunta que lo decide es sencilla: ¿alguien de fuera depende del número de versión? Si no, el versionado semántico no te aporta mucho.

