El pipeline y sus puertas: qué se valida en cada pull request y qué espera al final
Un pipeline de integración continua es la única declaración escrita que la mayoría de los equipos tiene sobre qué significa que un cambio está listo. Esta lección propone un criterio económico para decidir qué comprobación pertenece a cada pull request y cuál solo tiene sentido en el camino de publicación, desarrolla el concepto de presupuesto de tiempo del ciclo de revisión, separa las puertas bloqueantes de las señales informativas y de los trabajos programados, y examina por qué la acumulación indiscriminada de verificaciones produce pipelines lentos que detectan cada vez menos.
Un pipeline de integración continua no es una lista de comandos que alguien decidió automatizar un viernes por la tarde: es una declaración explícita, y normalmente la única que existe por escrito, de qué considera el equipo que significa que un cambio está listo. Todo lo que se ejecuta en cada pull request es una afirmación sobre el riesgo que el equipo no está dispuesto a asumir. Todo lo que se deja fuera es una afirmación simétrica, y mucho menos consciente, sobre el riesgo que sí acepta a cambio de velocidad. La mayoría de los equipos de Android nunca escriben esa segunda lista, y por eso terminan con pipelines que tardan cuarenta minutos en decir que sí, que nadie mira cuando fallan por motivos ajenos al cambio, y que siguen sin detectar la clase de defecto que de verdad acaba llegando a los usuarios.
- Clasificar cada verificación por su coste marginal y por su probabilidad real de detectar un defecto.
- Establecer un presupuesto de tiempo por pull request y defenderlo frente a la acumulación de tareas.
- Decidir qué comprobaciones pertenecen al camino de publicación y no al de revisión.
- Distinguir puertas bloqueantes, señales informativas y trabajos programados fuera del camino crítico.
El criterio económico: coste marginal contra probabilidad de detección
Cada tarea que se añade a un pipeline tiene dos números asociados que casi nunca se calculan. El primero es su coste marginal: los minutos que suma al camino crítico multiplicados por la frecuencia con la que se ejecuta, que en un pull request activo es una vez por cada empujón de commits, no una vez por rama. El segundo es su capacidad de detección condicionada: la probabilidad de que esa tarea concreta encuentre un defecto que ninguna de las tareas anteriores habría encontrado. Es una probabilidad condicionada, y ahí está la trampa, porque la mayoría de las suites de pruebas se solapan masivamente entre sí y la enésima comprobación aporta una fracción minúscula de detección nueva a cambio de un coste plenamente lineal.
La consecuencia inmediata es que la pregunta útil ante cualquier tarea del pipeline no es si aporta valor, porque toda tarea aporta algo, sino si aporta más valor que los minutos que cuesta multiplicados por todas las veces que se ejecutará este año.
Con esos dos números el orden de las verificaciones deja de ser una cuestión de gusto. Las tareas rápidas y de alta detección van primero y bloquean: la compilación de la variante de depuración, la comprobación de formato, las pruebas unitarias de la capa de dominio. Las tareas lentas y de alta detección se ejecutan en paralelo y bloquean, pero se les concede infraestructura para que su latencia no domine. Las tareas lentas y de baja detección no pertenecen al pull request en absoluto, y ese es el cajón que la mayoría de los equipos se resiste a abrir porque cada elemento que hay dentro fue añadido después de un incidente concreto que nadie quiere revivir.
Hay un fenómeno adicional que agrava el cálculo y que conviene nombrar: el coste de una verificación no es solo su tiempo de ejecución, sino su tasa de falsos positivos multiplicada por el tiempo humano que consume investigarlos. Una prueba de instrumentación que falla de forma intermitente en un cinco por ciento de las ejecuciones no cuesta ocho minutos: cuesta ocho minutos más la mitad de las veces en que alguien reejecuta y espera de nuevo, más la erosión gradual del hábito de mirar los resultados. Una suite con inestabilidad crónica termina enseñando al equipo a ignorar el rojo, y a partir de ese momento su capacidad de detección efectiva es cero aunque su capacidad teórica sea alta.
Rápido y detecta
Compilación de depuración, formato, análisis estático incremental y pruebas unitarias puras. Bloquean siempre.
Lento y detecta
Pruebas de instrumentación, capturas de pantalla y migraciones de base de datos. Bloquean, pero en paralelo.
Lento y no detecta
Compilaciones de todas las variantes, auditorías completas de dependencias, matrices de dispositivos exhaustivas.
Solo antes de publicar
Firma, ofuscación real, perfiles de referencia, subida de mapas de símbolos y generación del paquete.
Al evaluar una comprobación la pregunta correcta no es qué defectos puede encontrar, sino qué defectos encuentra que no habría encontrado ninguna de las anteriores. Una suite de pruebas de interfaz que cubre los mismos flujos que la suite de dominio aporta poco por mucho que su cobertura nominal sea alta, porque los fallos que detecta ya estaban detectados. La forma barata de estimarlo es histórica: revisar los últimos treinta fallos reales del pipeline y anotar, para cada uno, qué comprobación lo detectó primero. Casi siempre aparece que dos o tres tareas concentran la práctica totalidad de las detecciones y que el resto no ha encontrado nada en meses.
El presupuesto de tiempo y la degradación silenciosa
Conviene fijar un número explícito antes de discutir nada más: cuántos minutos puede tardar el pipeline de un pull request desde el empujón hasta el veredicto. Diez minutos es un objetivo defendible en un proyecto Android de tamaño medio y quince es el límite a partir del cual el comportamiento humano cambia de forma medible. Por encima de ese umbral la gente deja de esperar, cambia de tarea, pierde el contexto y vuelve media hora más tarde. El coste real de un pipeline lento no está en los minutos de máquina sino en el cambio de contexto que provoca, y ese coste es superlineal porque cada interrupción arrastra el tiempo de recuperación del hilo mental.
El presupuesto sirve además como mecanismo de gobierno. Cuando alguien propone añadir una comprobación nueva, la pregunta deja de ser si es útil, porque siempre lo es, y pasa a ser qué se saca del pull request para hacerle sitio. Sin ese mecanismo el pipeline solo crece, porque cada incidente produce una tarea nueva y ningún incidente produce jamás la retirada de una tarea vieja. Al cabo de dos años el equipo tiene un pipeline de cuarenta minutos donde la mitad de las tareas cubren riesgos que dejaron de existir cuando se reescribió el módulo que las motivó.
Vale la pena medir la degradación en lugar de intuirla. El dato relevante no es la media sino el percentil noventa y cinco del tiempo total, porque es el que gobierna la percepción, y el desglose por tarea del camino crítico, porque casi siempre revela que una sola tarea consume más que todas las demás juntas. Un pipeline honesto publica ese desglose en cada ejecución.
Hay una segunda métrica, menos habitual y más reveladora, que conviene registrar junto a la anterior: el tiempo transcurrido entre el empujón y la fusión efectiva, incluyendo la espera humana. Ese número descompone el problema en dos sumandos que se combaten con medidas distintas. Si domina la parte de máquina, la solución es técnica. Si domina la parte humana, ninguna optimización de caché la va a mover, y lo que hace falta es reducir el tamaño de los cambios o reorganizar cuándo revisa la gente. Muchos equipos invierten meses en acelerar una fase que representa el quince por ciento del retraso total.
# Puertas separadas por coste: lo rapido bloquea de inmediato
jobs:
rapido:
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
- run: ./gradlew --no-daemon spotlessCheck assembleDebug testDebugUnitTest
lento:
runs-on: ubuntu-latest
needs: rapido
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- run: ./gradlew --no-daemon lintDebug detekt
Ejecutar primero lo barato no ahorra tiempo cuando todo pasa, porque el camino crítico es el mismo. Ahorra tiempo cuando algo falla, que es entre el veinte y el cuarenta por ciento de las ejecuciones en un equipo sano. Si la comprobación de formato tarda quince segundos y la suite de instrumentación doce minutos, colocar el formato antes convierte la mitad de los fallos en un veredicto casi inmediato. El ahorro esperado es la probabilidad de fallo temprano multiplicada por el tiempo que se evita, y en la práctica supera con holgura cualquier optimización de caché.
El camino de publicación: lo que no tiene sentido validar antes
Hay una familia entera de verificaciones cuya ejecución en cada pull request es puro desperdicio porque solo cobran significado cuando existe un artefacto destinado a usuarios reales. La compilación de la variante de publicación con reducción y ofuscación activas es la más evidente: es lenta, depende de reglas que rara vez cambian y su fallo típico no es un defecto del cambio sino una regla que faltaba. Merece ejecutarse en la rama principal y antes de cada publicación, no en cada iteración de revisión.
En la misma categoría entran la generación de perfiles de referencia, que exige arrancar un dispositivo y recorrer los flujos críticos midiendo; la subida del mapa de símbolos al servicio de informes de fallos, que solo tiene sentido para un binario que alguien va a ejecutar; la comprobación de compatibilidad binaria de bibliotecas publicadas; y el análisis del tamaño del paquete final, que es informativo en un pull request pero solo es exigible cuando se compara con la última versión publicada.
Merece la pena nombrar también el caso de la verificación de dependencias, porque su ubicación correcta sorprende. Comprobar sumas de integridad y auditar licencias en cada pull request es lento y aporta poco, ya que el noventa y cinco por ciento de los cambios no toca ninguna dependencia. Pero la comprobación se vuelve imprescindible precisamente en el cinco por ciento restante, y esa condición es fácil de detectar: basta con disparar el trabajo solo cuando el cambio afecta al catálogo de versiones o a los ficheros de compilación. Es el patrón general que resuelve muchas de estas disyuntivas, y consiste en condicionar la ejecución al conjunto de rutas modificadas en lugar de decidir entre siempre y nunca.
El caso de las pruebas de instrumentación merece un párrafo propio porque es donde más equipos se equivocan en ambas direcciones. Ejecutar la suite completa en una matriz de seis dispositivos en cada pull request es un despilfarro que arruina el presupuesto de tiempo; no ejecutar ninguna es una omisión que deja fuera la clase de defecto que las pruebas unitarias jamás verán, porque vive en la interacción con el sistema operativo. El punto de equilibrio suele ser un subconjunto pequeño y estable de pruebas de humo en un único dispositivo virtual gestionado en el pull request, y la matriz completa en la rama principal por la noche.
flowchart TD
A[Push a la rama de trabajo] --> B[Formato y compilacion de depuracion]
B --> C{Pasa}
C -- No --> D[Veredicto en menos de dos minutos]
C -- Si --> E[Pruebas unitarias y analisis estatico]
E --> F[Humo de instrumentacion en un dispositivo]
F --> G[Revision humana y merge]
G --> H[Rama principal]
H --> I[Matriz completa de dispositivos]
H --> J[Build de publicacion con ofuscacion]
J --> K[Firma y subida al canal de prueba]
style D fill:#f38ba8,color:#11111b
style K fill:#a6e3a1,color:#11111bQueda una categoría difícil de ubicar y que conviene nombrar: las comprobaciones que dependen de servicios externos. Una prueba que llama a una interfaz de programación real, una descarga de configuración remota o una validación contra un entorno de pruebas compartido introducen en el veredicto una variable que tu equipo no controla. Su fallo no significa que el cambio sea incorrecto, sino que algo ajeno estaba caído, y confundir ambos mensajes es la forma más rápida de enseñar a la gente a reejecutar sin leer. Esas comprobaciones pertenecen a la categoría informativa o a un flujo aparte con su propia alerta, nunca al camino que bloquea una fusión.
Existe un indicador barato y sorprendentemente predictivo de la salud de un pipeline: la proporción de ejecuciones que alguien reintentó sin cambiar una sola línea. Un valor por debajo del dos por ciento indica que los fallos son informativos y que la gente confía en ellos. Un valor por encima del diez indica que el pipeline se ha convertido en una lotería y que su capacidad de detección efectiva se está desmoronando, porque cuando reintentar es el reflejo por defecto los fallos reales también se reintentan. Merece la pena graficarlo por semana junto al tiempo total.
Tres tipos de puerta y la disciplina de no bloquear con todo
No todas las señales deben tener el mismo poder. Una puerta bloqueante impide la fusión y debe reservarse para comprobaciones deterministas cuya violación es indiscutible: la compilación, las pruebas que no parpadean, las reglas de análisis estático que el equipo ha acordado tratar como errores. Una señal informativa se publica como comentario o como resumen y no bloquea nada: la variación del tamaño del paquete, el delta de cobertura, el inventario de dependencias nuevas. Un trabajo programado se ejecuta fuera del camino crítico, típicamente de madrugada sobre la rama principal, y su fallo abre una incidencia en lugar de detener a nadie.
Cada una de esas categorías implica además un destinatario distinto del mensaje. La puerta bloqueante habla al autor del cambio y debe explicarle qué hacer ahora. La señal informativa habla al revisor y debe darle contexto para decidir. El trabajo programado habla al equipo entero y debe abrir una incidencia con dueño, porque una alerta nocturna que llega a un canal sin responsable asignado tiene una vida media de dos semanas antes de que todos aprendan a ignorarla.
La disciplina consiste en resistir la tentación de promover todo a bloqueante. Cada puerta bloqueante nueva reparte poder de veto sobre el trabajo de los demás, y ese poder se ejerce inevitablemente en el peor momento, cuando alguien intenta desplegar una corrección urgente y descubre que una comprobación de estilo con un falso positivo se lo impide. Una regla práctica que funciona: una comprobación solo asciende a bloqueante después de haber corrido dos semanas como informativa sin producir un solo falso positivo.
Merece la pena que la configuración del repositorio refleje esa jerarquía explícitamente, y no que dependa de qué trabajos existan por accidente. En GitHub Actions esto significa nombrar los trabajos obligatorios en la protección de rama y dejar los demás fuera, no marcar todo lo que aparece en la lista.
Hay un cuarto elemento que completa el diseño y que suele llegar cuando el equipo crece: la cola de fusión. Su función es resolver un problema que las puertas por sí solas no cubren, el de dos cambios que pasan por separado y fallan juntos porque cada uno se validó contra un estado de la rama principal anterior al otro. La cola reejecuta las comprobaciones sobre la combinación real antes de integrar, y su coste es un retraso adicional que solo compensa cuando el volumen de fusiones diarias hace que ese escenario deje de ser teórico. Antes de instalarla conviene contar cuántas veces ha ocurrido de verdad.
# Inventario honesto: cuanto cuesta cada tarea del camino critico
./gradlew --no-daemon --profile assembleDebug testDebugUnitTest
# El informe queda en build/reports/profile y ordena las tareas por duracion
# Ejecutar solo lo afectado por el cambio en proyectos multimodulo
./gradlew --no-daemon testDebugUnitTest --parallel --configuration-cache
Un síntoma habitual de pipelines mal diseñados es que el conjunto de comprobaciones obligatorias no incluye la tarea que de verdad importa, porque se añadió después y nadie actualizó la protección de rama. El resultado es un repositorio donde todo aparece en verde y donde la suite crítica lleva tres semanas fallando sin que nadie lo note. Conviene auditar periódicamente la lista de comprobaciones requeridas contra la lista de trabajos que el flujo define, y tratar cualquier divergencia como un defecto de configuración con la misma seriedad que un defecto de código.
Hay una asimetría interesante entre lo que un equipo dice que valora y lo que su pipeline demuestra que valora, y esa asimetría es una de las herramientas de diagnóstico más honestas que existen. Un equipo puede afirmar en su documento de estándares que la accesibilidad es un requisito de primer orden, pero si ninguna comprobación automática la verifica, lo que el sistema comunica en cada fusión es que la accesibilidad es opcional. Puede insistir en que la cobertura importa, pero si el umbral no bloquea, lo que el sistema comunica es que la cobertura es decorativa. El pipeline funciona como el conjunto de reglas que el equipo obedece de verdad, porque es el único que se aplica sin necesidad de que nadie recuerde aplicarlo, sin depender del cansancio del revisor de turno ni de la urgencia de la semana. Esto lo convierte en un artefacto político tanto como técnico: cada puerta que se añade transfiere autoridad de las personas al sistema, y cada puerta que se retira la devuelve. Los equipos maduros lo entienden y tratan los cambios en el pipeline con más ceremonia que los cambios en el código, porque una modificación en una regla de análisis estático afecta a todos los cambios futuros mientras que un error en una función afecta a una función. La consecuencia práctica es que el diseño del pipeline no es una tarea de infraestructura que se delega al que tiene tiempo, sino un ejercicio de definición de estándares que merece la misma deliberación que una decisión de arquitectura. Y como toda especificación, envejece: las reglas que tenían sentido cuando el proyecto era un módulo monolítico se convierten en fricción cuando se reparte en veinte, y nadie las revisa porque revisar reglas no aparece en ninguna hoja de ruta. Un pipeline que nadie ha podado en dos años no describe el proyecto actual: describe los miedos acumulados de todos los incidentes anteriores.
- Mide el percentil noventa y cinco del tiempo total de tus últimas cincuenta ejecuciones y desglósalo por tarea del camino crítico.
- Para cada tarea bloqueante, busca en el historial la última vez que detectó un defecto real y no un falso positivo, y anota la fecha.
- Retira del pull request toda tarea cuya última detección real sea anterior a seis meses y muévela a un trabajo nocturno sobre la rama principal.
- Compara la lista de comprobaciones requeridas en la protección de rama con la lista de trabajos definidos y documenta cada divergencia.
- Fija por escrito un presupuesto de minutos y añade al flujo un paso que lo publique como comentario cuando se supere.