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ónEfectoCuándo usarlo
Ramificación en diamanteEjecución paralela reduce el tiempo de CIlint/test/typecheck no dependen entre sí
Grupos concurrencyCancela automáticamente ejecuciones obsoletasPushes consecutivos al mismo PR
fail-fast: falseUn fallo no arrastra al restoQuiere resultados de cada combinación de matrix
Workflows reutilizables (workflow_call)Elimina definiciones duplicadasLa misma secuencia de jobs se repite en varios workflows/repos
Job de notificación con if: always()Garantiza la notificación incluso con fallosAlertas de Slack, informes de estado
Espera parcial de matrixDesplegar antes desde un subconjunto de combinacionesDespliegue 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ónComportamientoAdecuado para
fail-fast: true (por defecto)Cualquier combinación que falle cancela el resto de inmediatoRetroalimentación rápida — una vez sabe que falló, no necesita más datos
fail-fast: falseTodas 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), needs por 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

  1. Pegue su workflow YAML en el Visualizador de GitHub Actions para ver el grafo de dependencias
  2. Busque tramos serializados en línea recta y pregúntese si ese orden es realmente necesario (Patrón 1)
  3. Si le preocupan las ejecuciones duplicadas por pushes consecutivos, añada concurrency (Patrón 2)
  4. Para jobs con matrix, compruebe si fail-fast coincide con su objetivo real (Patrón 3)
  5. 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.