Aller au contenu
Votre premier workflow

Votre premier workflow

200 Pratiquer ⏱ 1 h github-actionsci-cdpython

À la fin, vous saurez

  • Nommer les objets de GitHub Actions (événement, workflow, job, étape, action, runner) et les relier à ceux d'un pipeline générique
  • Écrire un workflow qui vérifie le code à chaque poussée et à chaque demande de fusion
  • Valider un workflow localement avec actionlint avant de le pousser
  • Expliquer ce que fait un runner hébergé, du message de job au script exécuté
  • Éviter les pièges de la première heure : shell sans pipefail, versions numériques en YAML, image qui bouge

Prérequis

Testé avec actionlint 1.7.12 actions/checkout v7.0.1 actions/setup-python v7.0.0 pytest 9.1.1 python 3.14.7 ruff 0.16.9 zizmor 1.30.1 , vérifié le 1 octobre 2026

Pourquoi

Au cours CI/CD : les principes, vous avez construit un serveur d'intégration continue en quelques dizaines de lignes de Bash : un crochet Git déclenchait un script, qui extrayait le commit dans un répertoire neuf, lançait chaque étape dans un conteneur jetable et enregistrait un statut. Ce serveur avait toutes les pièces d'un vrai système, et toutes ses faiblesses : une seule machine, aucun affichage dans les demandes de fusion, des secrets dans un fichier, et personne pour le maintenir.

GitHub Actions est ce même système, opéré par GitHub, intégré à la forge où vit déjà le code. Les pièces sont les mêmes, elles changent seulement de nom, et c'est par là que commence ce cours : en portant le pipeline de Signalements vers un premier workflow, puis en regardant ce que la plateforme fait réellement de ce fichier. Comprendre ce mécanisme dès la première leçon évite une bonne partie des surprises des onze suivantes : pourquoi une étape réussit alors qu'une commande a échoué, pourquoi un workflow ne se déclenche pas, pourquoi le même fichier casse un lundi matin sans que personne l'ait modifié.

Ce que l'on gagne est réel : des agents à la demande, le statut de chaque commit affiché dans la demande de fusion, un écosystème de briques réutilisables. Ce que l'on perd l'est aussi : le pipeline devient du YAML propre à une plateforme, il s'exécute sur des machines que l'on ne contrôle pas, et il assemble du code écrit par des inconnus. Les deux colonnes de ce bilan structurent le cours.

Les concepts

Du serveur fait main à GitHub Actions

Serveur du cours précédentGitHub ActionsCe qui change
Crochet post-receiveÉvénement (push, pull_request, schedule...)Une trentaine de types d'événements, filtrables
Fichier .pipelineWorkflow : .github/workflows/<nom>.ymlPlusieurs workflows par dépôt, chacun indépendant
Le script executer entierJobPlusieurs jobs par workflow, en parallèle par défaut
Une ligne de .pipelineÉtape (step) : run: ou uses:Une étape peut être une brique réutilisable
docker run python:3.14-slimAction (uses: actions/setup-python@v7)Du code tiers, versionné, téléchargé à chaque job
La machine du serveurRunner hébergé (ubuntu-24.04) ou auto-hébergéUne machine virtuelle neuve par job
travaux/<n>/Run (exécution) numéroté, et son espace de travailJournaux conservés 90 jours par défaut
Fichier statutCheck run rattaché au commitVisible dans la demande de fusion, exigible avant fusion

Six termes reviennent sans cesse, et la documentation de GitHub les emploie en anglais :

  • Un workflow est un fichier YAML du répertoire .github/workflows/. Il déclare à quels événements il réagit et quels jobs il exécute.
  • Un événement (event) est ce qui se passe sur le dépôt : une poussée, l'ouverture d'une demande de fusion, une étiquette, une heure fixe, un clic. La leçon 2 leur est consacrée.
  • Un job est un ensemble d'étapes exécutées sur une même machine, dans l'ordre. Deux jobs d'un même workflow s'exécutent par défaut en parallèle, chacun sur sa propre machine, et ne partagent rien d'autre que ce qu'on leur fait échanger explicitement (leçons 3 et 5).
  • Une étape (step) est soit une commande shell (run:), soit l'appel d'une action (uses:). Les étapes d'un job partagent le système de fichiers et les variables d'environnement exportées.
  • Une action est un programme réutilisable, publié dans un dépôt Git, qu'une étape appelle par propriétaire/dépôt@version. actions/checkout récupère le code, actions/setup-python installe un interpréteur. C'est le mécanisme qui fait la richesse de la plateforme, et sa principale surface d'attaque (leçon 11).
  • Un runner est la machine qui exécute un job. Les runners hébergés par GitHub sont des machines virtuelles neuves, détruites après le job ; les runners auto-hébergés sont les vôtres (leçon 12).

Le cycle de vie d'une exécution

    sequenceDiagram
  participant D as Développeur
  participant G as GitHub
  participant R as Runner hébergé (VM neuve)
  D->>G: git push
  G->>G: lit .github/workflows/*.yml au commit poussé
  G->>G: crée un run, met les jobs en file
  R->>G: « du travail pour moi ? » (connexion sortante)
  G-->>R: message de job (étapes, jeton, secrets)
  R->>R: télécharge les actions, exécute les étapes
  R-->>G: journaux en continu, résultat de chaque étape
  G->>G: statut du job rattaché au commit
  Note over R: la VM est détruite
  

Trois points de ce schéma ont des conséquences pratiques :

  1. Le workflow est lu au commit qui a déclenché l'événement. Modifier .github/workflows/ci.yml dans une branche change le pipeline de cette branche, et seulement d'elle. C'est la propriété que vous aviez dans le serveur fait main (le .pipeline venait du commit), avec la même conséquence de sécurité : quiconque peut pousser une branche peut changer ce qu'exécute le pipeline de cette branche. La leçon 11 montre comment GitHub limite ce que ce pipeline peut atteindre.
  2. Le runner va chercher le travail. Comme l'agent du cours précédent, il ouvre une connexion sortante vers GitHub et attend qu'on lui confie un job. Un runner auto-hébergé peut donc vivre dans un réseau privé sans aucun port entrant.
  3. Chaque job a sa machine. Rien ne survit à un job : ni fichier, ni paquet installé, ni processus. C'est la garantie d'un espace de travail neuf, et c'est pourquoi le cache et les artefacts existent.

En pratique

Le dépôt de départ

Partez de l'application Signalements telle qu'à la fin du cours précédent : app.py, test_app.py, test_integration.py, requirements.txt, requirements-dev.txt, le Dockerfile et le répertoire ci/. Poussez-la dans un dépôt GitHub de travail, de préférence public (voir la présentation du cours). Le fichier .pipeline du serveur fait main n'est plus utile : supprimez-le.

Rappel de ce que vérifiait ce pipeline, réduit à l'essentiel pour cette première leçon : l'analyse statique, le formatage et les tests unitaires. Les tests d'intégration contre PostgreSQL reviendront à la leçon 4.

Écrire le workflow

Créez .github/workflows/ci.yml :

name: CI

on:
  push:
  pull_request:

permissions:
  contents: read

jobs:
  verifier:
    name: Lint et tests
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    steps:
      - name: Récupérer le code
        uses: actions/checkout@v7
        with:
          persist-credentials: false

      - name: Installer Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.14"

      - name: Installer les dépendances
        run: pip install -r requirements-dev.txt

      - name: Lint
        run: ruff check .

      - name: Format
        run: ruff format --check .

      - name: Tests unitaires
        run: pytest -q test_app.py

Ligne à ligne :

  • name: CI est le nom affiché dans l'onglet Actions. Sans lui, GitHub affiche le chemin du fichier.
  • on: liste les événements. Ici, toute poussée sur n'importe quelle branche, et toute activité d'ouverture ou de mise à jour d'une demande de fusion. La leçon 2 montre pourquoi cette combinaison exécute souvent deux fois le même travail, et comment l'éviter.
  • permissions: contents: read restreint le jeton que GitHub confie automatiquement à chaque job, le GITHUB_TOKEN. Ce workflow ne fait que lire le code : il n'a besoin de rien d'autre. Déclarer les permissions dès le premier workflow est une habitude qui coûte une ligne ; la leçon 11 explique ce qu'elle protège.
  • jobs: contient un seul job, d'identifiant verifier. L'identifiant sert aux références entre jobs (leçon 4) ; name: est le libellé affiché, et c'est lui qui devient le nom du check sur le commit.
  • runs-on: ubuntu-24.04 choisit l'image du runner hébergé. On aurait pu écrire ubuntu-latest ; la section des pièges explique pourquoi c'est risqué précisément ce mois-ci.
  • timeout-minutes: 10 borne la durée du job. Sans cette ligne, la limite est de 360 minutes : un test bloqué sur une connexion réseau consommerait six heures de runner avant d'être tué.
  • uses: actions/checkout@v7 appelle l'action qui récupère le code. Un runner neuf ne contient pas les sources du dépôt : sans cette étape, le répertoire de travail est vide. Par défaut, checkout récupère uniquement le commit concerné (fetch-depth: 1), sans historique.
  • persist-credentials: false demande à checkout de ne pas laisser le jeton d'accès configuré dans le dépôt cloné une fois l'étape finie. Les étapes suivantes n'ont pas besoin de pousser quoi que ce soit, autant ne pas leur laisser l'identifiant pour le faire.
  • actions/setup-python@v7 installe CPython 3.14 et le place en tête du PATH. La version est entre guillemets, pour une raison que la section des pièges montre.
  • run: exécute une commande dans le shell par défaut. pip, ruff et pytest sont trouvés parce que setup-python a placé l'interpréteur et ses scripts dans le PATH.

Warning

Les actions sont référencées ici par leur étiquette de version majeure (@v7), comme dans la documentation officielle, pour que le fichier reste lisible pendant l'apprentissage. C'est insuffisant en production : une étiquette peut être déplacée vers du code hostile, ce qui s'est produit pour tj-actions/changed-files en 2025 et pour aquasecurity/trivy-action en 2026. La leçon 11 remplace chaque étiquette par l'empreinte du commit.

Valider avant de pousser

Un workflow invalide ne se découvre, par défaut, qu'après la poussée : GitHub affiche une erreur à la place de l'exécution. On perd un aller-retour à chaque faute de frappe. actionlint vérifie localement la syntaxe, les noms d'événements, les étiquettes de runner, les expressions, et passe les scripts run: à ShellCheck s'il est installé :

$ actionlint
$ echo $?
0

Aucun message et un code de sortie nul : le fichier est valide. Pour voir à quoi ressemble un échec, voici le même workflow avec trois erreurs courantes (un événement mal orthographié, une étiquette de runner inventée, une durée en toutes lettres) :

$ actionlint .github/workflows/casse.yml
.github/workflows/casse.yml:4:3: unknown Webhook event "pul_request". see https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#webhook-events for list of all Webhook event names [events]
  |
4 |   pul_request:
  |   ^~~~~~~~~~~~
.github/workflows/casse.yml:7:14: label "ubuntu-24.4" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xlarge", "macos-latest-large", "macos-26-intel", "macos-26-xlarge", "macos-26-large", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xlarge", "macos-14-large", "macos-14", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label]
  |
7 |     runs-on: ubuntu-24.4
  |              ^~~~~~~~~~~
.github/workflows/casse.yml:15:26: expecting a single ${{...}} expression or float number literal, but found plain text node [syntax-check]
   |
15 |         timeout-minutes: "dix"
   |                          ^~~~~
$ echo $?
1

Chaque erreur est localisée (ligne, colonne) et nommée entre crochets, ce qui permet de la désactiver au besoin dans .github/actionlint.yaml. Remarquez la liste des étiquettes connues : actionlint 1.7.12 ne connaît pas encore ubuntu-26.04, publiée en septembre 2026. Un outil de validation a toujours un temps de retard sur la plateforme ; une étiquette récente signalée comme inconnue se déclare dans la configuration d'actionlint plutôt que de désactiver la vérification.

Un second outil, zizmor, audite les workflows du point de vue de la sécurité. Il sera au centre de la leçon 11, mais il vaut la peine de le lancer dès maintenant sur ce premier fichier :

$ uvx zizmor --offline .github/workflows/ci.yml
error[unpinned-uses]: unpinned action reference
  --> .github/workflows/ci.yml:22:15
   |
22 |         uses: actions/setup-python@v7
   |               ^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy)
   |
   = note: audit confidence → High
   = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses

3 findings (1 suppressed): 0 informational, 0 low, 0 medium, 2 high

(La sortie est abrégée : le même constat est fait pour actions/checkout.) Les deux alertes de gravité haute portent exactement sur ce que l'encadré précédent annonçait. On les laisse en place jusqu'à la leçon 11, en sachant qu'elles sont là.

Reproduire le job localement

Avant de pousser, on peut vérifier que les commandes du job passent, dans un environnement proche de celui du runner. Ce n'est pas la même machine (le runner est une VM Ubuntu complète, pas une image python:3.14-slim), mais ce sont les mêmes commandes, avec les mêmes versions d'outils, épinglées dans requirements-dev.txt :

$ docker run --rm -v "$PWD":/src -w /src --user "$(id -u):$(id -g)" -e HOME=/tmp \
    -e PATH=/tmp/.local/bin:/usr/local/bin:/usr/bin:/bin python:3.14-slim \
    bash --noprofile --norc -eo pipefail -c '
      pip install --user --quiet --disable-pip-version-check -r requirements-dev.txt
      ruff check --no-cache .
      ruff format --check --no-cache .
      pytest -q -p no:cacheprovider test_app.py'
All checks passed!
5 files already formatted
....                                                                     [100%]
4 passed in 0.13s

Le shell est lancé avec exactement les options qu'utilise le runner quand on écrit shell: bash ; la section Sous le capot explique pourquoi ce détail compte.

Pousser et suivre l'exécution

$ git add .github/workflows/ci.yml
$ git rm -q .pipeline
$ git commit -m "CI : premier workflow GitHub Actions"
$ git push origin main

Dans l'onglet Actions du dépôt, un run apparaît, nommé d'après le message du commit. Il contient un job, Lint et tests, dont chaque étape se déplie pour montrer son journal. Le job commence par une étape que vous n'avez pas écrite, Set up job, et se termine par Post Récupérer le code et Complete job : elles sont décrites plus bas. Sur la page du commit, une coche verte (ou une croix rouge) apparaît à côté de l'empreinte : c'est le check run, le statut rattaché au commit.

Le même suivi se fait depuis le terminal avec gh, la ligne de commande de GitHub :

$ gh run list --workflow ci.yml --limit 5     # les dernières exécutions de ce workflow
$ gh run watch --exit-status                  # suit l'exécution en cours, échoue si elle échoue
$ gh run view --log-failed                    # n'affiche que le journal des étapes en échec

gh run watch --exit-status est utile dans un script local : il rend la main avec un code de sortie non nul si le run échoue, exactement comme le script executer du cours précédent.

Casser exprès

Un pipeline n'a de valeur que s'il passe au rouge quand il le doit. Introduisez une erreur dans un test, par exemple en remplaçant == 200 par == 201 dans test_accueil, poussez, et observez : l'étape Tests unitaires échoue, les étapes suivantes (il n'y en a pas ici) seraient sautées, le job est rouge, le commit porte une croix. Corrigez et poussez à nouveau. Faites-le une fois pour de vrai : c'est le seul moyen d'être sûr que le pipeline vérifie quelque chose.

Sous le capot

L'agent : un écouteur et un exécutant

Le runner est un programme libre, actions/runner, écrit en C#. Il se compose de deux processus. Runner.Listener maintient la connexion sortante avec GitHub et attend qu'un job lui soit attribué. Quand un job arrive, il lance Runner.Worker, qui reçoit le message de job : la liste des étapes déjà résolue, les variables, le GITHUB_TOKEN du job et les secrets dont il a besoin. Le Worker exécute alors les étapes une à une et renvoie les journaux au fil de l'eau.

Sur un runner hébergé, ce couple tourne dans une machine virtuelle créée pour le job, à partir d'une image publique (le dépôt actions/runner-images en décrit le contenu : compilateurs, Docker, gh, des dizaines d'outils). Pour un dépôt public, une VM ubuntu-24.04 offre 4 processeurs virtuels et 16 Go de mémoire ; pour un dépôt privé, 2 processeurs et 8 Go. La différence se voit sur les temps de construction, et sur la facture.

L'étape « Set up job »

Avant vos étapes, le Worker prépare le job. Le journal de Set up job indique la version de l'agent, l'image et sa version, les permissions effectives du GITHUB_TOKEN (vérifiez-y que permissions: contents: read a bien été appliqué), puis le téléchargement de chaque action référencée : le Worker récupère l'archive du dépôt de l'action à la version demandée, avant d'exécuter quoi que ce soit. Une action introuvable ou une version inexistante fait donc échouer le job dès cette étape.

Certaines actions déclarent une étape de nettoyage, exécutée en fin de job dans l'ordre inverse : c'est le Post Récupérer le code, où checkout retire les identifiants qu'il aurait laissés. Depuis la version 6, checkout ne les écrit plus dans .git/config mais dans un fichier séparé, référencé par la configuration Git ; avec persist-credentials: false, il ne les laisse pas du tout.

Ce que devient une étape run:

Le Worker n'interprète pas la commande : il l'écrit dans un fichier temporaire, sous $RUNNER_TEMP, et appelle un shell sur ce fichier. Le shell utilisé dépend de ce que vous avez écrit, et c'est un piège réel :

Ce que dit le workflowCommande exécutée sur Linux
rien (shell non précisé)bash -e {0}
shell: bashbash --noprofile --norc -eo pipefail {0}
shell: shsh -e {0}
shell: pythonpython {0}

{0} est remplacé par le chemin du fichier temporaire. La différence entre les deux premières lignes est l'option pipefail : sans elle, un tube renvoie le code de sortie de sa dernière commande. Démonstration, avec une étape qui lance un test en échec et garde une copie de la sortie avec tee :

$ cat etape.sh
python -m pytest -q test_rouge.py | tee rapport.txt
echo "étape terminée"
$ bash -e etape.sh | tail -3; echo "code de sortie : ${PIPESTATUS[0]}"
FAILED test_rouge.py::test_rouge - assert (1 + 1) == 3
1 failed in 0.01s
étape terminée
code de sortie : 0
$ bash --noprofile --norc -eo pipefail etape.sh | tail -3; echo "code de sortie : ${PIPESTATUS[0]}"
=========================== short test summary info ============================
FAILED test_rouge.py::test_rouge - assert (1 + 1) == 3
1 failed in 0.02s
code de sortie : 1

Avec le shell par défaut, le test échoue, tee réussit, et l'étape est verte. C'est exactement le pipeline « toujours vert » décrit dans le cours précédent, et il arrive ici sans aucun || true, simplement parce que shell: n'a pas été écrit. La parade tient en trois lignes, au niveau du workflow :

defaults:
  run:
    shell: bash

Toutes les étapes run: du workflow utilisent alors bash avec pipefail. Les options --noprofile --norc évitent en plus que des fichiers d'initialisation de l'image modifient l'environnement. Ajoutez ce bloc à ci.yml, juste après permissions: ; il fait partie du workflow de toutes les leçons suivantes.

Le check run

À la fin de chaque job, GitHub enregistre un check run sur le commit, via l'API Checks, avec le libellé du job (Lint et tests). C'est ce nom qu'une règle de protection de branche exige avant fusion. Conséquence directe : renommer un job casse la règle qui l'exige, la demande de fusion attend indéfiniment un check qui ne viendra plus. On choisit donc les libellés de jobs requis avec soin, et on les change en même temps que la règle.

Pièges courants

Le workflow ne se déclenche pas du tout. Vérifiez l'emplacement exact (.github/workflows/, au pluriel, à la racine du dépôt), l'extension (.yml ou .yaml), et que le fichier est bien sur la branche poussée. Un fichier invalide produit une exécution en erreur, pas une absence d'exécution : s'il n'y a rien du tout dans l'onglet Actions, c'est que l'événement ne correspond pas. Vérifiez aussi que les Actions ne sont pas désactivées dans les réglages du dépôt ou de l'organisation.

python-version: 3.10 installe Python 3.1. En YAML, 3.10 sans guillemets est un nombre décimal, que l'analyseur lit 3.1 :

$ python3 -c 'import yaml; print(yaml.safe_load("python-version: 3.10"))'
{'python-version': 3.1}

actionlint ne le signale pas : il ne connaît pas le sens de chaque entrée d'action. La règle est simple, toujours mettre les versions entre guillemets, y compris "3.14", qui fonctionne par chance.

on devient true. En YAML 1.1, on, yes et y sont des booléens. GitHub lit correctement la clé on: de ses workflows, mais un outil générique (un script Python qui analyse vos workflows, un générateur) verra la clé True :

$ python3 -c 'import yaml; print(list(yaml.safe_load("on:\n  push:\n").keys()))'
[True]

Si vous écrivez des outils autour des workflows, utilisez un analyseur YAML 1.2 ou prévoyez ce cas.

ubuntu-latest change sous vos pieds. ubuntu-latest désigne aujourd'hui Ubuntu 24.04, et GitHub a annoncé sa bascule vers Ubuntu 26.04 de façon progressive, entre le 19 octobre et le 19 novembre 2026. Pendant cette période, deux exécutions du même commit peuvent tourner sur deux systèmes différents, avec des versions de bibliothèques, de compilateurs et d'outils différentes. Un pipeline qui casse un lundi sans changement de code a souvent cette cause. Épinglez ubuntu-24.04, et passez à ubuntu-26.04 par un commit, testé, au moment choisi.

Une étape réussit alors qu'une commande a échoué. Voir pipefail plus haut. Autres variantes du même problème : une commande dans un sous-shell $( ) dont le code de sortie est ignoré, ou un script qui se termine par une commande qui réussit (echo "fini").

L'image du runner n'a pas votre outil, ou pas la bonne version. Les images hébergées sont mises à jour chaque semaine et les versions préinstallées changent. Un workflow qui dépend de « la version de Node présente sur l'image » est fragile ; installez explicitement ce dont vous dépendez (setup-python, setup-node, téléchargement d'une version fixée).

Error: Process completed with exit code 127. La commande n'a pas été trouvée : outil absent de l'image, étape d'installation sautée, ou PATH non mis à jour. Le code 127 est celui du shell pour « commande introuvable », pas une erreur de GitHub.

Sécurité

Le premier workflow pose déjà trois réflexes que la suite du cours approfondit :

  • Déclarer permissions:. Selon l'ancienneté du dépôt et de l'organisation, le GITHUB_TOKEN par défaut peut avoir des droits d'écriture sur le contenu, les demandes de fusion, les paquets. Les organisations, entreprises et comptes créés depuis février 2023 ont un jeton en lecture seule par défaut (les dépôts d'une organisation plus ancienne héritent de son réglage), mais un workflow ne doit pas dépendre d'un réglage qu'il ne voit pas. Une ligne suffit à rendre l'intention explicite et vérifiable.
  • persist-credentials: false quand le job ne pousse rien. Un identifiant laissé sur disque est lisible par toutes les étapes suivantes, y compris par une dépendance compromise que pip install aurait téléchargée.
  • Savoir ce que l'on exécute. uses: fait tourner sur le runner du code tiers, avec le jeton du job et les secrets qu'on lui passe. Pour l'instant nous n'utilisons que des actions publiées par GitHub (actions/*) ; la leçon 11 donne une méthode pour évaluer les autres.

Enfin, une poussée sur une branche exécute le workflow de cette branche : un collaborateur qui peut pousser peut modifier ce que fait le pipeline sur sa branche. C'est voulu (on doit pouvoir faire évoluer le pipeline par demande de fusion), et c'est pourquoi les secrets et les droits d'écriture sont réservés à des contextes protégés (leçons 9 et 11).

En production

Le coût. Les minutes de runner hébergé sont gratuites pour les dépôts publics. Pour les dépôts privés, le plan gratuit d'une organisation inclut 2 000 minutes par mois, puis la minute Linux à deux cœurs coûte 0,006 dollar depuis la baisse de prix de janvier 2026 (0,005 dollar pour Linux ARM, 0,062 dollar pour macOS). Chaque job est facturé à la minute entamée : dix jobs de vingt secondes coûtent dix minutes. Ce détail pousse à regrouper les vérifications très courtes dans un même job, et à réserver les jobs séparés à ce qui gagne réellement à être parallélisé.

ubuntu-slim. Pour des jobs légers (une vérification de format, un appel d'API, un commentaire sur une demande de fusion), GitHub propose ubuntu-slim : un processeur, 5 Go de mémoire, facturé 0,002 dollar la minute. C'est un conteneur non privilégié plutôt qu'une VM complète : peu d'outils préinstallés, pas de Docker, et une durée limitée à 15 minutes par job. Il convient aux tâches d'automatisation, pas aux constructions.

Les limites. Un job hébergé ne dépasse pas 6 heures, un run complet pas 35 jours (en comptant les attentes d'approbation), et une organisation au plan gratuit exécute au plus 20 jobs simultanément. Ces chiffres sont rarement atteints, sauf par les matrices (leçon 4).

Les plans et la protection des branches. Sur le plan gratuit, les règles qui exigent un check vert avant fusion (rulesets, branches protégées) ne sont disponibles que pour les dépôts publics. C'est le cas de l'organisation GitHub de Lyneko : sur ses dépôts privés, un check rouge s'affiche mais n'empêche pas la fusion. Le pipeline n'y est donc qu'un signal, et la discipline d'équipe (ne pas fusionner au rouge) fait le reste, ou il faut passer au plan Team. Ce genre d'arbitrage se fait en connaissance de cause, pas en le découvrant le jour où un commit rouge part en production.

Le workflow de ce site. Le site que vous lisez est publié par un workflow GitHub Actions : vérification éditoriale, construction d'une image, publication sur le registre Scaleway, puis écriture du tag dans le dépôt pour qu'Argo CD déploie. Il sert d'exemple réel à la leçon 9.

Exercices

1. Ajoutez le bloc defaults: run: shell: bash à votre workflow, puis écrivez une étape qui contient false | true. Prévoyez le résultat de l'étape avant de pousser, puis vérifiez. Que se passe-t-il si vous ajoutez shell: sh à cette seule étape ?

Solution

Avec defaults: run: shell: bash, l'étape est lancée avec -o pipefail : false | true renvoie le code de false, 1, et l'étape échoue. Avec shell: sh sur l'étape, la valeur de l'étape l'emporte sur la valeur par défaut du workflow : la commande devient sh -e {0}, sans pipefail, et l'étape réussit. Le réglage par défaut ne protège donc que les étapes qui ne le surchargent pas.

2. Un collègue propose runs-on: ubuntu-latest « pour avoir toujours les dernières mises à jour de sécurité ». Rédigez une réponse argumentée en trois points.

Solution
  1. Les VM hébergées sont reconstruites chaque semaine pour chaque étiquette : ubuntu-24.04 reçoit les correctifs de sécurité aussi bien que ubuntu-latest. L'étiquette ne choisit que la version majeure du système.
  2. ubuntu-latest change de version majeure sans commit (bascule vers 26.04 entre le 19 octobre et le 19 novembre 2026, de façon progressive) : deux exécutions du même commit peuvent donner deux résultats, ce qui casse la reproductibilité et brouille le diagnostic.
  3. Monter de version majeure reste nécessaire : on le fait par un commit qui change l'étiquette, testé sur une branche, à un moment choisi, et annulable par un simple revert.

3. Vous renommez le job verifier de Lint et tests en Vérifications. Sur un dépôt public où la branche main exige le check Lint et tests, que se passe-t-il pour les demandes de fusion ouvertes ? Comment faire le changement proprement ?

Solution

Les nouvelles exécutions publient un check Vérifications ; la règle attend toujours Lint et tests, qui ne viendra plus. Les demandes de fusion restent bloquées avec un check « en attente ». Proprement : ajouter Vérifications aux checks exigés, fusionner le renommage, puis retirer Lint et tests de la règle. Ou, plus simple, ne pas renommer les jobs exigés.

4. Sans pousser, trouvez avec actionlint les erreurs du workflow suivant, puis expliquez celle qu'actionlint ne trouve pas.

name: CI
on: [push, pull-request]
jobs:
  tests:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/setup-python@v7
        with:
          python-version: 3.10
      - run: pip install -r requirements-dev.txt && pytest
Solution

actionlint signale l'événement inconnu pull-request (le bon nom est pull_request, avec un tiret bas). Il ne signale ni python-version: 3.10, lu comme le nombre 3.1, ni l'absence de actions/checkout : le job s'exécute sur un répertoire vide, et pip échoue faute de requirements-dev.txt. actionlint vérifie la forme du workflow, pas sa logique.

5. Mesurez le coût mensuel, sur un dépôt privé, d'un workflow de trois jobs de 40 secondes chacun, déclenché 25 fois par jour ouvré (22 jours). Puis le coût du même travail regroupé en un seul job de 1 minute 30. Les 2 000 minutes incluses suffisent-elles dans chaque cas ?

Solution

Trois jobs de 40 secondes sont facturés trois minutes (minute entamée), soit 3 × 25 × 22 = 1 650 minutes par mois. Un seul job de 1 minute 30 est facturé 2 minutes, soit 2 × 25 × 22 = 1 100 minutes. Les deux tiennent dans les 2 000 minutes incluses, mais le premier en consomme 82 %, et un second workflow de même taille ferait dépasser le forfait : au-delà, 0,006 dollar la minute. Le regroupement fait gagner un tiers des minutes, au prix d'un retour moins détaillé (un seul check au lieu de trois).

Récapitulatif

  • Un workflow (.github/workflows/*.yml) réagit à des événements et exécute des jobs ; un job est une suite d'étapes sur une même machine, le runner ; une étape est une commande (run:) ou une action (uses:).
  • Le workflow est lu au commit qui a déclenché l'événement : le pipeline se versionne et se relit comme le code.
  • Chaque job hébergé a sa VM neuve, détruite ensuite : rien ne passe d'un job à l'autre sans mécanisme explicite.
  • Validez avant de pousser avec actionlint ; auditez avec zizmor.
  • Sans shell: bash, une étape s'exécute sans pipefail : posez defaults: run: shell: bash dans chaque workflow.
  • Mettez les versions entre guillemets, épinglez l'image du runner (ubuntu-24.04), bornez la durée (timeout-minutes), déclarez les permissions.
  • Le libellé d'un job est le nom de son check : le renommer casse les règles de protection qui l'exigent.

Pour aller plus loin

Voir ma constellation →

Sources