El pipeline: instalar, verificar, construir, desplegar
Un pipeline de CI/CD no es una lista de comandos que corren en fila, sino un grafo de etapas con dependencias reales. Instalar es la raíz común; lint, typecheck y test son ramas independientes que paralelizan; construir alimenta el despliegue. Entender ese grafo es lo que permite ordenar las etapas para fallar pronto sin sacrificar el paralelismo, y separar la verificación de cada PR del despliegue de la rama principal por su disparador.
El error de principiante al montar CI es escribir el pipeline como un script: instalar, luego lint, luego test, luego build, luego deploy, cada paso esperando al anterior aunque no lo necesite. El resultado es un reloj que suma todo lo que podría solaparse. Un pipeline maduro se modela como un grafo dirigido acíclico de etapas: instalar dependencias es la raíz de la que todo cuelga; lint, typecheck y los tests unitarios son ramas hermanas que no se necesitan entre sí y corren a la vez; construir es la etapa que alimenta al despliegue. Ver ese grafo es lo que te permite ordenar las etapas para dos objetivos a la vez que parecen reñidos: dar feedback lo antes posible cuando algo falla, y minimizar el tiempo total cuando todo pasa.
- Modelar el pipeline como un grafo de etapas, no como una secuencia de comandos.
- Distinguir las etapas que deben serializar de las que pueden paralelizar.
- Ordenar por coste para fallar pronto sin renunciar al paralelismo.
- Separar la verificación de cada PR del despliegue de la rama principal por su disparador.
Las etapas y sus dependencias reales
Toda la arquitectura de un pipeline se sigue de una pregunta por etapa: ¿qué necesita para empezar? La respuesta dibuja el grafo. Instalar las dependencias no necesita nada más que el código y el lockfile, así que es la raíz. lint, typecheck —la comprobación de tipos con tsc --noEmit— y los tests unitarios necesitan node_modules, pero no se necesitan entre sí: son tres ramas hermanas que cuelgan de la instalación y pueden correr en paralelo. Construir también depende solo de la instalación, no de que los tests hayan pasado. Y desplegar es el único nodo que exige que todo lo anterior esté verde: es la confluencia donde el grafo se cierra.
name: ci
on: [push, pull_request]
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
lint: { needs: install, runs-on: ubuntu-latest, steps: [{ run: pnpm lint }] }
typecheck: { needs: install, runs-on: ubuntu-latest, steps: [{ run: pnpm typecheck }] }
test: { needs: install, runs-on: ubuntu-latest, steps: [{ run: pnpm test }] }
build: { needs: install, runs-on: ubuntu-latest, steps: [{ run: pnpm build }] }
La palabra clave que expresa una arista es needs. Cuando escribes needs: install, no estás pidiendo que el job corra “después” en sentido temporal, sino declarando una precedencia: no puede empezar hasta que su dependencia termine con éxito. Todos los jobs que declaran el mismo needs y ninguna arista entre ellos arrancan a la vez. Modelar bien el pipeline es, en el fondo, declarar exactamente las aristas que existen y ni una de más: un needs sobrante serializa lo que podía solaparse y alarga el reloj sin ninguna razón.
Rápido en fallar y ancho en paralelo
Hay dos fuerzas que tiran del orden en direcciones opuestas, y un buen pipeline las reconcilia en lugar de elegir una. La primera es fallar pronto: si vas a rechazar el cambio, hazlo con la comprobación más barata primero, para no gastar minutos de build en código que un lint de tres segundos habría tumbado. La segunda es paralelizar: correr a la vez todo lo que no depende entre sí para que el reloj lo marque la etapa más lenta, no la suma. Parecen reñidas —ordenar por coste sugiere una fila; paralelizar sugiere lanzarlo todo junto— pero operan en planos distintos: el paralelismo gobierna los jobs independientes, y el orden por coste gobierna los pasos dentro de cada job.
flowchart TD I[install deps] --> L[lint] I --> TC[typecheck] I --> T[unit test] I --> B[build] L --> G[gate todo verde] TC --> G T --> G B --> G G --> D[deploy] style I fill:#89b4fa,color:#11111b style B fill:#f9e2af,color:#11111b style D fill:#a6e3a1,color:#11111b
La reconciliación práctica es esta: los jobs lint, typecheck y test corren en paralelo, y dentro de cada uno pones lo barato antes que lo caro. Si dudas si merece la pena tener jobs separados o pasos ordenados en uno solo, la regla es simple: separa en jobs lo que puede fallar por razones independientes y quieres ver por separado, y ordena en pasos lo que comparte contexto. Un typecheck que tarda diez segundos y un test suite que tarda cuatro minutos no deberían compartir job, porque quieres el veredicto del typecheck sin esperar al test.
Cada vez que empujas a una rama de PR, el pipeline anterior sobre esa misma rama queda obsoleto: su veredicto es sobre código que ya no existe. Un grupo de concurrencia con cancelación aborta la corrida en curso cuando llega una nueva sobre la misma referencia, para no quemar runners en vano.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Sin un grupo de concurrencia, tres empujones seguidos dejan tres pipelines corriendo a la vez, dos de ellos sobre código que ya nadie mira. La cancelación en curso no solo ahorra minutos de runner: acorta el tiempo hasta el veredicto que sí cuenta, el del último commit, porque libera los ejecutores que malgastaban las corridas muertas. Es una de esas mejoras que no tocan ninguna etapa y aun así aceleran todo el sistema.
El orden que minimiza el reloj
El tiempo de un pipeline no es la suma de sus etapas, sino la longitud de su camino crítico: la cadena más larga de precedencias que hay que atravesar en orden desde la raíz hasta el despliegue. Instalar está en ese camino para todos, porque todo cuelga de ello; por eso la caché de dependencias es la primera palanca, y es el tema de la lección siguiente. Construir está en el camino hacia el despliegue. Los tests, en cambio, gobiernan si el despliegue ocurre, pero no lo preceden en el sentido de que el artefacto no los necesita: build y test corren a la vez, y el despliegue espera a los dos.
| Etapa | Depende de | Coste típico | En camino crítico |
|---|---|---|---|
| install | nada | segundos a minutos | sí, para todos |
| lint | install | segundos | no |
| typecheck | install | segundos a un minuto | no |
| test | install | minutos | como gate |
| build | install | minutos | sí, hacia deploy |
| deploy | build y todo verde | segundos a minutos | sí |
La lectura de la tabla dicta las optimizaciones. Acortar el camino crítico significa atacar install, build y deploy, que es donde el reloj vive; acelerar lint no baja el tiempo total si ya corre en paralelo con un build más lento, porque no está en el camino crítico. Este es el error de intuición más común: se optimiza la etapa que molesta, no la que manda. La matriz —correr los tests contra varias versiones de Node o varios navegadores— ensancha el grafo sin alargarlo, y por eso escala bien: veinte combinaciones en paralelo tardan casi lo mismo que una.
CI y CD: dos disparadores, un grafo
El mismo grafo sirve para dos propósitos según quién lo dispara. Un empujón a una rama de PR dispara la integración continua: verifica el cambio y, como veremos, publica una preview, pero no toca producción. Una fusión a la rama principal dispara la entrega continua: reconstruye y despliega. La etapa de despliegue se protege con una condición sobre la referencia, de modo que el nodo deploy solo se activa en el contexto correcto.
deploy:
needs: [lint, typecheck, test, build]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: pnpm run deploy
Que sea un grafo con dos disparadores, y no dos pipelines duplicados, es lo que garantiza que lo que verificas en el PR es exactamente lo que despliegas al fusionar. Duplicar la definición es la fuente clásica de la sorpresa “pasaba en el PR pero rompió en producción”: dos configuraciones que divergen sin que nadie lo note. La forma correcta es una sola descripción del grafo, donde la única diferencia entre integrar y entregar es la guarda if sobre el nodo final.
Merece la pena resistir la tentación de ver el pipeline como una lista de cosas que la máquina teclea por ti, porque esa metáfora te ciega ante todo lo que importa. Un pipeline es la definición ejecutable del contrato de calidad de tu proyecto: la afirmación, comprobable en cada cambio, de que el código que entra a la rama principal instala, pasa el linter, es sano en tipos, verde en tests, construible, y desplegable. Y como todo contrato serio, su valor está en su estructura, no en sus cláusulas sueltas. Modelarlo como un grafo, y no como una secuencia, cambia lo que puedes razonar sobre él: puedes preguntar cuál es su camino crítico y atacarlo; puedes ver qué etapas son independientes y paralelizarlas; puedes distinguir lo que precede al despliegue de lo que lo autoriza; puedes separar el disparador que verifica del que entrega sin duplicar una línea. El script secuencial no admite ninguna de esas preguntas porque ha aplanado la información que las respondería: ha convertido un grafo con estructura en una fila sin ella. Interiorizar esto reordena tu forma de trabajar en CI. Dejas de preguntarte “qué comando va después” y empiezas a preguntarte “qué necesita esta etapa para empezar”, que es la única pregunta que dibuja las aristas correctas. El reloj del pipeline, la nitidez de su feedback y la fidelidad entre lo que verificas y lo que despliegas son, las tres, consecuencias directas de haber acertado con el grafo. Un pipeline bien modelado no es más rápido por casualidad: es rápido porque su forma es la de sus dependencias reales, y ni una arista más.
- En papel, lista las etapas de tu proyecto y anota, por cada una, qué necesita para empezar; une con flechas y verás el grafo.
- Identifica el camino crítico desde
installhastadeployy marca qué etapas quedan fuera de él. - Convierte ese grafo a
needsen tu CI, comprobando quelint,typecheckytestno dependen unos de otros. - Añade un grupo de concurrencia con cancelación y verifica que un segundo empujón cancela el pipeline en vuelo.
- Protege el nodo
deploycon una guarda sobre la rama principal y confirma que un PR verifica pero no despliega.