La lección anterior dejó una pregunta abierta. GitHub Flow pide ramas cortas, pero no dice cuánto de cortas, y no ofrece respuesta para el trabajo que no cabe en tres días.
Trunk Based Development (desarrollo basado en el tronco, a menudo abreviado TBD) responde a las dos cosas con una regla que suena casi agresiva:
Cada persona del equipo integra su trabajo en el tronco al menos una vez al día.
No "cuando la funcionalidad esté lista". No "cuando pase la revisión". Cada día. Aunque la funcionalidad no esté terminada. Aunque no sirva todavía para nada.
La primera reacción de casi todo el mundo es la misma: eso es imposible, ¿cómo voy a integrar código a medias en la rama que se despliega a producción? Esa objeción es correcta, y responderla es el contenido central de esta lección. Existe un mecanismo que lo hace posible —separar el despliegue de la publicación— y sin entenderlo, TBD parece una temeridad. Con él, es probablemente la práctica que más impacto tiene sobre la velocidad de entrega de un equipo.
En gestor-tareas, el equipo lleva ya un tiempo con GitHub Flow en la versión en la nube. Funciona, pero han detectado un patrón: dos o tres veces al mes, una rama que ha vivido diez días llega con conflictos en app.js que cuestan media jornada de resolver y que a veces introducen fallos. Ana propone ir un paso más allá.
Contenido
- La idea y de dónde viene
- Por qué el objetivo real es reducir el tiempo entre integraciones
- Las dos variantes del modelo
- Separar despliegue de publicación: banderas de funcionalidad
- El coste de las banderas: la deuda de banderas
- Branch by abstraction: cambios grandes sin ramas largas
- Ramas de versión solo cuando hacen falta
- Qué exige TBD
- Comparativa final de los tres flujos
- Árbol de decisión: cuál elegir
- La idea y de dónde viene
Trunk Based Development no es un modelo nuevo: es el más antiguo de los tres. Antes de que Git popularizara las ramas baratas, ramificar era caro y doloroso —en CVS o Subversion, una rama de dos semanas era una aventura— y por eso todo el mundo trabajaba contra la línea principal. TBD toma esa práctica antigua y la reivindica con argumentos modernos.
Su formulación actual viene de dos sitios. Del movimiento de integración continua —Kent Beck y la programación extrema en los años noventa, que ya decía "integra al menos una vez al día"— y del libro Continuous Delivery de Jez Humble y David Farley (2010). Es también el modelo que usan internamente Google, Facebook y otras empresas con repositorios enormes.
Los elementos del modelo:
- Un tronco (
main,trunk,master): la única rama de larga vida. - Todo el mundo integra ahí al menos una vez al día.
- Las ramas, si existen, viven horas, no días.
- El trabajo sin terminar viaja al tronco oculto tras una bandera.
- El tronco está siempre sano, garantizado por pruebas automáticas.
La diferencia con GitHub Flow es más pequeña de lo que parece y más importante de lo que parece. Ambos tienen una sola rama de larga vida. La diferencia es la frecuencia obligatoria de integración y las técnicas que se necesitan para sostenerla. GitHub Flow dice "ramas cortas"; TBD dice "menos de un día, sin excepciones, y aquí está cómo".
- Por qué el objetivo real es reducir el tiempo entre integraciones
Este apartado es el fundamento. Si no se entiende esto, TBD parece una regla arbitraria.
La curva del conflicto
Cuando dos personas trabajan sobre el mismo código en ramas separadas, sus versiones divergen. Y el coste de reconciliar esa divergencia no crece de forma lineal con el tiempo: crece mucho más deprisa.
| Tiempo separado | Commits divergentes | Coste típico de integrar |
|---|---|---|
| Horas | 1–3 | Casi siempre automático |
| 1 día | 3–10 | Conflicto ocasional, trivial |
| 1 semana | 20–50 | Varios conflictos, media hora |
| 2 semanas | 50–150 | Conflictos serios, medio día, riesgo de error |
| 1 mes o más | 150+ | Días, y a menudo se rehace el trabajo |
La razón de la aceleración es combinatoria. Con dos commits divergentes hay pocas formas de chocar. Con doscientos, la probabilidad de que alguien haya tocado el mismo fichero es casi uno, y además los cambios se han construido sobre supuestos distintos: no es solo que las líneas choquen, es que la función que ibas a modificar ya no existe, o el módulo se ha dividido en tres.
Y hay un tipo de conflicto que Git no detecta: el conflicto semántico. Ana renombra una función en app.js y Carla, en su rama, añade una llamada a esa función con el nombre viejo. Al fusionar, no hay conflicto textual —tocan líneas distintas— pero el código está roto. Cuanto más tiempo separadas, más de estos aparecen, y solo los detectan las pruebas.
flowchart LR
A["Integración<br/>poco frecuente"] --> B["Divergencia<br/>grande"]
B --> C["Conflictos costosos<br/>y arriesgados"]
C --> D["Miedo a integrar"]
D --> A
E["Integración<br/>diaria"] --> F["Divergencia<br/>mínima"]
F --> G["Conflictos triviales<br/>o inexistentes"]
G --> H["Integrar es rutina"]
H --> E
Los dos bucles son estables. El de arriba es el que sufre el equipo de gestor-tareas dos veces al mes. El de abajo es a donde quiere llegar Ana.
La consecuencia: es un problema de frecuencia, no de herramienta
Aquí está el giro conceptual que da nombre a la práctica. La integración continua no es un servidor de CI: es integrar de verdad, y a menudo. Un equipo puede tener el mejor servidor de comprobaciones del mundo y no estar haciendo integración continua, porque cada persona trabaja tres semanas en su rama y el servidor solo prueba ramas aisladas. Volveremos sobre esta idea en la lección 07-06, porque es la definición misma de CI.
TBD es simplemente la política de ramificación que hace posible la integración continua de verdad. Todo lo demás del modelo son técnicas para que esa política sea viable.
- Las dos variantes del modelo
TBD admite dos formas, y la elección depende sobre todo del tamaño del equipo y de si la revisión es obligatoria.
Variante A: commit directo al tronco
Cada persona confirma directamente en main, varias veces al día.
git switch main
git pull --rebase
# editar app.js
git commit -am "Anadir el esqueleto del panel de estadisticas"
git pull --rebase
git pushEl pull --rebase antes de enviar mantiene el historial lineal y evita commits de fusión triviales (lección 05-01).
Requisitos innegociables: batería de pruebas rápida que se ejecute antes de confirmar, equipo pequeño (2–5 personas), mucha confianza mutua y, normalmente, programación en pareja como sustituto de la revisión asíncrona.
Cuándo tiene sentido: equipos muy pequeños y experimentados, o proyectos donde el coste de un error es bajo. En la práctica es minoritaria, porque choca frontalmente con la revisión obligatoria y con las ramas protegidas de la lección 07-04.
Variante B: ramas de vida muy corta
La variante mayoritaria. Ramas que viven horas, con pull request y revisión rápida.
# 09:15 — empieza el trabajo
git switch main && git pull
git switch -c ana/panel-estadisticas
# 09:15–11:30 — trabajar
git commit -am "Anadir el esqueleto del panel de estadisticas"
git push -u origin ana/panel-estadisticas
# 11:30 — PR, revisión en menos de una hora, integrar, borrar rama
# 12:45 — de vuelta al tronco. Empieza la siguiente rama.Se parece mucho a GitHub Flow, y de hecho la frontera entre ambos es difusa. Las diferencias reales:
| GitHub Flow | TBD variante B | |
|---|---|---|
| Vida de la rama | Días (a veces más) | Horas, menos de un día |
| Alcance de la rama | Una funcionalidad completa | Un paso hacia la funcionalidad |
| ¿Se integra código sin terminar? | No | Sí, tras una bandera |
| Revisión | Cuidadosa, puede tardar | Rápida, minimizando la espera |
La segunda fila es la clave. En GitHub Flow, la unidad de trabajo es "la funcionalidad". En TBD, la unidad es "lo que puedo integrar hoy sin romper nada". Una funcionalidad grande se convierte en ocho o diez integraciones sucesivas, cada una segura por sí misma.
Un apunte sobre nombres: en TBD es habitual prefijar la rama con el nombre de quien la crea (ana/panel-estadisticas, bruno/cache-de-etiquetas). Deja claro de un vistazo que es una rama personal y efímera, no una rama compartida del proyecto.
- Separar despliegue de publicación: banderas de funcionalidad
Y ahora, la objeción del principio: ¿cómo se integra a diario código sin terminar en una rama que se despliega a producción?
La respuesta es una de las ideas más importantes de la ingeniería de software moderna:
Desplegar (que el código esté en producción) y publicar (que los usuarios lo usen) son dos cosas distintas y pueden ocurrir en momentos distintos.
El mecanismo son las banderas de funcionalidad (feature flags, feature toggles): condiciones que deciden en tiempo de ejecución si una funcionalidad está activa.
El ejemplo en gestor-tareas
Ana está construyendo el panel de estadísticas. Le llevará dos semanas. En lugar de una rama de dos semanas, el primer día integra esto en app.js:
// configuracion/banderas.js
// Banderas de funcionalidad de gestor-tareas.
// Cada entrada documenta quién la creó y cuándo debe retirarse.
export const BANDERAS = {
// GT-352 — Ana Ferrer — creada 2026-08-03 — retirar tras la publicación
panelEstadisticas: false,
// GT-338 — Bruno Salas — creada 2026-07-20 — retirar antes del 2026-08-15
exportacionCSV: true,
};// app.js
import { BANDERAS } from './configuracion/banderas.js';
function renderizarBarraLateral() {
const barra = document.querySelector('#barra-lateral');
barra.innerHTML = '';
barra.append(construirListaDeFiltros());
// La funcionalidad viaja a producción, pero apagada.
if (BANDERAS.panelEstadisticas) {
barra.append(construirPanelEstadisticas());
}
}Qué ha conseguido con esto:
- El código está en producción desde el primer día. Se despliega, se compila, se prueba junto al resto.
- Ningún usuario lo ve, porque la bandera está apagada.
- No hay rama de dos semanas, así que no hay divergencia ni conflictos grandes.
- Cada día Ana integra un paso más de
construirPanelEstadisticas(), siempre detrás de la misma bandera. - El día que está lista, publicarla es cambiar
falseportrue: un cambio de una línea, sin despliegue de código nuevo. - Y si algo va mal, apagarla es cambiar
trueporfalse: la reversión más rápida y menos arriesgada que existe. Nada degit revertcon prisa a las once de la noche.
Ese último punto se subestima. Revertir un despliegue con Git significa construir, probar y desplegar de nuevo; apagar una bandera es inmediato y no toca el código.
Grados de sofisticación
Las banderas admiten mucha más lógica que un booleano en un fichero:
// banderas.js — versión con activación selectiva
const CONFIGURACION = {
panelEstadisticas: {
activa: true,
// Solo para el equipo interno mientras se afina
usuarios: ['[email protected]', '[email protected]'],
// Y para un 10 % del resto, para medir impacto
porcentaje: 10,
},
};
export function estaActiva(nombre, usuario) {
const b = CONFIGURACION[nombre];
if (!b || !b.activa) return false;
if (b.usuarios?.includes(usuario.email)) return true;
if (b.porcentaje) return hashEstable(usuario.id) % 100 < b.porcentaje;
return true;
}// Uso en app.js
if (estaActiva('panelEstadisticas', usuarioActual)) {
barra.append(construirPanelEstadisticas());
}Con esto aparecen posibilidades que no tienen nada que ver con Git pero que explican por qué las banderas se han generalizado tanto: despliegue progresivo (activar para el 1%, luego el 10%, luego todos), pruebas A/B, activación por cliente y desactivación de emergencia.
Las cuatro familias de bandera
No todas las banderas son iguales, y confundirlas es el origen de la mayoría de los problemas:
| Tipo | Para qué | Vida esperada | ¿Se retira? |
|---|---|---|---|
| De publicación | Ocultar trabajo en curso hasta que esté listo | Días o semanas | Sí, siempre |
| De experimento | Prueba A/B, medir una variante | Semanas | Sí, al decidir |
| Operativa | Apagar algo pesado bajo carga (kill switch) | Permanente | No |
| De permisos | Funcionalidades por plan de suscripción | Permanente | No |
Las dos primeras son temporales por definición, y tratarlas como si fueran permanentes es exactamente el problema del apartado siguiente. Las dos últimas son configuración legítima del producto y no son deuda.
Un detalle importante de Git: el fichero de banderas es código versionado. Cambiar una bandera es un commit, con su mensaje, su revisión y su rastro en el historial. Cuando alguien pregunte "¿cuándo se activó el panel de estadísticas?", git log -- configuracion/banderas.js lo responde. Las plataformas de gestión de banderas mueven esa configuración fuera del repositorio, lo que gana inmediatez y pierde trazabilidad; es un intercambio consciente.
- El coste de las banderas: la deuda de banderas
Las banderas no son gratis, y conviene decirlo con la misma claridad con la que se venden sus ventajas.
Cada bandera duplica los caminos posibles del código. Con una bandera hay dos comportamientos que probar. Con diez banderas independientes hay, en teoría, 1024 combinaciones. Nadie prueba 1024 combinaciones. En la práctica se prueban dos o tres, y las demás son territorio inexplorado donde viven los fallos raros que solo le pasan a un cliente.
Los síntomas de un equipo con deuda de banderas:
- Banderas de hace dos años que nadie se atreve a tocar porque nadie sabe qué hacen.
- Código muerto tras banderas permanentemente apagadas, que sigue compilándose y manteniéndose.
- Condicionales anidados de banderas:
if (A && !B) { ... } else if (B && C) { ... }. - Fallos que solo se reproducen con una combinación concreta de banderas, imposibles de depurar.
- Nadie sabe qué está realmente activo en producción.
La disciplina que lo evita
Regla 1: toda bandera temporal nace con fecha de caducidad. Escrita en el propio código, como en el ejemplo del apartado anterior. Si al llegar la fecha sigue ahí, es un ticket automático.
Regla 2: retirar la bandera es parte de la tarea, no un extra. La funcionalidad no está "terminada" cuando se publica: está terminada cuando la bandera ha desaparecido del código. Muchos equipos crean el ticket de retirada en el mismo momento de crear la bandera.
Regla 3: la retirada es un cambio propio y pequeño.
// Antes
if (BANDERAS.panelEstadisticas) {
barra.append(construirPanelEstadisticas());
}
// Después: se elimina la condición y la entrada en banderas.js
barra.append(construirPanelEstadisticas());git switch -c limpieza/retirar-bandera-panel-estadisticas
git commit -am "Retirar la bandera panelEstadisticas
La funcionalidad lleva tres semanas activa al 100 % sin incidencias.
Cierra GT-352."Regla 4: audita periódicamente. Un comando sencillo, ejecutable desde el CI, que liste las banderas y su antigüedad:
# Banderas declaradas y cuándo se tocó cada línea por última vez
git blame --date=short -- configuracion/banderas.js | grep -E '^\S+.*: (true|false)'Regla 5: pon un techo. Algunos equipos limitan el número de banderas temporales simultáneas (por ejemplo, cinco). Para crear la sexta, hay que retirar una. Es artificial, y funciona.
El intercambio, dicho sin adornos: las banderas cambian complejidad de ramificación (que Git gestiona mal en ramas largas) por complejidad de código (que se gestiona con disciplina y borrado). Es un buen negocio solo si el borrado ocurre de verdad. Un equipo que añade banderas y no las retira acaba en un sitio peor que si hubiera usado ramas largas.
- Branch by abstraction: cambios grandes sin ramas largas
Las banderas resuelven bien "añadir algo nuevo, oculto". No resuelven bien sustituir algo existente por otra cosa: reemplazar el almacenamiento en localStorage por una API en el servidor, migrar componentes-ui a una versión incompatible, reescribir el motor de renderizado.
Para eso existe branch by abstraction ("ramificar por abstracción"), una técnica que consigue el mismo efecto sin ramas y sin banderas dispersas por todo el código. Son cinco pasos, y cada uno se integra en el tronco por separado.
Paso 1: introducir una abstracción entre el código que llama y la implementación actual.
// almacenamiento/index.js — nueva capa, sin cambiar comportamiento
import * as local from './almacenamiento-local.js';
export const guardarTareas = local.guardarTareas;
export const cargarTareas = local.cargarTareas;Todo el código pasa a usar almacenamiento/index.js. No cambia nada funcionalmente, así que es una integración segura y revisable en diez minutos.
Paso 2: construir la nueva implementación detrás de la misma interfaz. Se integra a diario, sin que nadie la use todavía.
// almacenamiento/almacenamiento-api.js
export async function guardarTareas(tareas) { /* llamada al servidor */ }
export async function cargarTareas() { /* llamada al servidor */ }Paso 3: elegir implementación con una bandera. Una sola, en un solo sitio.
// almacenamiento/index.js
import { BANDERAS } from '../configuracion/banderas.js';
import * as local from './almacenamiento-local.js';
import * as api from './almacenamiento-api.js';
const impl = BANDERAS.almacenamientoEnServidor ? api : local;
export const guardarTareas = impl.guardarTareas;
export const cargarTareas = impl.cargarTareas;Paso 4: migrar progresivamente. Activar para el equipo, luego para el 5%, luego para todos, vigilando.
Paso 5: retirar lo viejo. Borrar almacenamiento-local.js, borrar la bandera y —si ya no aporta— aplanar la abstracción.
La ventaja frente a una rama de dos meses es contundente: en ningún momento existe una divergencia grande. Cada paso es un cambio pequeño, revisable, integrado y desplegado. Y si a mitad de camino hay que parar y atender otra prioridad, el trabajo hecho ya está en el tronco y no se pierde ni se pudre.
- Ramas de versión solo cuando hacen falta
TBD no prohíbe las ramas de versión: dice que no deben existir por defecto. Se crean cuando hay una razón concreta, y siempre desde el tronco.
# Solo si hace falta congelar una versión concreta
git switch -c release/3.2 main
git tag -a v3.2.0 -m "Version 3.2.0"
git push -u origin release/3.2 --follow-tagsY hay una regla direccional que define el modelo:
Las correcciones se hacen primero en el tronco y después se llevan a la rama de versión, nunca al revés.
# 1. Arreglar en el tronco (donde se arregla TODO, siempre)
git switch main && git pull
git switch -c correccion/fecha-vencimiento
git commit -am "Corregir la zona horaria en la fecha de vencimiento"
# PR, revisión, integrar en main
# 2. Llevar el arreglo a la versión afectada
git switch release/3.2
git cherry-pick <hash-del-arreglo-en-main>
git tag -a v3.2.1 -m "Version 3.2.1"
git push origin release/3.2 --follow-tagsEsta técnica se llama cherry-pick hacia atrás (backporting), y usa exactamente el comando de la lección 05-03. Compárala con el hotfix/* de Git Flow, que arreglaba primero en main (producción) y luego propagaba a develop:
| Git Flow | TBD | |
|---|---|---|
| Dónde se arregla primero | En la rama de producción (hotfix/ desde main) |
Siempre en el tronco |
| Cómo llega al otro sitio | Fusión de vuelta a develop |
cherry-pick a la rama de versión |
| Riesgo de olvido | El arreglo no llega a desarrollo: el fallo vuelve | El arreglo no llega a la versión antigua: el cliente sigue con el fallo |
El riesgo de TBD es más benigno. Si te olvidas del cherry-pick, el cliente antiguo sigue con el fallo —malo, pero visible y reclamable—. Si en Git Flow te olvidas de la segunda fusión, el fallo reaparece en la siguiente versión, que es peor y mucho más confuso.
Las ramas de versión en TBD deben además vivir poco: se crean, reciben sus parches, y cuando la versión deja de mantenerse se abandonan. No son ramas permanentes.
- Qué exige TBD
Como GitHub Flow, TBD compra simplicidad estructural pagando en disciplina y automatización. Aquí la factura es aún más alta.
Exigencia 1: batería de pruebas rápida y fiable
Rápida es tan importante como fiable. Si el CI tarda cuarenta minutos, nadie integra tres veces al día: la gente acumula trabajo para amortizar la espera, y ya no estás haciendo TBD. La referencia práctica es menos de diez minutos para la batería que bloquea la integración. Cómo conseguirlo —caché, paralelismo, ejecutar solo lo afectado— es tema de la lección 07-06.
Fiable significa cero pruebas inestables. Con integraciones diarias de todo el equipo, una prueba que falla el 5% de las veces se convierte en varios falsos rojos al día y el equipo aprende a ignorarlos.
Exigencia 2: cultura de commits pequeños
Hay que saber descomponer el trabajo. Es una habilidad concreta y se entrena: ante "construir el panel de estadísticas", saber ver ocho pasos que se pueden integrar por separado sin romper nada. Quien solo sabe pensar en unidades de "funcionalidad completa" no puede hacer TBD.
Exigencia 3: CI en cada envío
Cada envío a cualquier rama, y cada integración en el tronco, dispara las comprobaciones. Sin excepciones.
Exigencia 4: el tronco sano es la prioridad absoluta
Igual que en GitHub Flow, pero más agudo: aquí todo el equipo trabaja contra el tronco varias veces al día. Un tronco roto no bloquea a quien despliega, bloquea a todo el mundo a la vez. La política habitual es revertir de inmediato y arreglar en una rama.
Exigencia 5: revisión rápida
Si una PR de tres horas de trabajo espera dos días a ser revisada, el modelo se rompe. Los equipos que hacen TBD adoptan compromisos explícitos (revisar en menos de una hora), revisión en pareja, o programación en pareja como sustituto.
Exigencia 6: madurez para usar bien las banderas
Crear banderas es fácil. Retirarlas exige disciplina sostenida. Un equipo que no la tenga acumulará deuda de banderas hasta que el código sea inmanejable.
- Comparativa final de los tres flujos
Ya tenemos los tres modelos completos. Esta es la tabla que resume el módulo.
| Criterio | Git Flow | GitHub Flow | Trunk Based |
|---|---|---|---|
| Ramas de larga vida | 2 (main, develop) + soporte |
1 (main) |
1 (tronco) |
| Clases de rama de apoyo | 3 (feature, release, hotfix) |
1 (rama de trabajo) | 1 (rama efímera) u ninguna |
| Frecuencia de integración | Baja: al terminar la funcionalidad (semanas) | Media: días | Alta: al menos una vez al día |
| Vida típica de una rama | 1–4 semanas | 2–5 días | Horas |
| Complejidad del proceso | Alta: 5 clases, doble integración | Baja | Muy baja en ramas, alta en técnica |
| Tipo de entrega | Planificada, por versiones | Continua, tras cada PR | Continua, varias al día |
| Tamaño de equipo | Mediano y grande, con roles | Pequeño y mediano | Pequeño a muy grande (con inversión) |
| Mantener versiones antiguas | Excelente: es su razón de ser | Malo: requiere añadir ramas de soporte | Aceptable: ramas de versión + backport |
| Coste de los conflictos | Alto: divergencia larga | Medio | Muy bajo: casi no hay divergencia |
| Exigencia de pruebas automáticas | Media (hay QA manual en la rama de versión) | Alta | Muy alta |
| Tiempo de idea a producción | Semanas o meses | Días | Horas |
| Curva de aprendizaje | Alta (mucho protocolo) | Baja | Media (el protocolo es simple; las técnicas no) |
| Riesgo por despliegue | Alto: muchos cambios juntos | Bajo: un cambio por despliegue | Muy bajo: cambios diminutos |
| Trabajo grande sin terminar | En su rama, aislado | Problema no resuelto | Banderas y branch by abstraction |
| Reversión de emergencia | Nuevo hotfix + versión de parche |
git revert + despliegue |
Apagar una bandera: inmediato |
Dos lecturas que conviene extraer de la tabla:
Primera: la complejidad no desaparece, se mueve de sitio. Git Flow la pone en la estructura de ramas (cinco clases, reglas de origen y destino, dobles integraciones). TBD la pone en el código y la automatización (banderas, abstracciones, pruebas rápidas, disciplina). GitHub Flow queda en medio. Ningún modelo es "más simple" en términos absolutos: eligen dónde pagar.
Segunda: hay una relación directa entre madurez técnica y modelo viable. Cuanto menos fiable es tu automatización, más estructura de ramas necesitas como red de seguridad. Un equipo sin pruebas automáticas que adopta TBD no está siendo moderno: está desplegando código sin comprobar. La progresión natural es Git Flow → GitHub Flow → TBD a medida que mejora la automatización, no al revés.
- Árbol de decisión: cuál elegir
flowchart TD
A["¿Mantienes varias versiones<br/>en producción a la vez?"]
A -->|Sí| B["¿Entregas planificadas<br/>con fase de QA manual?"]
A -->|No| C["¿Tienes pruebas automáticas<br/>fiables y CI?"]
B -->|Sí| D["**Git Flow**<br/>Producto instalable, móvil,<br/>bibliotecas, empotrados"]
B -->|No| E["**GitHub Flow + ramas<br/>de soporte**<br/>desde las etiquetas"]
C -->|No| F["**GitHub Flow**<br/>y prioriza construir<br/>la batería de pruebas"]
C -->|Sí| G["¿El CI tarda menos<br/>de 10 minutos?"]
G -->|No| H["**GitHub Flow**<br/>y trabaja en acelerar el CI<br/>antes de dar el paso"]
G -->|Sí| I["¿El equipo sabe descomponer<br/>y retirar banderas?"]
I -->|No| J["**GitHub Flow**<br/>con ramas cada vez más cortas<br/>como transición"]
I -->|Sí| K["**Trunk Based**<br/>Web, SaaS, despliegue<br/>continuo, equipos maduros"]
Y cuatro criterios prácticos que valen más que cualquier diagrama:
1. Elige el modelo más simple que resuelva tu problema real. No adoptes ramas de versión "por si algún día hace falta". Añade la pieza el día que la necesites, como hizo el equipo de gestor-tareas con soporte/1.4.
2. Los modelos se mezclan. Casi ningún equipo aplica uno de estos tres en su forma canónica. GitHub Flow con una rama de soporte, TBD con ramas de versión trimestrales, Git Flow sin develop. Lo que importa es que el equipo entero sepa cuál es el acuerdo, no que tenga nombre propio.
3. La restricción real casi nunca es Git. Es cuánto tardan las pruebas, cuánto se tarda en revisar y cuánto cuesta desplegar. Si quieres integrar más a menudo y no puedes, el cuello de botella está en uno de esos tres sitios, no en la política de ramas.
4. Escríbelo. El acuerdo debe estar en el CONTRIBUTING.md del repositorio: qué ramas existen, de dónde nacen, cómo se integran, qué método de integración se usa y qué hay que cumplir para integrar. Un modelo que solo vive en la cabeza de tres personas no sobrevive a la cuarta contratación.
Errores Comunes y Consejos
Error 1: adoptar TBD sin pruebas automáticas. No es TBD, es enviar código sin comprobar directamente a producción. Construye primero la red.
Error 2: llamar TBD a ramas de una semana. Si la rama vive días, es GitHub Flow. Está bien, pero no es esto, y no obtendrás la reducción de conflictos.
Error 3: crear banderas y no retirarlas nunca. La deuda de banderas hace el código inmanejable y acaba siendo peor que las ramas largas que evitaba.
Error 4: banderas anidadas. if (A && !B) ... else if (B && C) ... es imposible de probar y de razonar. Una bandera, un punto de decisión.
Error 5: usar banderas para sustituir una implementación por otra en veinte sitios. Para eso está branch by abstraction: una sola bandera en un solo punto.
Error 6: no cambiar la forma de descomponer el trabajo. TBD con la mentalidad de "termino la funcionalidad y luego integro" no funciona. Hay que aprender a partir en pasos integrables.
Error 7: convivir con un CI lento. Es la causa más común del fracaso de TBD. Si el CI tarda cuarenta minutos, el modelo es inviable por muchas ganas que se le pongan.
Error 8: arreglar directamente en la rama de versión. El arreglo se pierde para el tronco y reaparece en la versión siguiente. Se arregla en el tronco y se lleva con cherry-pick.
Error 9: elegir modelo por moda. TBD porque lo usa Google, cuando tu equipo son dos personas sin pruebas automáticas. Google también tiene mil ingenieros dedicados a la infraestructura que lo hace posible.
Consejo 1: mide el tiempo de vida de tus ramas. git for-each-ref --sort=committerdate --format='%(committerdate:short) %(refname:short)' refs/remotes/origin/ te da la foto en una línea. Si la media es de dos semanas, ya sabes por dónde empezar.
Consejo 2: documenta cada bandera con ticket, autor y fecha de caducidad. En el propio fichero, junto a la declaración.
Consejo 3: crea el ticket de retirada al crear la bandera. No al publicar: al crearla.
Consejo 4: transición gradual. Pasar de ramas de dos semanas a integración diaria de golpe no funciona. Reduce a una semana, luego a tres días, luego a uno, arreglando en cada paso lo que se rompa.
Consejo 5: git pull --rebase como costumbre. Con integraciones frecuentes evita decenas de commits de fusión inútiles. Configúralo con git config --global pull.rebase true (lección 01-06).
Consejo 6: escribe el acuerdo en CONTRIBUTING.md. Con ejemplos de comandos concretos, no solo con la teoría.
Ejercicios
Ejercicio 1: el coste real de la divergencia
Demuestra empíricamente por qué integrar a menudo reduce los conflictos.
- Crea un repositorio con
app.jsde veinte líneas numeradas. - Escenario A (divergencia grande): crea dos ramas y haz seis commits en cada una, tocando líneas cercanas del mismo fichero. Fusiónalas y cuenta los conflictos.
- Escenario B (integración frecuente): parte del mismo estado inicial en una copia. Alterna: un commit en la rama 1, integrar en
main; un commit en la rama 2 rebasada sobremain, integrar; y así hasta seis por cada una. - Compara el número de conflictos y de líneas en conflicto entre los dos escenarios.
- Provoca un conflicto semántico: en una rama renombra una función y en la otra añade una llamada con el nombre antiguo. Comprueba que Git fusiona sin quejarse y que el resultado está roto.
Ejercicio 2: banderas de funcionalidad
- Crea
configuracion/banderas.jscon dos banderas documentadas (ticket, autor, fecha de caducidad). - Implementa en
app.jsuna funcionalidad nueva oculta tras una bandera apagada, en tres commits integrados enmainuno a uno (esqueleto, lógica, presentación). Comprueba que en cada paso la aplicación sigue siendo funcional con la bandera apagada. - "Publica" la funcionalidad con un commit que solo cambie
falseportrue. - Simula una incidencia: revierte la publicación con un commit que vuelva a poner
false, y compáralo con lo que habría costado revertir el código entero. - Retira la bandera: elimina la condición y la entrada del fichero, en un commit propio con un mensaje que explique por qué.
- Escribe un comando que liste las banderas del fichero junto a la fecha del último cambio de cada línea.
Ejercicio 3: branch by abstraction
Migra el almacenamiento de gestor-tareas de localStorage a una API simulada, sin ramas largas. Cada paso debe ser un commit en main que deje la aplicación funcionando.
- Estado inicial:
app.jsllama directamente alocalStorage. - Paso 1: crea
almacenamiento/index.jsque reexporte la implementación actual, y haz queapp.jsla use. Sin cambio de comportamiento. - Paso 2: añade
almacenamiento/almacenamiento-api.jscon la nueva implementación (puede ser simulada). - Paso 3: selecciona la implementación con una bandera en un único punto.
- Paso 4: activa la bandera.
- Paso 5: elimina la implementación antigua y la bandera.
- Muestra
git log --oneliney comprueba que son seis pasos pequeños e independientes, cada uno desplegable.
Soluciones
Solución 1:
mkdir /tmp/tbd-conflictos && cd /tmp/tbd-conflictos
git init -qb main
seq 1 20 | sed 's/^/const linea/;s/$/ = 0;/' > app.js
git add . && git commit -q -m "Estado inicial"
cd /tmp && cp -r tbd-conflictos tbd-frecuente && cd /tmp/tbd-conflictos# Escenario A: divergencia grande
git switch -qc rama-ana
for i in 1 2 3 4 5 6; do
sed -i "${i}s/= 0;/= ${i}00; \/\/ ana/" app.js
git commit -qam "Ana cambio $i"
done
git switch -q main
git switch -qc rama-bruno
for i in 1 2 3 4 5 6; do
sed -i "${i}s/= 0;/= ${i}99; \/\/ bruno/" app.js
git commit -qam "Bruno cambio $i"
done
git switch -q main
git merge -q rama-ana
git merge rama-brunoAuto-merging app.js CONFLICT (content): Merge conflict in app.js Automatic merge failed; fix conflicts and then commit the result.
# Escenario B: integración frecuente
cd /tmp/tbd-frecuente
for i in 1 2 3 4 5 6; do
git switch -q main
git switch -qc ana-$i
sed -i "${i}s/= 0;/= ${i}00; \/\/ ana/" app.js
git commit -qam "Ana cambio $i"
git switch -q main && git merge -q ana-$i && git branch -qd ana-$i
git switch -qc bruno-$i
sed -i "${i}s/\/\/ ana/\/\/ ana bruno/" app.js
git commit -qam "Bruno cambio $i"
git switch -q main && git merge -q bruno-$i && git branch -qd bruno-$i
done
echo "Integraciones completadas sin conflicto"En el escenario B cada rama parte del tronco actualizado y la divergencia nunca supera un commit: no hay ni un conflicto. El mismo trabajo, el mismo fichero, distinto coste.
# 5. Conflicto semántico
cd /tmp/tbd-conflictos
git switch -q main
printf 'function guardarTareas(t) { return t; }\nguardarTareas([]);\n' > tareas.js
git add . && git commit -q -m "Anadir tareas.js"
git switch -qc renombrado
sed -i 's/function guardarTareas/function persistirTareas/;s/^guardarTareas(\[\]);/persistirTareas([]);/' tareas.js
git commit -qam "Renombrar guardarTareas a persistirTareas"
git switch -q main
git switch -qc nueva-llamada
echo 'guardarTareas([{id: 1}]);' >> tareas.js
git commit -qam "Anadir una llamada mas"
git switch -q main
git merge -q renombrado
git merge nueva-llamada
cat tareas.jsGit fusiona sin conflicto porque las líneas son distintas, y el resultado llama a una función que ya no existe. Solo lo detectan las pruebas, y solo si existen.
Solución 2:
mkdir /tmp/tbd-banderas && cd /tmp/tbd-banderas
git init -qb main
mkdir configuracion
cat > configuracion/banderas.js <<'EOF'
// Banderas de funcionalidad de gestor-tareas.
export const BANDERAS = {
// GT-352 — Ana Ferrer — creada 2026-08-03 — retirar antes del 2026-09-01
panelEstadisticas: false,
// GT-338 — Bruno Salas — creada 2026-07-20 — retirar antes del 2026-08-15
exportacionCSV: true,
};
EOF
printf "import { BANDERAS } from './configuracion/banderas.js';\n\nfunction renderizarBarraLateral() {\n const barra = document.querySelector('#barra-lateral');\n barra.innerHTML = '';\n}\n" > app.js
git add . && git commit -q -m "Anadir el fichero de banderas de funcionalidad"# 2. Tres pasos, tres integraciones, siempre desplegable
cat >> app.js <<'EOF'
function construirPanelEstadisticas() {
const panel = document.createElement('section');
panel.className = 'panel-estadisticas';
return panel;
}
EOF
git commit -qam "Anadir el esqueleto del panel de estadisticas (GT-352)"
sed -i "s| panel.className = 'panel-estadisticas';| panel.className = 'panel-estadisticas';\n panel.dataset.total = String(calcularTotales().total);|" app.js
cat >> app.js <<'EOF'
function calcularTotales() {
return { total: 0, completadas: 0 };
}
EOF
git commit -qam "Calcular los totales del panel de estadisticas (GT-352)"
sed -i "s| barra.innerHTML = '';| barra.innerHTML = '';\n if (BANDERAS.panelEstadisticas) {\n barra.append(construirPanelEstadisticas());\n }|" app.js
git commit -qam "Mostrar el panel de estadisticas tras su bandera (GT-352)"# 3. Publicar: una línea
sed -i 's/panelEstadisticas: false,/panelEstadisticas: true,/' configuracion/banderas.js
git commit -qam "Activar el panel de estadisticas para todos los usuarios
Cierra GT-352 (publicacion)."# 4. Reversión de emergencia: otra línea
sed -i 's/panelEstadisticas: true,/panelEstadisticas: false,/' configuracion/banderas.js
git commit -qam "Desactivar el panel de estadisticas: error de calculo en los totales
Incidencia GT-360."Revertir el código entero habría exigido git revert de tres commits, resolver los conflictos con lo integrado después, construir y desplegar. Apagar la bandera es un cambio de una línea que no toca la lógica.
# 5. Retirada (tras reactivar y estabilizar)
sed -i 's/panelEstadisticas: false,/panelEstadisticas: true,/' configuracion/banderas.js
git commit -qam "Reactivar el panel de estadisticas tras corregir los totales"
python3 - <<'EOF'
import re
s = open('app.js').read()
s = s.replace(""" if (BANDERAS.panelEstadisticas) {
barra.append(construirPanelEstadisticas());
}""", " barra.append(construirPanelEstadisticas());")
open('app.js','w').write(s)
EOF
sed -i '/GT-352/,+1d' configuracion/banderas.js
git commit -qam "Retirar la bandera panelEstadisticas
Lleva tres semanas activa al 100 % sin incidencias. La condicion ya no
aporta nada y elimina un camino de codigo que probar. Cierra GT-352."# 6. Auditoría de banderas
git blame --date=short -- configuracion/banderas.js | grep -E ': (true|false),'Solución 3:
mkdir /tmp/tbd-abstraccion && cd /tmp/tbd-abstraccion
git init -qb main
mkdir -p almacenamiento configuracion
printf "function guardar(t) { localStorage.setItem('tareas', JSON.stringify(t)); }\nfunction cargar() { return JSON.parse(localStorage.getItem('tareas') || '[]'); }\n" > app.js
echo "export const BANDERAS = {};" > configuracion/banderas.js
git add . && git commit -q -m "Estado inicial: almacenamiento en localStorage"# Paso 1: la abstracción, sin cambiar comportamiento
cat > almacenamiento/almacenamiento-local.js <<'EOF'
export function guardarTareas(t) { localStorage.setItem('tareas', JSON.stringify(t)); }
export function cargarTareas() { return JSON.parse(localStorage.getItem('tareas') || '[]'); }
EOF
cat > almacenamiento/index.js <<'EOF'
import * as local from './almacenamiento-local.js';
export const guardarTareas = local.guardarTareas;
export const cargarTareas = local.cargarTareas;
EOF
printf "import { guardarTareas, cargarTareas } from './almacenamiento/index.js';\n\nexport { guardarTareas, cargarTareas };\n" > app.js
git add . && git commit -q -m "Introducir la capa de abstraccion de almacenamiento
Sin cambio de comportamiento: index.js reexporta la implementacion
actual basada en localStorage. Prepara la migracion a servidor."# Paso 2: la nueva implementación, sin usarla
cat > almacenamiento/almacenamiento-api.js <<'EOF'
export async function guardarTareas(t) {
await fetch('/api/tareas', { method: 'PUT', body: JSON.stringify(t) });
}
export async function cargarTareas() {
const r = await fetch('/api/tareas');
return r.ok ? r.json() : [];
}
EOF
git add . && git commit -q -m "Anadir la implementacion de almacenamiento contra la API
Todavia no la usa nadie: index.js sigue apuntando a localStorage."# Paso 3: la bandera, en un único punto
cat > configuracion/banderas.js <<'EOF'
export const BANDERAS = {
// GT-377 — Ana Ferrer — creada 2026-08-05 — retirar tras la migracion
almacenamientoEnServidor: false,
};
EOF
cat > almacenamiento/index.js <<'EOF'
import { BANDERAS } from '../configuracion/banderas.js';
import * as local from './almacenamiento-local.js';
import * as api from './almacenamiento-api.js';
const impl = BANDERAS.almacenamientoEnServidor ? api : local;
export const guardarTareas = impl.guardarTareas;
export const cargarTareas = impl.cargarTareas;
EOF
git add . && git commit -q -m "Seleccionar la implementacion de almacenamiento con una bandera (GT-377)"# Paso 4: activar
sed -i 's/almacenamientoEnServidor: false,/almacenamientoEnServidor: true,/' configuracion/banderas.js
git commit -qam "Activar el almacenamiento en servidor para todos los usuarios (GT-377)"
# Paso 5: retirar lo viejo
git rm -q almacenamiento/almacenamiento-local.js
cat > almacenamiento/index.js <<'EOF'
export { guardarTareas, cargarTareas } from './almacenamiento-api.js';
EOF
echo "export const BANDERAS = {};" > configuracion/banderas.js
git add . && git commit -q -m "Retirar el almacenamiento en localStorage y su bandera
La migracion lleva dos semanas activa al 100 %. Cierra GT-377."f2a9c1e Retirar el almacenamiento en localStorage y su bandera 8d3b7f4 Activar el almacenamiento en servidor para todos los usuarios (GT-377) 1c6e0a9 Seleccionar la implementacion de almacenamiento con una bandera (GT-377) 5b9d2f7 Anadir la implementacion de almacenamiento contra la API 3e7a4c8 Introducir la capa de abstraccion de almacenamiento 9f1c5b2 Estado inicial: almacenamiento en localStorage
Una migración completa de arquitectura, sin ninguna rama que viviera más de unas horas, y con cada paso desplegable de forma independiente.
Conclusión
Trunk Based Development es el extremo de la integración frecuente, y su valor está en entender por qué esa frecuencia importa. Lo esencial:
- La regla: todo el mundo integra en el tronco al menos una vez al día. Ramas de horas, o commit directo en equipos muy pequeños.
- El objetivo real no es tener pocas ramas: es reducir el tiempo entre integraciones, porque el coste de un conflicto crece mucho más rápido que el tiempo de divergencia. Y los conflictos semánticos —los que Git no detecta— crecen igual.
- La integración continua no es un servidor de CI: es integrar de verdad, y a menudo. TBD es la política de ramas que la hace posible.
- La técnica que lo permite es separar despliegue de publicación con banderas de funcionalidad: el código viaja a producción apagado, se publica cambiando una línea y se revierte apagándola. Distingue las banderas temporales (de publicación y experimento, hay que retirarlas) de las permanentes (operativas y de permisos).
- Las banderas tienen un coste real: la deuda de banderas multiplica los caminos del código. Fecha de caducidad documentada, ticket de retirada creado al crearlas, retirada como parte de la tarea y auditoría periódica.
- Para sustituir una implementación por otra,
branch by abstraction: abstracción, nueva implementación, bandera en un único punto, migración progresiva, retirada de lo viejo. Cinco pasos, ninguna rama larga. - Las ramas de versión existen solo cuando hacen falta, y la dirección es siempre arreglar en el tronco y llevar el arreglo con
cherry-pick, nunca al revés. - Exige CI rápido (menos de diez minutos) y fiable, cultura de commits pequeños, revisión rápida, tronco sano como prioridad absoluta y madurez para gestionar banderas.
- Y la lectura de la comparativa: la complejidad no desaparece, se traslada. Git Flow la pone en la estructura de ramas; TBD, en el código y la automatización. Cuanto menos fiable sea tu automatización, más estructura de ramas necesitas como red de seguridad.
Los tres modelos, con todas sus diferencias, dependen de lo mismo: comprobaciones automáticas en las que se pueda confiar. Git Flow las necesita menos porque tiene QA manual; GitHub Flow las exige para que main sea desplegable; TBD las exige rápidas y perfectas porque el tronco recibe cambios cada hora. Sin ellas, ningún flujo funciona.
Toca por fin construir esa pieza. En la lección 07-06: Integración Continua con Git veremos qué es realmente la integración continua, cómo se engancha a Git mediante disparadores, qué commit se comprueba exactamente en una pull request —la respuesta sorprende—, cómo las comprobaciones de estado y las ramas protegidas se convierten en un requisito que nadie puede saltarse con un --no-verify, y retomaremos por fin los hooks de servidor que la lección 06-01 dejó pendientes.
Dominando Git: De Principiante a Avanzado
Módulo 1: Introducción a Git
- ¿Qué es Git?
- Instalando Git
- Terminología Básica de Git
- El Modelo de Datos de Git
- Configurando Git
- Configuración Inicial
Módulo 2: Operaciones Básicas de Git
- Creando un Repositorio
- Clonando un Repositorio
- Flujo de Trabajo Básico de Git
- Preparando y Confirmando Cambios
- Inspeccionando Cambios con git diff
- Visualizando el Historial de Confirmaciones
Módulo 3: Ramas y Fusión
- Entendiendo las Ramas
- Creando y Cambiando Ramas
- Fusionando Ramas
- Estrategias de Fusión
- Resolviendo Conflictos de Fusión
- Gestión de Ramas
Módulo 4: Trabajando con Repositorios Remotos
- Entendiendo los Repositorios Remotos
- Agregando un Repositorio Remoto
- Autenticación con Repositorios Remotos
- Obteniendo y Extrayendo Cambios
- Enviando Cambios
- Rastreando Ramas
Módulo 5: Operaciones Avanzadas de Git
- Rebase
- Rebase Interactivo
- Cherry-Picking de Confirmaciones
- Guardando Cambios Temporales
- Etiquetando Confirmaciones
- Revirtiendo Confirmaciones
Módulo 6: Herramientas y Técnicas de Git
- Usando Git Hooks
- Git Bisect
- Git Blame
- Git Log y Alias
- Submódulos de Git
- Múltiples Copias de Trabajo con git worktree
Módulo 7: Estrategias de Colaboración y Flujo de Trabajo
- Forks y Pull Requests
- Revisiones de Código con Git
- Flujo de Trabajo Git Flow
- GitHub Flow
- Trunk Based Development
- Integración Continua con Git
Módulo 8: Mejores Prácticas y Consejos de Git
- Escribiendo Buenos Mensajes de Confirmación
- Manteniendo un Historial Limpio
- Ignorando Archivos con .gitignore
- Atributos de Fichero con .gitattributes
- Mejores Prácticas de Seguridad
- Consejos de Rendimiento
Módulo 9: Solución de Problemas y Depuración
- Problemas Comunes de Git
- Deshaciendo Cambios
- Resolviendo Divergencias con el Remoto
- Recuperando Confirmaciones Perdidas
- Tratando con Repositorios Corruptos
- Técnicas Avanzadas de Depuración
