Saltar al contenido principal

Git avanzado para DevOps

Git no es solo donde guardamos el código. Es el disparador de todo el pipeline: un push lanza los tests, un merge despliega a staging, un tag publica una release.

Por eso la estrategia de ramas que elijamos no es una preferencia estética. Determina con qué frecuencia podemos desplegar.

Las estrategias de branching​

GitFlow​

Ramas separadas para desarrollo, releases, hotfixes y funcionalidades. Fue el estándar durante años.

Su problema para DevOps es estructural: está pensado para releases planificadas, con versiones que se preparan y se publican cada cierto tiempo. Si queremos desplegar varias veces al día, la ceremonia de ramas sobra.

Sigue teniendo sentido en un caso concreto: software que se distribuye y del que mantenemos varias versiones a la vez, como una aplicación de escritorio o una librería con soporte a versiones antiguas.

GitHub Flow​

Una sola rama permanente, main, siempre desplegable. Ramas de funcionalidad cortas que se fusionan mediante pull request.

main ──●──●──●──●──●──●── (siempre desplegable)
╲ ╱ ╲ ╱
●──● ●──● (ramas de días, no semanas)

Es lo que queremos para entrega continua, y la opción por defecto para la mayoría de los equipos que despliegan servicios web.

La condición que lo hace funcionar: las ramas tienen que ser cortas. Una rama de tres semanas convierte cualquier estrategia en integración esporádica, con sus conflictos gigantes correspondientes.

Trunk-based development​

El extremo: todo el mundo integra en main a diario, con ramas de horas o directamente sin ramas.

Es el modelo que la investigación de DORA correlaciona con mejor rendimiento, pero exige una base sólida: tests fiables, feature flags para el código a medias y una cultura donde romper main se arregla en minutos.

GitFlowGitHub FlowTrunk-based
Ramas permanentesVariasUnaUna
Vida de una ramaSemanasDíasHoras
Encaja conReleases planificadasEntrega continuaDespliegue continuo
RequierePocoTests decentesTests sólidos y feature flags

Elijamos por la frecuencia de despliegue que necesitamos, no por lo que hace la empresa de al lado.

Conventional commits​

Un formato de mensaje que las herramientas pueden interpretar:

feat(auth): añadir autenticación con JWT
fix(api): corregir fuga de memoria en el servicio de usuarios
docs(readme): actualizar instrucciones de instalación
chore(deps): actualizar dependencias

feat!: cambiar el método de autenticación de la API

El valor no está en la estética, sino en lo que permite automatizar:

  • Generar el changelog sin escribirlo a mano
  • Deducir la siguiente versión automáticamente
  • Filtrar el historial por tipo de cambio

Esa exclamación del último ejemplo marca un cambio incompatible, y es la que hace saltar la versión mayor.

Versionado semántico​

MAYOR.MENOR.PARCHE → 2.4.1
  • MAYOR — rompe compatibilidad
  • MENOR — añade funcionalidad sin romper nada
  • PARCHE — corrige errores

Combinado con conventional commits, la versión se calcula sola: un fix sube el parche, un feat sube la menor y un ! sube la mayor.

Ahí está el truco de todo esto: si los mensajes de commit son consistentes, no hay que decidir la versión ni escribir el changelog. Sale del historial.

Releases automáticas​

name: Release
on:
push:
branches: [main]

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # se necesita el historial completo
- uses: cycjimmy/semantic-release-action@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Con eso, cada merge a main analiza los commits nuevos, calcula la versión, crea el tag, genera el changelog y publica la release. Nadie decide nada a mano.

El fetch-depth: 0 es imprescindible: sin el historial completo, la herramienta no puede saber qué ha cambiado desde el último tag.

Hooks​

Los hooks ejecutan comprobaciones en momentos concretos del flujo de Git. El más útil es pre-commit:

# .git/hooks/pre-commit
#!/bin/sh
npm run lint || exit 1
npm test || exit 1

Con dos advertencias importantes:

No se versionan. Los ficheros de .git/hooks/ no viajan con el repositorio, así que cada persona tiene que instalárselos. Herramientas como pre-commit o husky resuelven esto versionando la configuración.

Cualquiera puede saltárselos con git commit --no-verify. Por eso un hook nunca es un control de seguridad: sirve para evitar despistes y dar feedback rápido. El control de verdad va en CI, donde nadie puede saltárselo.

Un buen candidato para un hook de pre-commit es la detección de secretos, que tenemos con laboratorio en el capítulo 2 del curso de DevSecOps.

Rebase o merge​

Una discusión eterna que tiene una respuesta razonable:

  • Rebase en nuestra rama, antes de abrir el pull request, para tener un historial limpio y lineal
  • Merge para integrar en main, para conservar el contexto de que esos commits eran una unidad

Y la regla que no se negocia: nunca hagamos rebase de algo que ya hemos compartido. Reescribir commits que otros tienen en su copia local es la forma más rápida de arruinarle la mañana a alguien.

Proteger la rama principal​

Si main tiene que estar siempre desplegable, hay que impedir que no lo esté:

  • Prohibir el push directo
  • Exigir un pull request con al menos una aprobación
  • Exigir que el CI esté en verde
  • Exigir que la rama esté actualizada antes de fusionar

Esa última evita un fallo sutil: dos ramas que pasan los tests por separado pueden romper al juntarse.

Firmar commits​

git config --global commit.gpgsign true

Sin firma, cualquiera puede hacer un commit con nuestro nombre y nuestro correo: Git no lo comprueba. En proyectos abiertos o con requisitos de trazabilidad, la firma es lo que convierte la autoría en algo verificable.

Resumen​

  • La estrategia de ramas determina con qué frecuencia podemos desplegar
  • GitHub Flow para la mayoría; GitFlow solo para releases planificadas
  • Las ramas cortas son lo que hace que cualquier estrategia funcione
  • Conventional commits permiten calcular versión y changelog automáticamente
  • Los hooks evitan despistes; el control real está en CI
  • Rebase en nuestra rama, merge para integrar, y nunca rebase de lo compartido
  • Protejamos main, incluida la exigencia de estar actualizada antes de fusionar

Recursos adicionales​


💡 Recuerda: si nuestras ramas viven semanas, no estamos integrando de forma continua por muchos jobs en verde que tengamos.

En el próximo capítulo vemos cómo encajan las metodologías ágiles con todo esto.