wandres.dev
CI/CD PARA IOS · automatizar el ciclo

El pipeline: qué corre en cada cambio y qué solo antes de publicar

Un pipeline no es una lista de comprobaciones sino un sistema de colas con un presupuesto de tiempo y un valor esperado por cada verificación que ejecuta. Esta lección construye el criterio para asignar cada tarea a la puerta que le corresponde, con la latencia de retroalimentación como recurso escaso, el valor esperado de cada comprobación como regla de reparto, y las tres palancas baratas que casi nadie usa antes de comprar máquinas más rápidas.

⏱ 22 min

Casi todo el mundo diseña su integración continua por acumulación: alguien añade una comprobación tras un incidente, nadie la quita nunca, y dos años después cada cambio trivial arrastra cuarenta minutos de espera que ya nadie sabe justificar. El diseño correcto va en la dirección contraria. Se parte de una pregunta económica: cuánto tiempo puede pasar entre que alguien sube un cambio y sabe si rompió algo, y cuánto vale evitar cada tipo de fallo comparado con lo que cuesta buscarlo. De esas dos magnitudes salen las puertas, y de las puertas sale el reparto. Un pipeline bien diseñado no ejecuta menos cosas: ejecuta cada cosa en el momento en que su coste es asumible y su información todavía sirve para algo.

🎯 Al terminar esta lección sabrás
  • Tratar la latencia de retroalimentación como el requisito de primer orden y derivar de ahí el contenido de cada etapa.
  • Repartir las comprobaciones entre tres puertas usando el valor esperado de cada una y no la intuición.
  • Aplicar las tres palancas que reducen tiempo sin quitar cobertura: orden, filtrado y cancelación.
  • Distinguir las comprobaciones que bloquean un cambio de las que solo informan, y sostener esa distinción con una política.

La latencia de la señal es el recurso escaso

Hay un umbral empírico bastante estable en equipos de producto: por debajo de diez minutos la gente espera el resultado mirando; entre diez y veinte, cambia de ventana pero no de tarea; por encima de veinte, se va a otra cosa y el coste de retomar el contexto se suma íntegro a cada iteración. Ese coste no es lineal. Un pipeline de treinta minutos no cuesta el triple que uno de diez: cuesta el triple más el precio de todos los cambios que se agrupan para no pagarlo, y ahí empieza el daño real, porque cuando cinco cambios viajan juntos el fallo deja de señalar a un autor concreto y la depuración se convierte en una investigación.

De ahí sale la primera inversión del razonamiento habitual. El presupuesto de tiempo no es el resultado de sumar lo que tardan las comprobaciones que uno quiere; es una restricción que se fija primero y dentro de la cual se decide qué cabe. Diez minutos en la puerta de cada cambio, quince o veinte en la de fusión, sin techo relevante en la nocturna. Cualquier comprobación que no quepa en su presupuesto no se ejecuta más despacio: se mueve a la puerta siguiente o se descarta.

💡
Mide la cola, no solo la ejecución

El número que la gente cita es la duración del trabajo, pero el que sufre el equipo es el tiempo total desde el envío hasta el resultado, cola incluida. Con agentes de macOS escasos, un trabajo de ocho minutos con doce de espera se percibe como veinte. Antes de optimizar el xcodebuild conviene mirar cuántos trabajos compiten por cuántos agentes: a veces la mejora más grande es cancelar ejecuciones obsoletas, no acelerar la compilación.

Las tres puertas y el criterio para repartir

La regla de reparto puede escribirse casi como una fórmula. Una comprobación merece estar en una puerta si el producto de su probabilidad de detectar algo por el coste que evita al detectarlo supera el coste de ejecutarla multiplicado por el número de veces que se ejecutará. Eso explica de inmediato por qué el análisis estático y las pruebas de lógica pura viven en la puerta más temprana —cuestan segundos, se ejecutan cientos de veces, y atrapan errores baratos pero constantes— y por qué la suite completa de recorridos de interfaz en cinco idiomas no puede vivir ahí: su probabilidad de detectar algo en un cambio cualquiera es baja, y su coste por ejecución es enorme.

flowchart LR
c[Cambio subido a una rama] --> p1[Puerta 1 en cada push]
p1 --> l[Formato y analisis estatico]
p1 --> b[Compilar en debug]
p1 --> u[Tests de logica sin simulador]
u --> p2[Puerta 2 al fusionar]
p2 --> i[Tests de integracion]
p2 --> s[Snapshots y accesibilidad]
p2 --> a[Archive de verificacion sin subir]
a --> p3[Puerta 3 antes de publicar]
p3 --> e[Suite completa de extremo a extremo]
p3 --> f[Firmar y subir a TestFlight]
p3 --> v[Validaciones de la tienda]

Puerta 1: cada push

Formato, análisis estático, compilación en debug y toda la lógica que no necesite simulador gráfico. Presupuesto duro de diez minutos. Bloquea siempre.

🔀

Puerta 2: al fusionar

Integración, instantáneas, un archive de verificación que demuestre que la configuración de release compila. Veinte minutos. Bloquea la fusión, no el push.

🌙

Puerta 3: nocturna y prerelease

Recorridos completos, matriz de dispositivos e idiomas, pruebas de rendimiento y la subida real. Sin techo de tiempo porque nadie espera mirando.

📊

Fuera de puerta: informativo

Cobertura, tamaño del binario, tiempos de arranque. Se publican como comentario o panel, nunca como veto, hasta que el equipo acuerde un umbral que esté dispuesto a defender.

La puerta 2 merece un comentario aparte porque casi siempre falta. El archive de verificación no busca defectos funcionales: busca los fallos que solo aparecen con optimizaciones activadas, con la configuración de release y con el conjunto de recursos completo. Es un tipo de fallo raro pero carísimo, porque cuando aparece lo hace el día de la entrega, con prisa y sin margen. Ejecutarlo al fusionar lo convierte en un problema de un ingeniero y un martes cualquiera.

# Puerta 1: rapido, determinista, sin interfaz grafica.
xcodebuild build-for-testing \
  -scheme MiApp \
  -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.0' \
  -derivedDataPath .derived | xcbeautify

xcodebuild test-without-building \
  -xctestrun .derived/Build/Products/MiApp_iphonesimulator26.0.xctestrun \
  -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.0' \
  -only-testing:MiAppTests \
  -parallel-testing-enabled YES

# Puerta 2: demostrar que la configuracion de release compila de verdad.
xcodebuild archive \
  -scheme MiApp -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath build/Verificacion.xcarchive \
  CODE_SIGNING_ALLOWED=NO

Ese CODE_SIGNING_ALLOWED=NO del final es lo que hace viable la puerta 2 en cualquier máquina: se comprueba la compilación en release sin arrastrar el problema de las identidades de firma, que es el tema de una lección posterior y que no tiene por qué contaminar una verificación de compilador.

Tres palancas antes de comprar máquinas

Cuando un pipeline va lento, el reflejo es pedir agentes más grandes. Suele haber tres mejoras más baratas y más grandes disponibles antes de eso.

La primera es el orden. Las etapas deben estar ordenadas por coste creciente y por probabilidad de fallo decreciente, de modo que lo que falla mucho y cuesta poco se ejecute primero. Un error de formato detectado en el segundo veinte ahorra los once minutos de simulador que venían detrás. Esto exige que las etapas fallen rápido y no continúen por inercia.

La segunda es el filtrado por camino. Un cambio que solo toca documentación o cadenas traducidas no necesita la suite entera. El riesgo aquí es real y conviene nombrarlo: un filtro mal escrito crea un agujero silencioso por el que pasa un cambio sin verificar. La regla práctica es filtrar solo hacia rutas claramente inertes y nunca hacia el código de producción.

La tercera es la cancelación. Cuando alguien sube tres correcciones seguidas a la misma rama, las dos primeras ejecuciones ya no informan de nada y sin embargo siguen ocupando agentes escasos. Agrupar por rama y cancelar lo obsoleto suele liberar más capacidad que duplicar el número de máquinas.

Hay una cuarta palanca que no es de configuración sino de arquitectura y por eso se menciona aparte: el tamaño del cambio. Una propuesta de cambio pequeña recorre el pipeline más rápido no porque el pipeline sea más listo, sino porque toca menos módulos, invalida menos caché y produce menos conflictos en la cola de fusión. Los equipos que reducen a la mitad el tamaño medio de sus cambios suelen obtener una mejora de tiempo de ciclo mayor que la que consiguen con cualquier ajuste de infraestructura, y además la obtienen gratis.

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

on:
  pull_request:
    paths-ignore:
      - 'docs/**'
      - '**/*.md'

Queda una decisión de gobierno que ninguna herramienta toma por ti: qué comprobaciones bloquean y cuáles solo informan. Una métrica que nadie está dispuesto a defender delante de un compañero apurado no debe tener poder de veto, porque en la primera urgencia se saltará y con ella se erosionará el respeto por todas las demás. Es preferible un conjunto pequeño de comprobaciones bloqueantes que el equipo entero considera legítimas, con el resto publicado como información, que un muro de vetos que se desactivan con un permiso especial cada viernes.

⚠️
La cola de fusión resuelve un problema que la puerta 1 no ve

Dos cambios pueden pasar sus comprobaciones por separado y romper la rama principal al juntarse, porque cada uno se verificó contra un estado anterior. Una cola de fusión que reejecuta la puerta 2 sobre el resultado real de la combinación elimina esa clase entera de roturas. Cuesta tiempo de agente y solo compensa cuando la frecuencia de fusión es alta, pero cuando compensa no hay sustituto.

El pipeline también es un producto que se mide

Un pipeline sin instrumentación se gestiona por anécdota, y la anécdota siempre favorece a quien tiene la queja más reciente. Las cuatro magnitudes que hacen falta para discutir con datos son pocas y baratas de recoger. La duración por etapa, que dice dónde está el tiempo. El tiempo de cola, que dice si el problema es capacidad y no ejecución. La tasa de fallo por comprobación, que separa las que atrapan defectos de las que solo hacen ruido. Y la tasa de reejecución, que es el indicador más honesto de inestabilidad porque mide lo que la gente hace, no lo que dice.

# Extraer duraciones y resultados de una ejecucion para alimentar el panel.
xcrun xcresulttool get test-results summary --path resultado.xcresult --format json \
  | jq '{total: .totalTestCount, fallos: .failedTests, duracion: .totalDuration}'

# Ranking de las clases mas lentas, que es la entrada del reparto por particiones.
xcrun xcresulttool get test-results tests --path resultado.xcresult --format json \
  | jq -r '.testNodes[] | "\(.duration)\t\(.name)"' | sort -rn | head -20

Con esos dos comandos guardados como artefacto en cada ejecución se tiene, al cabo de un mes, todo lo necesario para tomar las decisiones de reparto sin discutir. El ranking de duración alimenta particiones equilibradas, que es bastante mejor que repartir por número de clases; la tasa de fallo por comprobación identifica candidatas a bajar de puerta; y la comparación entre duración y tiempo de cola decide si la respuesta correcta es optimizar o comprar capacidad.

Hay un quinto indicador que no sale de ninguna herramienta y que conviene recoger a mano cada trimestre: cuántas veces alguien saltó una comprobación bloqueante con un permiso especial, y por qué. Ese número es el termómetro de la legitimidad del pipeline. Si es cero durante meses, o el conjunto de vetos es razonable o nadie tiene la autoridad para saltárselos; si es alto, el diseño ya no representa lo que el equipo cree y conviene rehacerlo antes de que la desviación se institucionalice.

Un pipeline es un sistema de decisión, no un conjunto de comprobaciones

La confusión que gobierna casi todos los pipelines malos es tratar cada comprobación como un bien en sí mismo, cuando su único valor está en las decisiones que alguien toma con su resultado. Eso tiene tres consecuencias que ordenan todo lo anterior. La primera: una comprobación cuyo fallo no cambia el comportamiento de nadie no tiene valor, solo coste, y debería salir del camino crítico aunque sea técnicamente impecable. La segunda: la información tiene fecha de caducidad, y una señal correcta que llega cuarenta minutos tarde vale mucho menos que una señal parcial que llega en cinco, porque el autor ya no tiene el problema en la cabeza y porque otros cambios ya se han apilado encima. La tercera, y la más incómoda: la fiabilidad del conjunto se parece más al mínimo de sus partes que a su media, de modo que una sola comprobación inestable en la puerta 1 destruye la autoridad de las diez que son perfectamente deterministas, porque enseña al equipo que el rojo puede no significar nada y esa lección no se puede desaprender selectivamente. De ahí sale el criterio de diseño completo: pocas puertas, cada una con un presupuesto de tiempo explícito que se respeta, dentro de cada una solo comprobaciones deterministas cuyo fallo alguien está dispuesto a tratar como motivo suficiente para parar, y todo lo demás publicado como información sin poder de veto. Todo lo que no encaja en ese esquema no es una comprobación insuficientemente configurada: es una comprobación que está en la puerta equivocada.

⚔️ Auditar y rediseñar tu pipeline
  1. Mide la duración real de cada etapa de tu pipeline actual y también el tiempo de cola, por separado. Ordena de mayor a menor.
  2. Para cada comprobación, escribe cuántas veces falló en los últimos tres meses y cuántas de esas veces el fallo señalaba un defecto real. Las que nunca detectaron nada son candidatas a bajar de puerta.
  3. Fija un presupuesto explícito para la puerta 1 y reasigna a la puerta 2 o a la nocturna todo lo que no quepa dentro.
  4. Añade un archive de verificación en release sin firma a la puerta de fusión y comprueba si aparece algún fallo que hoy solo verías el día de la entrega.
  5. Activa cancelación por rama y mide durante una semana cuántos minutos de agente libera. Compara ese número con el coste de añadir un agente más.