Chapitre 335 minIntermédiaire
GitHub Actions : écrire un workflow YAML
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire un workflow YAML valide dans .github/workflows ;
- choisir les bons déclencheurs, filtres et horaires cron ;
- enchaîner des jobs avec needs et des conditions if ;
- lire les contextes et transmettre des valeurs entre étapes et jobs ;
- éviter l'injection de script et limiter les droits du jeton.
5 exercices corrigés1 schéma
GitHub Actions est le service d'automatisation intégré à GitHub : un workflow, décrit dans un fichier YAML du dossier .github/workflows/, se déclenche sur un événement (un push, une pull request, un horaire, un clic) et exécute des jobs composés d'étapes sur des machines fournies par GitHub. Ce chapitre explique la syntaxe YAML, puis chaque partie d'un workflow : déclencheurs, jobs, étapes, expressions, variables et permissions. Il se termine par le premier workflow complet de sorbier-frais.
Où vivent les workflows
Un workflow est un fichier .yml ou .yaml placé dans .github/workflows/, à la racine du dépôt. Le nom du fichier est libre ; un dépôt peut en avoir autant qu'il veut, chacun avec ses déclencheurs.
sorbier-frais/
├── .github/
│ ├── workflows/
│ │ ├── ci.yml ← lint et tests à chaque pull request
│ │ ├── deploy.yml ← déploiement après fusion sur main
│ │ └── release.yml ← publication d'une version sur un tag
│ └── dependabot.yml
├── src/
├── tests/
├── package.json
└── package-lock.json
GitHub lit les workflows dans le commit concerné : un workflow ajouté sur une branche s'applique aux push de cette branche. Deux exceptions : les déclencheurs schedule (horaire) et workflow_dispatch (lancement manuel) ne fonctionnent qu'une fois le fichier présent sur la branche par défaut.
Le YAML en dix minutes
YAML est un format texte pour décrire des données. Il n'a que trois constructions : des dictionnaires (des paires clé: valeur), des listes (des éléments précédés de - ) et des valeurs simples (texte, nombre, booléen).
# Un commentaire commence par un dièse
name: CI # une clé et sa valeur texte
on: # une clé dont la valeur est un dictionnaire…
push: # …indenté de deux espaces
branches: [main] # une liste écrite sur une ligne
jobs:
tests:
runs-on: ubuntu-latest
steps: # une liste écrite sur plusieurs lignes
- run: npm ci
- run: npm test
Les règles qui causent presque toutes les erreurs :
| Règle | Correct | Faux |
|---|---|---|
| L'indentation se fait avec des espaces, jamais des tabulations | deux espaces par niveau | une tabulation : found character '\t' that cannot start any token |
| Un espace après les deux-points | runs-on: ubuntu-latest | runs-on:ubuntu-latest (lu comme un texte, pas une clé) |
| Un élément de liste commence par un tiret et un espace | - run: npm ci | -run: npm ci |
| Les éléments d'une même liste ont la même indentation | tous les - alignés | un - décalé d'un espace : il change de parent |
| Les versions avec un zéro final se mettent entre guillemets | python-version: '3.10' | python-version: 3.10, lu comme le nombre 3.1 |
Un texte qui commence par *, {, [, & ou !, ou qui contient : , se met entre guillemets | cron: '0 6 * * 1' | cron: 0 6 * * 1 est valide, mais - *.js ne l'est pas |
Pour une commande sur plusieurs lignes, le bloc littéral | garde les retours à la ligne :
- name: Préparer la base de test
run: |
mysql -h 127.0.0.1 -uroot -proot -e "CREATE DATABASE IF NOT EXISTS frais_test"
mysql -h 127.0.0.1 -uroot -proot frais_test < sql/schema.sql
Les listes et les dictionnaires ont aussi une forme compacte : branches: [main, develop] vaut une liste de deux éléments, with: { node-version: 24 } un dictionnaire d'une clé.
L'anatomie d'un workflow
Voici un workflow annoté. Chaque partie est détaillée dans la suite du chapitre.
name: CI # nom affiché dans l'onglet Actions
on: # 1. les déclencheurs
push:
branches: [main]
pull_request:
permissions: # 2. les droits du jeton GITHUB_TOKEN
contents: read
concurrency: # 3. une seule exécution à la fois par branche
group: ci-${{ github.ref }}
cancel-in-progress: true
env: # 4. des variables pour tous les jobs
NODE_VERSION: 24
jobs: # 5. les jobs, en parallèle par défaut
lint:
runs-on: ubuntu-latest # la machine
timeout-minutes: 10 # durée maximale du job
steps: # 6. les étapes, dans l'ordre
- uses: actions/checkout@v7 # une action réutilisable
- uses: actions/setup-node@v7
with: # ses paramètres
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci # une commande
- run: npm run lint
tests:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
- run: npm test
| Terme | Définition |
|---|---|
| Workflow | Un fichier YAML de .github/workflows/ ; une exécution (run) est un lancement de ce workflow |
| Événement | Ce qui déclenche le workflow : push, pull_request, schedule, workflow_dispatch… |
| Job | Un ensemble d'étapes exécutées sur une même machine ; les jobs d'un workflow tournent en parallèle, sauf si needs les enchaîne |
| Étape (step) | Une commande shell (run) ou une action (uses) ; les étapes d'un job s'exécutent dans l'ordre et partagent le même disque |
| Action | Un composant réutilisable publié dans un dépôt, comme actions/checkout |
| Runner | La machine qui exécute un job : une machine virtuelle neuve, détruite à la fin |
Les déclencheurs : on
| Événement | Il se produit quand | Usage typique |
|---|---|---|
push | Des commits ou un tag sont poussés | Vérifier main, publier sur un tag |
pull_request | Une pull request est ouverte, mise à jour ou rouverte | La CI avant fusion |
workflow_dispatch | Quelqu'un clique sur Run workflow (ou lance gh workflow run) | Un déploiement manuel, une tâche ponctuelle |
schedule | Une heure donnée, au format cron | Un audit des dépendances chaque nuit |
release | Une release GitHub est publiée | Construire et joindre les binaires |
workflow_call | Un autre workflow l'appelle | Un workflow réutilisable (chapitre 4) |
pull_request_target | Comme pull_request, mais exécuté avec les droits du dépôt cible | Cas très particuliers, dangereux (chapitre 7) |
Filtrer les branches, les tags et les chemins. Les filtres limitent les exécutions inutiles :
on:
push:
branches: [main, 'release/**'] # main et toutes les branches sous release/
tags: ['v*'] # les tags qui commencent par v : v1.0.0, v2.3.1
paths-ignore: # ne rien faire si seuls ces fichiers changent
- 'docs/**'
- '**.md'
pull_request:
branches: [main] # pour pull_request : la branche CIBLE de la fusion
* remplace n'importe quels caractères sauf /, ** traverse aussi les /. Pour pull_request, branches désigne la branche cible (celle dans laquelle on veut fusionner), pas la branche de travail. Quand un événement a à la fois un filtre branches et un filtre paths, les deux doivent correspondre.
Lancer à la main, avec des paramètres.
on:
workflow_dispatch:
inputs:
environnement:
description: Environnement cible
type: choice
options: [recette, production]
default: recette
dry-run:
description: Simuler sans déployer
type: boolean
default: false
Les valeurs se lisent avec ${{ inputs.environnement }} et ${{ inputs.dry-run }}.
Planifier avec cron. La syntaxe a cinq champs : minute, heure, jour du mois, mois, jour de la semaine (0 ou 7 = dimanche).
on:
schedule:
- cron: '30 5 * * 1-5' # à 5 h 30 UTC, du lundi au vendredi
Par défaut, l'heure est en UTC : 5 h 30 UTC, c'est 6 h 30 à Paris en hiver (UTC+1) et 7 h 30 en été (UTC+2). Depuis mars 2026, une clé timezone, placée à côté de cron, indique un fuseau horaire sous son nom officiel (Europe/Paris) : le workflow part alors à l'heure locale indiquée, toute l'année, changement d'heure compris.
on:
schedule:
- cron: '30 6 * * 1-5' # à 6 h 30, heure de Paris, du lundi au vendredi
timezone: Europe/Paris
L'intervalle minimal est de cinq minutes, l'exécution peut être retardée aux heures chargées, et sur un dépôt public sans activité pendant 60 jours, GitHub désactive les workflows planifiés.
Jobs, needs et conditions
Un job se déclare sous jobs, avec un identifiant (lint, tests, build) qui ne contient que des lettres, des chiffres, - et _.
| Clé | Rôle | Exemple |
|---|---|---|
runs-on | La machine | ubuntu-latest, windows-latest, macos-latest |
needs | Les jobs qui doivent réussir avant | needs: [lint, tests] |
if | Une condition pour lancer le job | if: github.ref == 'refs/heads/main' |
timeout-minutes | Durée maximale, 360 minutes par défaut | timeout-minutes: 15 |
env | Des variables pour toutes les étapes du job | env: { NODE_ENV: test } |
services | Des conteneurs annexes, comme une base de données | voir le chapitre 6 |
strategy | Une matrice de variantes | voir le chapitre 4 |
environment | L'environnement de déploiement | environment: production (chapitre 10) |
outputs | Des valeurs transmises aux jobs suivants | voir plus bas |
Sans needs, tous les jobs démarrent en même temps. Avec needs, ils forment un graphe : un job attend que tous ceux qu'il cite aient réussi. Si l'un échoue, les jobs qui en dépendent sont marqués skipped (ignorés).
jobs:
lint:
runs-on: ubuntu-latest
steps: [...]
tests:
runs-on: ubuntu-latest
steps: [...]
build:
needs: [lint, tests] # attend les deux
runs-on: ubuntu-latest
steps: [...]
deploy:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps: [...]
Dans cet extrait, [...] remplace les étapes pour la lisibilité : ce n'est pas un workflow valide tel quel. lint et tests démarrent ensemble ; build attend les deux ; deploy ne tourne qu'après un push sur main, jamais pour une pull request.
Les fonctions de statut. Par défaut, une étape ou un job ne s'exécute que si tout ce qui précède a réussi : c'est la condition implicite success(). Trois autres fonctions changent ce comportement :
| Condition | L'étape s'exécute… | Usage |
|---|---|---|
if: success() | si tout a réussi (par défaut) | le cas normal |
if: failure() | si une étape précédente a échoué | envoyer une alerte, publier les journaux d'erreur |
if: always() | dans tous les cas, même après une annulation | publier un rapport de tests, nettoyer |
if: cancelled() | si l'exécution a été annulée | rare |
continue-on-error: true sur une étape ou un job laisse le reste continuer même en cas d'échec. Réserve-le aux vérifications non bloquantes assumées (un test sur une version de Node.js expérimentale), jamais aux tests principaux.
Les étapes : run ou uses
Une étape fait soit une commande (run), soit un appel d'action (uses).
steps:
- name: Récupérer le code # nom affiché dans le journal
uses: actions/checkout@v7
- name: Installer Node.js
uses: actions/setup-node@v7
with: # paramètres de l'action
node-version: 24
cache: npm
- name: Lancer les tests
id: tests # identifiant, pour lire ses sorties
run: npm test -- --ci
env: # variables de cette étape
NODE_ENV: test
working-directory: ./api # dossier d'exécution
Les commandes run. Sous Linux et macOS, une étape run s'exécute avec bash -e : elle s'arrête à la première commande qui échoue. Avec shell: bash écrit explicitement, GitHub ajoute -o pipefail : une commande en échec au milieu d'un tube (npm test | tee resultat.txt) fait aussi échouer l'étape. Sous Windows, le shell par défaut est PowerShell (pwsh).
Les actions uses. La référence a la forme propriétaire/dépôt@version :
| Référence | Ce qu'elle désigne | Sûreté |
|---|---|---|
actions/checkout@v7 | La dernière version 7.x : le tag v7 est déplacé à chaque correctif | Pratique ; acceptable pour les actions officielles de GitHub |
actions/checkout@v7.0.1 | Une version précise | Le tag peut encore, en théorie, être déplacé |
actions/checkout@<empreinte de 40 caractères> | Un commit précis | Immuable : la référence la plus sûre, recommandée pour les actions tierces |
./.github/actions/preparer | Une action définie dans ton dépôt | Sous ton contrôle |
Les actions se trouvent sur le GitHub Marketplace. Avant d'en utiliser une, regarde qui la publie (badge Verified creator), si elle est maintenue, et préfère les actions officielles (actions/…, github/…, docker/…). Une action exécute son code avec accès à ton dépôt et à tes secrets (chapitre 7).
Expressions et contextes
Tout ce qui est écrit entre ${{ et }} est une expression, évaluée par GitHub avant l'exécution. Elle lit des contextes, des objets qui décrivent l'exécution en cours.
| Contexte | Contenu | Exemple |
|---|---|---|
github | L'événement, le dépôt, la branche, le commit | github.ref_name (main), github.sha, github.event_name, github.actor |
env | Les variables définies avec env: | env.NODE_VERSION |
vars | Les variables de configuration du dépôt ou de l'environnement | vars.URL_RECETTE (chapitre 8) |
secrets | Les secrets | secrets.DEPLOY_SSH_KEY |
inputs | Les paramètres d'un workflow_dispatch ou d'un workflow_call | inputs.environnement |
matrix | La combinaison en cours d'une matrice | matrix.node |
steps | Les sorties et le résultat des étapes précédentes du job | steps.tests.outcome, steps.version.outputs.numero |
needs | Les sorties et le résultat des jobs cités dans needs | needs.build.outputs.image |
runner | La machine | runner.os (Linux, Windows, macOS), runner.temp |
Quelques fonctions utiles : contains(github.event.head_commit.message, '[skip deploy]'), startsWith(github.ref, 'refs/tags/v'), format('sorbier-frais:{0}', github.sha), hashFiles('package-lock.json') (une empreinte du fichier, utilisée pour les caches).
Dans un if, les accolades sont facultatives : if: github.ref == 'refs/heads/main' et if: ${{ github.ref == 'refs/heads/main' }} sont équivalents.
GitHub définit aussi des variables d'environnement sur chaque runner, lisibles dans les commandes : GITHUB_SHA (le commit), GITHUB_REF_NAME (la branche ou le tag), GITHUB_REPOSITORY (sorbier/sorbier-frais), GITHUB_WORKSPACE (le dossier du code), RUNNER_OS, et CI=true, que beaucoup d'outils détectent pour passer en mode non interactif.
Transmettre des données entre étapes et entre jobs
Trois fichiers spéciaux, dont le chemin est donné par des variables, permettent à une étape de communiquer :
| Fichier | Effet | Exemple |
|---|---|---|
$GITHUB_ENV | Définit une variable d'environnement pour les étapes suivantes du job | echo "DATE_BUILD=$(date -u +%F)" >> "$GITHUB_ENV" |
$GITHUB_OUTPUT | Définit une sortie de l'étape, lisible par steps.<id>.outputs.<nom> | echo "numero=2.4.0" >> "$GITHUB_OUTPUT" |
$GITHUB_STEP_SUMMARY | Ajoute du Markdown au résumé affiché sur la page de l'exécution | echo "### 42 tests réussis" >> "$GITHUB_STEP_SUMMARY" |
Pour passer une valeur à un autre job, on la déclare comme sortie du job :
jobs:
version:
runs-on: ubuntu-latest
outputs:
numero: ${{ steps.lire.outputs.numero }} # sortie du job
steps:
- uses: actions/checkout@v7
- id: lire
run: echo "numero=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
afficher:
needs: version
runs-on: ubuntu-latest
steps:
- run: echo "Version à livrer = ${{ needs.version.outputs.numero }}"
Permissions et GITHUB_TOKEN
À chaque exécution, GitHub crée un jeton temporaire, GITHUB_TOKEN, qui permet au workflow d'agir sur le dépôt : lire le code, commenter une pull request, publier un paquet. Il expire à la fin du job. Ses droits se règlent avec permissions, au niveau du workflow ou d'un job :
permissions:
contents: read # lire le code : suffit pour une CI
jobs:
publier-image:
permissions:
contents: read
packages: write # pousser une image dans le registre de GitHub
runs-on: ubuntu-latest
steps: [...]
Dès que tu écris un bloc permissions, tout ce qui n'y figure pas passe à none. Donne le minimum à chaque job : si une action malveillante s'exécute, elle ne pourra faire que ce que le jeton permet (chapitre 8).
Éviter les exécutions en double : concurrency
Hugo pousse trois commits en deux minutes : sans précaution, trois pipelines complets tournent, dont deux déjà obsolètes. concurrency regroupe les exécutions et annule les anciennes :
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }} # un groupe par workflow et par branche
cancel-in-progress: true # le nouveau lancement annule l'ancien
Pour un workflow de déploiement, garde cancel-in-progress: false : on ne coupe pas un déploiement au milieu, on attend qu'il finisse avant de lancer le suivant.
Les runners hébergés par GitHub et leur coût
Étiquette runs-on | Système | Quand l'utiliser |
|---|---|---|
ubuntu-latest | La dernière version d'Ubuntu proposée par GitHub | Presque tout : Node.js, PHP, Python, Java, .NET, Docker |
windows-latest | Windows Server | Applications de bureau .NET (WinForms, WPF), scripts PowerShell spécifiques |
macos-latest | macOS | Applications iOS et macOS |
Les étiquettes -latest changent de version quand GitHub met à jour ses images. Pour figer le système, utilise une étiquette numérotée (comme ubuntu-24.04) ; chaque image liste ses logiciels préinstallés dans le dépôt actions/runner-images.
Le coût : sur un dépôt public, les runners standards sont gratuits et illimités. Sur un dépôt privé, l'offre gratuite inclut chaque mois un crédit de calcul équivalent à environ 2 000 minutes Linux, et 500 Mo de stockage pour les artefacts. Une minute Windows coûte environ 1,7 fois une minute Linux, une minute macOS environ 10 fois : reste sur Linux quand le projet le permet. Le GitHub Student Developer Pack donne aux étudiants l'offre GitHub Pro, plus généreuse.
Suivre une exécution
L'onglet Actions du dépôt liste les exécutions de chaque workflow avec leur statut :
| Statut | Signification |
|---|---|
| En file d'attente (queued) | En attente d'un runner libre |
| En cours (in progress) | Une étape s'exécute ; son journal défile en direct |
| Réussi (success), coche verte | Toutes les étapes ont réussi |
| Échec (failure), croix rouge | Une étape a renvoyé un code d'erreur |
| Annulé (cancelled) | Annulé à la main ou par concurrency |
| Ignoré (skipped) | Un if était faux, ou un job de needs a échoué |
Clique sur un job pour déplier chaque étape et lire sa sortie. Le bouton Re-run jobs relance un job sans nouveau commit (Re-run failed jobs ne relance que ceux qui ont échoué), et la case Enable debug logging relance avec un journal détaillé. Le même suivi existe en ligne de commande avec l'outil gh : gh run list, gh run watch, gh run view --log-failed.
Le premier workflow complet de sorbier-frais
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint et formatage
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx prettier --check .
- run: npm run lint
tests:
name: Tests unitaires
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- name: Lancer les tests
run: npm test -- --ci
- name: Résumé
if: always()
run: echo "Tests terminés pour le commit $GITHUB_SHA" >> "$GITHUB_STEP_SUMMARY"
lint et tests tournent en parallèle ; une pull request n'est fusionnable que si les deux sont verts, une fois la règle de protection configurée (chapitre 6). npm test -- --ci transmet l'option --ci à Jest, qui refuse alors d'écrire de nouveaux instantanés de test au lieu de les créer en silence.
Exercices
Exercice 1 sur 5 Écrire les déclencheurs Réussi À revoir
Écris le bloc on d'un workflow qui doit tourner :
- à chaque pull request vers
main; - à chaque
pushsurmain, sauf si seuls des fichiers Markdown ont changé ; - chaque lundi à 6 h 00 heure de Paris en hiver ;
- quand on clique sur Run workflow.
Voir la réponseMasquer la réponse de l'exercice 1
on:
pull_request:
branches: [main]
push:
branches: [main]
paths-ignore:
- '**.md'
schedule:
- cron: '0 5 * * 1'
workflow_dispatch:
Paris est à UTC+1 en hiver : 6 h 00 à Paris = 5 h 00 UTC. Le cinquième champ vaut 1 pour lundi. Sans timezone, cron compte en UTC : en été, ce workflow tournera à 7 h 00 heure de Paris. Mieux, depuis mars 2026 : - cron: '0 6 * * 1' suivi de timezone: Europe/Paris sur la ligne suivante, qui tourne à 6 h 00 à Paris toute l'année.
Tu avais trouvé ?
Exercice 2 sur 5 Corriger un workflow invalide Réussi À revoir
GitHub refuse ce workflow. Trouve les quatre erreurs.
name: CI
on:
push:
branches: main
jobs:
tests:
runs-on:ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: 3.10
- run: pip install -r requirements.txt
-run: pytest
Voir la réponseMasquer la réponse de l'exercice 2
runs-on:ubuntu-latest: il manque l'espace après les deux-points, la ligne n'est pas lue comme une clé. Écrireruns-on: ubuntu-latest.python-version: 3.10est lu comme le nombre 3.1 : GitHub installerait Python 3.1, qui n'existe pas dans le catalogue, et l'étape échouerait. Écrirepython-version: '3.10'.- run: pip install…est décalé d'un espace par rapport aux autres éléments de la liste : YAML signale une indentation incohérente.-run: pytest: il manque l'espace après le tiret. Écrire- run: pytest.
branches: main (sans crochets) n'est pas une erreur : GitHub accepte un texte seul à la place d'une liste d'un élément.
Tu avais trouvé ?
Exercice 3 sur 5 Quels jobs tournent ? Réussi À revoir
on:
push:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps: [...]
tests:
runs-on: ubuntu-latest
steps: [...]
build:
needs: [lint, tests]
runs-on: ubuntu-latest
steps: [...]
deploy:
needs: build
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps: [...]
Indique quels jobs s'exécutent, et dans quel ordre, dans chaque cas :
- Lina ouvre une pull request ; tout réussit.
- La pull request est fusionnée dans
main; tout réussit. - Après une fusion dans
main, le jobtestséchoue.
Voir la réponseMasquer la réponse de l'exercice 3
lintettestsen parallèle, puisbuild.deployest ignoré : l'événement estpull_request, paspush.- La fusion crée un
pushsurmain:lintettestsen parallèle, puisbuild, puisdeploy. lintréussit,testséchoue ;buildest ignoré (un job deneedsa échoué) etdeployaussi, puisqu'il dépend debuild. Rien n'est déployé.
Tu avais trouvé ?
Exercice 4 sur 5 Passer une valeur d'un job à l'autre Réussi À revoir
Le job build calcule l'étiquette de l'image Docker : sorbier-frais: suivi des sept premiers caractères du commit. Le job deploy doit l'afficher. Complète les deux jobs.
jobs:
build:
runs-on: ubuntu-latest
# à compléter
deploy:
needs: build
runs-on: ubuntu-latest
# à compléter
Voir la réponseMasquer la réponse de l'exercice 4
jobs:
build:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.etiquette.outputs.image }}
steps:
- id: etiquette
run: echo "image=sorbier-frais:${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Image à déployer = ${{ needs.build.outputs.image }}"
${GITHUB_SHA::7} est une syntaxe de Bash qui garde les sept premiers caractères de la variable. La valeur passe par trois étapes : écrite dans $GITHUB_OUTPUT par l'étape etiquette, exposée comme sortie du job par outputs, lue par needs.build.outputs.image.
Tu avais trouvé ?
Exercice 5 sur 5 Repérer l'injection Réussi À revoir
Un workflow déclenché par issues (l'ouverture d'un ticket) contient cette étape :
- run: echo "Nouveau ticket = ${{ github.event.issue.title }}" >> tickets.log
- Pourquoi est-ce dangereux ?
- Réécris l'étape de façon sûre.
Voir la réponseMasquer la réponse de l'exercice 5
- N'importe quel visiteur peut ouvrir un ticket et choisir son titre. L'expression est remplacée dans le texte du script avant l'exécution : un titre contenant des guillemets et des commandes shell ferait exécuter ces commandes sur le runner, avec le jeton du workflow. C'est une injection de script.
- On passe le titre par une variable d'environnement, que le shell traite comme une donnée :
- env:
TITRE: ${{ github.event.issue.title }}
run: echo "Nouveau ticket = $TITRE" >> tickets.log
Tu avais trouvé ?