Aller au contenu
English
CI/CD & Automatisation medium

needs et job outputs : modéliser un pipeline en graphe

Read this page in English

30 min de lecture

Par défaut, les jobs d'un workflow s'exécutent en parallèle et ne partagent rien. Une vraie CI n'est pas une liste de jobs indépendants : c'est un graphe où certaines étapes en attendent d'autres et consomment leurs résultats. Cette page couvre needs, les outputs de job, et surtout ce qui se passe quand une branche du graphe échoue.

  • Enchaîner des jobs avec needs, en parallèle et en séquence
  • Faire circuler une valeur d'un job à l'autre avec les outputs
  • Transporter une structure via JSON et fromJSON
  • Maîtriser l'échec : jobs sautés, always(), failure(), cancelled()
  • Combiner needs et matrices sans se piéger

Le parallélisme par défaut, et ce que needs change

Section intitulée « Le parallélisme par défaut, et ce que needs change »

Sans needs, tous les jobs démarrent en même temps, chacun sur une machine neuve. C'est rapide, et c'est faux dès qu'une étape dépend d'une autre : publier avant d'avoir testé n'a pas de sens.

needs déclare une dépendance et, par voie de conséquence, un ordre :

name: CI
on:
pull_request:
branches: [main]
permissions: {}
jobs:
lint:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm ci && npm run lint
test:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm ci && npm test
build:
needs: [lint, test]
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm ci && npm run build

lint et test démarrent ensemble, build attend les deux. La forme du graphe se lit dans les needs, pas dans l'ordre d'écriture des jobs : déplacer build en tête du fichier ne change rien.

lint ───┐
├──> build ──> scan ──> publish
test ───┘

Cette structure, plusieurs analyses en parallèle puis une convergence, est la forme la plus courante d'une CI sérieuse. Elle donne le retour le plus rapide possible sur les erreurs les plus fréquentes, tout en garantissant que rien n'est construit sur du code qui ne passe pas.

Chaque job tourne sur une machine différente. Rien ne se transmet automatiquement : ni fichiers, ni variables. Pour les fichiers, ce sont les artefacts ; pour les valeurs, ce sont les outputs.

Le mécanisme se déclare à deux niveaux. L'étape écrit dans $GITHUB_OUTPUT, le job promeut cette valeur en output de job :

jobs:
version:
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
numero: ${{ steps.calcul.outputs.numero }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Calculer le numéro de version
id: calcul
run: echo "numero=1.4.$GITHUB_RUN_NUMBER" >> "$GITHUB_OUTPUT"
build:
needs: version
runs-on: ubuntu-24.04
permissions:
contents: read
env:
VERSION: ${{ needs.version.outputs.numero }}
steps:
- name: Construire avec la version calculée
run: echo "Construction de la version $VERSION"

Trois règles évitent l'essentiel des erreurs :

  • L'id de l'étape est obligatoire pour être référencée. Sans id:, la valeur est inaccessible.
  • Le job consommateur doit déclarer needs. Le contexte needs.<job>.outputs n'existe que pour les jobs dont on dépend, même si le job producteur a déjà terminé.
  • Tout output est une chaîne de caractères. Il n'y a ni entier, ni booléen, ni tableau.

Puisqu'un output est une chaîne, transporter une liste ou un objet passe par du JSON sérialisé, décodé à l'arrivée par fromJSON.

jobs:
cibles:
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
liste: ${{ steps.detect.outputs.liste }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Détecter les services modifiés
id: detect
run: echo 'liste=["api","worker","frontend"]' >> "$GITHUB_OUTPUT"
deployer:
needs: cibles
runs-on: ubuntu-24.04
permissions:
contents: read
strategy:
matrix:
service: ${{ fromJSON(needs.cibles.outputs.liste) }}
steps:
- name: Déployer un service
env:
SERVICE: ${{ matrix.service }}
run: echo "Déploiement de $SERVICE"

C'est le motif de la matrice dynamique : un premier job calcule la liste des cibles, un second en dérive autant d'exécutions parallèles. Il évite de maintenir à la main une matrice qui répète ce que le dépôt contient déjà.

Une limite à connaître : la chaîne JSON doit être valide et sur une seule ligne. Un JSON produit par un outil peut contenir des retours à la ligne qui cassent l'écriture dans $GITHUB_OUTPUT. Le compacter règle le problème :

Fenêtre de terminal
echo "liste=$(jq -c . cibles.json)" >> "$GITHUB_OUTPUT"

C'est la partie qui surprend, et elle tient en une phrase de la documentation GitHub :

Si un job échoue ou est sauté, tous les jobs qui en dépendent sont sautés, à moins que ces jobs n'utilisent une expression conditionnelle qui les fasse continuer.

Deux conséquences pratiques. Un job dépendant n'est pas marqué en échec, il est sauté : le workflow affiche un résultat qui n'est pas rouge partout, ce qui trompe à la lecture rapide. Et un job sauté propage : toute sa descendance est sautée à son tour.

La documentation est explicite sur le moyen :

Si vous voulez qu'un job s'exécute même lorsqu'un job dont il dépend n'a pas réussi, utilisez l'expression conditionnelle always().

rapport:
needs: [lint, test, build]
if: always()
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Publier le rapport quel que soit le résultat
env:
RESULTAT_TEST: ${{ needs.test.result }}
run: echo "Résultat des tests : $RESULTAT_TEST"

Le contexte needs.<job>.result porte l'issue réelle du job dont on dépend : success, failure, cancelled ou skipped. C'est lui qui permet à un job de rapport de dire ce qui a échoué.

Quatre fonctions couvrent les cas courants :

ConditionLe job s'exécute
success()Si tous les jobs de needs ont réussi, c'est le défaut implicite
failure()Si au moins un job de needs a échoué
cancelled()Si le workflow a été annulé
always()Systématiquement, y compris après annulation

Un job marqué continue-on-error: true est considéré comme réussi du point de vue de ses dépendants, même s'il a échoué. C'est utile pour un contrôle informatif, et dangereux pour un contrôle de sécurité : un scanner en continue-on-error ne bloque plus rien, il décore.

Si vous voulez conserver l'information sans bloquer, gardez le job bloquant et laissez le job suivant décider à partir de needs.<job>.result.

Un job qui dépend d'un job matriciel attend toutes les combinaisons, pas la première. C'est le comportement souhaitable : on ne construit pas une image parce que les tests passent sur une seule version de Python.

jobs:
test:
runs-on: ubuntu-24.04
permissions:
contents: read
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python }}
- run: pytest -q
build:
needs: test
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- run: echo "Toutes les versions ont passé les tests"

Deux points de vigilance. fail-fast: false laisse toutes les branches aller au bout : sans lui, le premier échec annule les autres et vous perdez l'information la plus utile, à savoir si le problème touche une version ou toutes. Et les outputs d'un job matriciel sont ambigus : toutes les combinaisons écrivent dans le même output, la dernière terminée l'emporte. Ne comptez pas dessus, utilisez des artefacts nommés par combinaison.

Une fois le pipeline écrit, trois questions suffisent à repérer les défauts structurels courants.

  1. Quels jobs pourraient tourner en parallèle mais ne le font pas ? Un needs de confort allonge le pipeline sans rien garantir.

  2. Quel job publie ou déploie, et de quoi dépend-il réellement ? Si sa chaîne de needs ne remonte pas jusqu'aux tests et aux scanners de sécurité, il peut livrer du code refusé ailleurs.

  3. Quel job porte un always() ou un continue-on-error ? Chacun est une exception à justifier, et c'est là que les contrôles s'évaporent le plus discrètement.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Les jobs sont parallèles par défaut ; needs déclare une dépendance et dessine le graphe, indépendamment de l'ordre d'écriture.
  • La forme utile est fan-out puis convergence : analyses en parallèle, puis build, puis publication.
  • Une valeur circule par les outputs : l'étape écrit dans $GITHUB_OUTPUT, le job la promeut, le consommateur déclare needs.
  • Tout output est une chaîne : les structures passent par du JSON compacté et fromJSON, motif de la matrice dynamique.
  • Ne faites pas transiter un secret par un output : le job qui en a besoin le lit directement.
  • Un job dont une dépendance échoue est sauté, pas en échec, et il propage ce statut à sa descendance.
  • always() s'exécute même après annulation : acceptable pour un rapport, jamais pour un job qui publie ou déploie ; préférez !cancelled().
  • continue-on-error fait passer un job pour réussi auprès de ses dépendants : à proscrire sur un contrôle de sécurité.
  • Un job qui dépend d'une matrice attend toutes les combinaisons ; ses outputs sont ambigus, préférez des artefacts.
  • Workflows réutilisables : Factoriser un graphe qui se répète d'un dépôt à l'autre, avec ses propres entrées et sorties.
  • Actions composites : Regrouper une suite d'étapes récurrentes, là où le workflow réutilisable serait surdimensionné.
  • Sécuriser GitHub Actions : Ce que le graphe de dépendances change pour la sécurité, à commencer par les jobs qui publient.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn