Trunk-Based development: pusheando a master

2
3521

Introducción

En este tutorial vamos a ver una política de ramas de un sistema de gestión de código fuente (source control management, SCM en adelante) que prima la integración continua, más bien inmediata, frente a otras en donde cada desarrollador realiza su trabajo en una rama separada para luego realizar una integración al finalizar su trabajo.

1. Qué es una política de ramas en un SCM

La mayoría de SCM, sobre todos los modernos (SVN, Git), tienen la posibilidad de crear ramas y tags para poder realizar evoluciones en paralelo del código fuente.

Así, se puede partir de una sola rama principal (master por ejemplo) e ir creando ramas a partir de un punto determinado de la principal, donde se puede trabajar de forma independiente. Al final del trabajo realizado, se unen (merge) las ramas en un punto común, normalmente de donde partieron, para integrar un solo producto.

Como imaginarás, hay diferentes formas de organizar estas ramas: si usarlas o no, cómo usarlas, cuándo se crean, cuándo se unen, cómo se llaman…

Para el SCM más popular hoy en día, GIT, existe GIT-FLOW, que establece una política de ramas bastante adaptable a cualquier entorno.

2. ¿Qué tienen de malo las ramas por feature?

Supongamos que partimos de una rama principal (development) de desarrollo y comenzamos un nuevo sprint (iteración o como quieras llamarlo). Normalmente se suele decir:

"Por cada ‘feature’ se creará una rama de desarrollo partiendo de la rama principal".

Digamos que tenemos 3 features para el sprint que va a realizar el equipo de desarrollo: ADT-12, ADT-14 y ADT-15. Nuestro grupo de desarrolladores hará algo como esto:

¿Cuál es el problema?

El problema está en la falta de integración. ¿En qué estado está el código en el momento en el que se cruza la línea de puntos? Efectivamente, hasta que no se haga la integración no funcionará.

Bien, tenemos una rama de development que ha permanecido invariable desde el comienzo del Sprint y otras ramas que tienen su desarrollo particular y el resto asumen que parten del punto inicial, por lo que asumen una situación idílica: "los desarrollos de mis compañeros no me afectan".

Es cierto que… "si una historia está perfectamente dividida y la arquitectura del sistema es la adecuada, cada historia podría desarrollarse de forma independiente". Esto es mucho suponer… muchísimo diría yo.

La realidad es que suele haber conflictos en los merges, o en el peor de los casos, invalidar el desarrollo de una historia de usuario porque otra le afecta gravemente. Sí, a veces pasa… si pasa con 4 desarrolladores es para "ponerles firmes", pero hay equipos de desarrollo de 4, 40 o 400 integrantes… las cosas no son fáciles.

¿Qué consecuencias tiene las feature branches?

Una muy positiva para el ritmo del proyecto (aunque no por la calidad):

"¡El primero que comitee a master gana!" Mejor dicho: hace que los demás se "coman" su merge.

¿No os ha pasado nunca que cuando estás a punto de acabar tu feature un compañero dice: "pusheo a develop y ahí te comes el merge, jajaja"?

Cuando acabas tu feature con tu sólida base de precondiciones heredada de development, te das cuenta de que lo que has hecho no vale, porque quizá la base haya cambiado: te queda un tiempo de "merge" sobre un código con modificaciones que desconoces, lo que aumenta el riesgo de equivocarte… tienes que arreglar y adaptar clases, tests… ver que todo vuelve a compilar…

3. Trabajar en la rama principal directamente

La mejor forma de evitar los problemas de las features branches es eliminándolas. Sí, no usándolas. Y no, no me he vuelto loco.

En proyectos conocidos como Google Chrome o Facebook siguen esta política: son tantos y van tan rápido que no tienen tiempo para "mergeos de integraciones".

Imagino que si nunca habías oído hablar de este modo de trabajo y estás habituado a sistemas como GitFlow, se te estará poniendo un nudo en la garganta.

No te preocupes, esto funciona (en mi experiencia) y trae bastantes consecuencias creo yo positivas. Te expongo algunas:

El estado del producto es conocido: no hay fase de integración

La fase de "merges" se reduce a pequeños merges cuando los desarrolladores pasan de su ordenador al repositorio remoto al hacer un push. Si hay conflicto éste se resuelve al instante: tiene que ser pequeño y es "fresco", es decir, se ha cambiado hace pocas horas y los afectados lo recordarán.

El mejor CI son tus compañeros

Ya sabemos que ponemos la build en Jenkins cuando se hace push o por las noches, pero normalmente lo configuramos sobre la rama principal (master, development…) no sobre las ramas de development. Vamos, que la "integración continua" no es real.

Cuando los commits van sobre la rama principal Jenkins ya tiene sentido. Y si tienes un filtro en Gmail para ignorar los correos de Jenkins avisando de que la build se ha roto, no te preocupes, no hay nada que filtre las voces de tus compañeros por haber roto la rama principal… sí la rama sobre la que ahora también trabajan ellos. ¿No es maravilloso?

Política del terror

Lo primero que te habrá sugerido el cerebro cuando has leído este post es… ¡pero me pasaré el día rompiendo la build! Claro, como estás en tu rama única trabajando tú solo, no pasas los test para ir más rápido ¿verdad?

Pues ahora si quieres librarte del escarnio público y que no te llamen el "rompebuilds" no te queda otra que asegurar los arneses y pasar todos los tests (sí, los de integración también) antes de "liarla parda" y hacer que el resto del proyecto esté parado una hora por tu culpa.

Pasito a pasito

Corolario del anterior: además de pasar los test irás pasito a pasito. Pasos pequeños te permitirán hacer rollback rápidamente si la lías. Seguro que te gustará tener identificados commit a commit todos los cambios. Se acabó hacer un commit cada dos días. Ahora cada vez que hagas un pequeño avance de funcionalidad harás un commit bien explicado y documentado, suficientemente aislado como para hacer un revert rápidamente.

¿No es así como se supone que se debería hacer? Con este método no sólo se sugiere, sino que es pura supervivencia.

Transparencia

Todos compartimos el mismo "codeBase"; todo está al aire libre; todo es público. Vamos, que ya no puedes hacer esas chapucillas que hacías temporalmente en tu rama oculto a la vista de todos y que luego aplastabas al hacer el merge a la principal. Se acabaron los comentarios temporales, esos refactor pendientes y esas deudas inasumibles ocultas. Todo con luz y taquígrafos.

Programando "Mejor"

¿La política de ramas afecta al código? Depende de lo listo que seas… como programar es una actividad abstracta seguro que lo eres, y te puedes adaptar al nuevo ritmo.

Tu SCM (GIT, SVN) no sabe de programar, si es Java, si es PHP, Python… no entiende lo que subes. Sólo sabe de dos cosas:

  1. Ficheros (de texto en nuestro caso)
  2. De diferencia de líneas.

Esas son las unidades de tu SCM. De modo que si 2 personas tocan el mismo fichero, habrá un merge que habrá que solucionar de una forma dolorosa. ¿Cuál es la mejor forma de evitar los merges?

La mejor forma pasa por separar ficheros. Sí, separar ficheros. Yo soy javero así que casi siempre se cumple "clase = fichero". Esto provoca:

  1. Clases más pequeñas: cuando más pequeñas y desacopladas, menos posibilidades de que 2 personas tenga que editarla. Imagina la típica clase "no-SOLID" que hace de todo (godclass) y tiene 3.000 líneas de código. Con este sistema no es que esté mal desde el punto de vista de código, es que seguramente estén tocando 3 o 4 persona a la vez provocando unos merges enormes y arriesgados.
  2. Diseño Open-Close en su máximo apogeo: Si no quiero hacer un merge lo que tengo que hacer es no tocar los ficheros que existen. Puedo extender esas clases (sin tocar la padre) con clases nuevas, que serán ficheros nuevos y no afectarán a nadie.
  3. Diseño desacoplado: mi técnica favorita es ir añadiendo clases y clases que forman como "islas" y que contienen la funcionalidad de la feature. Son clases completamente nuevas y que nadie usa salvo mis tests, hasta que llega un momento que las tengo que integrar. Mi isla de clases ha crecido y tiene sentido por sí misma pero aún no es usada por el resto de sistema: sólo queda hacer un puente. Este puente consiste en modificar algún fichero existente (clase o configuración) para "conectar" mi isla de clases (funcionalidad). El 95% del trabajo ya está hecho y probado (la isla) y el resto es simplemente un 5% de conexión que se suele hacer en 1 commmit o 2, fácilmente reversibles. Si no hay puente, mi funcionalidad está en el código, incluso en producción, pero no la usa nadie.

Todo va en la release

En la release que se pone en producción seguramente haya funcionalidad a medias o funcionalidad "latente" que no usa nadie, bien porque no se ha conectado al sistema general o no se ha informado a los clientes de los nuevos endpoints.

Aunque el código esté, si no lo usa nadie es como si no estuviera.

Obviamente esto requiere que los desarrolladores y el equipo de QA tengan en cuenta cómo afecta cada feature a lo existente y trabajen para desarrollar y testear de modo que aunque suba el código en el empaquetado, de cara a los clientes no haya cambios.

Feature toggles o Switchs

Claro que hay veces que la nueva funcionalidad afecta a una existente en producción y no somos capaces de encontrar la forma en la que no afecte. Para eso están los puentes levadizos (por continuar con el símil de conexión de islas de código a través de puentes).

Básicamente se trata de poner un IF (on/off) en el código, de modo que la conexión entre la nueva funcionalidad y la antigua se controle por ese IF: si el IF está activo usará la nueva funcionalidad, si no, usará la vieja. Podemos poner un mecanismo de control, como por ejemplo una tabla de base de datos. Puedes ver más en [https://martinfowler.com/articles/feature-toggles.html]

Así, cuando una funcionalidad no ha sido acabada o probada, pero afecta al código y no se quiere que el usuario final la tenga disponible todavía, se desactiva el IF (false). En la siguiente subida, cuando se haya probado bien o terminado de desarrollar, se puede experimentar activando el IF (true) y viendo como se comporta.

Claro está que una vez se ha confirmado que todo funciona correctamente deberemos eliminar esos "IF" porque su labor ya no tiene sentido. Así no dejamos el código plagado de condicionantes.

4. Conclusiones

En este tutorial hemos visto el trunk-based development que es una política de ramas del SCM enfocada a la integración contínua y la velocidad de desarrollo. Puede parecer arriesgada y requiere cierto entrenamiento por parte de los equipos, pero con el tiempo trae muchas ventajas.

Hay mucha más información en páginas como [https://trunkbaseddevelopment.com/]

2 Comentarios

  1. Hola,

    Interesante punto de vista nunca antes habia sabido de ese extremo y me gustaria saber si realmente si es realmente es usado por mucha gente ya que yo no conozco ningún caso u empresa que use esta forma.

Dejar respuesta

Please enter your comment!
Please enter your name here