Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
needs es un punto de encuentro — y cada uno que añade puede ralentizar la CI
La palabra clave needs de GitHub Actions crea una dependencia (un punto de encuentro) entre jobs. Es útil, pero tiene un coste: cada needs que escribe es una fuente potencial de tiempo añadido a la CI. Los equipos tienden a escribir needs de forma conservadora, y jobs que podrían ejecutarse en paralelo terminan serializados con mucha más frecuencia de la necesaria.
Este artículo es una referencia de patrones y antipatrones centrada en needs. Compare su propio YAML con cada patrón mientras lee.
Chuleta de patrones
| Patrón | Efecto | Cuándo usarlo |
|---|---|---|
| Ramificación en diamante | Ejecución paralela reduce el tiempo de CI | lint/test/typecheck no dependen entre sí |
Grupos concurrency | Cancela automáticamente ejecuciones obsoletas | Pushes consecutivos al mismo PR |
fail-fast: false | Un fallo no arrastra al resto | Quiere resultados de cada combinación de matrix |
Workflows reutilizables (workflow_call) | Elimina definiciones duplicadas | La misma secuencia de jobs se repite en varios workflows/repos |
Job de notificación con if: always() | Garantiza la notificación incluso con fallos | Alertas de Slack, informes de estado |
| Espera parcial de matrix | Desplegar antes desde un subconjunto de combinaciones | Despliegue anticipado tipo canary |
Repasemos cada uno con un ejemplo concreto.
Patrón 1: romper la serialización con forma de diamante
El antipatrón más común es alinear jobs que en realidad son independientes.
# Antipatrón: lint -> unit-test -> typecheck -> build, todo en serie
jobs:
lint:
runs-on: ubuntu-latest
unit-test:
needs: lint
typecheck:
needs: unit-test
build:
needs: typecheck
unit-test y typecheck no comparten ninguna precondición más allá de “el código ya está ahí”. Si ambos solo necesitan que lint termine, cuélguelos directamente de lint y forme un diamante.
# Mejor: unit-test y typecheck se ejecutan en paralelo tras lint
jobs:
lint:
runs-on: ubuntu-latest
unit-test:
needs: lint
typecheck:
needs: lint
build:
needs: [unit-test, typecheck] # espera a ambos
Convertir el needs de build en un array expresa “ejecutar una vez que ambos terminen”. Mismo número de jobs, pero la anchura del grafo de dependencias — su paralelismo — reduce directamente el tiempo de CI. Pegue el workflow en el Visualizador de GitHub Actions y este tipo de serialización innecesaria salta a la vista en el diagrama.
Patrón 2: cancelar la ejecución anterior con concurrency
Incluso con un grafo de needs bien diseñado, no ayuda de nada si las ejecuciones obsoletas se acumulan en la cola por pushes consecutivos a la misma rama. concurrency elimina ese desperdicio.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Incluir github.ref (nombre de rama o número de PR) en group hace que un nuevo push a la misma rama cancele automáticamente la ejecución obsoleta. Si hace push tres veces seguidas a un PR, idealmente solo la última ejecución significativa llega hasta el final.
:::message
Si group no incluye también el nombre del job o del workflow, workflows no relacionados pueden acabar cancelándose entre sí sin querer. Un patrón como ${{ github.workflow }}-${{ github.ref }} delimita el grupo por workflow y es la opción más segura por defecto.
:::
Patrón 3: elegir fail-fast con criterio
strategy.matrix expande una definición de job en múltiples ejecuciones (Node 18/20/22 × 3 sistemas operativos, por ejemplo). Cómo configure fail-fast aquí afecta mucho al comportamiento de la CI.
strategy:
fail-fast: false # el valor por defecto es true
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest, macos-latest]
| Configuración | Comportamiento | Adecuado para |
|---|---|---|
fail-fast: true (por defecto) | Cualquier combinación que falle cancela el resto de inmediato | Retroalimentación rápida — una vez sabe que falló, no necesita más datos |
fail-fast: false | Todas las combinaciones se ejecutan hasta el final aunque una falle | ”¿Es solo Node 20, o también depende del SO?” — quiere el panorama completo en una sola ejecución |
Antes de una release, cuando quiera ver el estado de todas las combinaciones a la vez, false merece la pena. Para comprobaciones de PR del día a día donde la velocidad importa más, deje el true por defecto y priorice la detección rápida de fallos.
Patrón 4: la trampa de esperar un job de matrix con needs
Cuando build depende de test (expandido en matrix), escribir needs: test espera correctamente a todas las combinaciones de la matrix. Hasta aquí es intuitivo. Dos cosas no lo son:
- Si una sola combinación falla, el job dependiente también se salta — especialmente con
fail-fast: true, el trabajo posterior puede detenerse por un resultado parcial - Si quiere que solo algunas combinaciones avancen antes (por ejemplo, desplegar Linux de inmediato y revisar Windows/macOS después),
needspor sí solo no puede expresarlo — hay que dividir el propio job
# Dividir jobs para que Linux pueda desplegarse antes que el resto
jobs:
test-linux:
runs-on: ubuntu-latest
test-other-os:
strategy:
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
deploy:
needs: test-linux # no espera a Windows/macOS
La idea clave: “esperar a todo, o esperar solo a una parte” se decide por cómo divide los jobs, no por cómo escribe needs.
Patrón 5: compartir el grafo de dependencias con workflows reutilizables
Si la misma secuencia de jobs se reutiliza en varios workflows — o en varios repositorios — workflow_call le permite centralizarla en un solo lugar.
# .github/workflows/ci.yml (quien llama)
jobs:
call-shared-ci:
uses: ./.github/workflows/shared-test-suite.yml
with:
node-version: '20'
La estructura needs dentro del workflow llamado (shared-test-suite.yml) se reutiliza por completo, así que no tiene que copiar y pegar la relación “lint depende de test” entre repos. Una vez que ha consolidado un patrón de grafo de dependencias, este es el movimiento final: convertir el diseño mismo en un componente reutilizable.
Mantenga el job de notificación fuera de la propagación de fallos de needs
Por cómo funciona needs, un job de notificación se salta automáticamente si el job del que depende falla. Las notificaciones son lo único que quiere que se ejecute siempre, así que márquelo explícitamente con if: always().
notify:
needs: [lint, test, build]
if: always() # siempre se ejecuta, sin importar el éxito/fallo previo
runs-on: ubuntu-latest
steps:
- run: echo "Result: ${{ needs.build.result }}"
needs.<job>.result expone el resultado de cada dependencia (success / failure / cancelled / skipped), así que puede construir un mensaje de notificación que diga exactamente qué falló.
Lista de verificación para auditar su grafo de dependencias
- Pegue su workflow YAML en el Visualizador de GitHub Actions para ver el grafo de dependencias
- Busque tramos serializados en línea recta y pregúntese si ese orden es realmente necesario (Patrón 1)
- Si le preocupan las ejecuciones duplicadas por pushes consecutivos, añada
concurrency(Patrón 2) - Para jobs con matrix, compruebe si
fail-fastcoincide con su objetivo real (Patrón 3) - Si la misma estructura de dependencias se repite entre workflows, considere extraer un workflow reutilizable (Patrón 5)
El tiempo de ejecución de la CI a menudo se aprovecha mejor arreglando la forma del grafo de dependencias que recortando pasos individuales. Diagrame su workflow una vez y busque serializaciones innecesarias.
Preguntas frecuentes
¿Añadir más needs siempre ralentiza la CI?
No es needs en sí — el problema es escribir una dependencia que en realidad no necesita esperar. Un needs que espera un prerrequisito genuino (un artefacto compilado, por ejemplo) es una dependencia necesaria y no debería eliminarse. Lo que vale la pena revisar es la cadena innecesaria — por ejemplo, esperar a que termine todo un job anterior cuando en realidad solo hacía falta que lint completara.
¿El cancel-in-progress de concurrency debería ser siempre true?
Para CI activada por pushes continuos a un PR, generalmente sí. Pero en workflows de despliegue que se ejecutan tras hacer merge a main, la cancelación puede dejar un despliegue a medias — considere cancel-in-progress: false, o directamente no usar concurrency en workflows de despliegue.
¿fail-fast: false hace que la CI tarde más?
Puede que sí. fail-fast: true (el valor por defecto) corta el resto de jobs en el primer fallo, lo que suele acortar el tiempo medio de CI. Reserve false para situaciones donde realmente necesite resultados de todas las combinaciones — comprobaciones de entorno previas a una release, por ejemplo.
¿El YAML del workflow que uso para revisar dependencias se envía a algún sitio?
No. El Visualizador de GitHub Actions procesa todo en su navegador, así que pegar definiciones de workflow internas nunca las envía a un servidor.