GitHub Actions, c'est du CI collé au code. Vous déposez du YAML sous .github/workflows/, vous poussez, et des runners prennent les jobs sur les événements que vous choisissez : pull request, push sur main, un schedule, ou un clic manuel.
Ce billet donne le modèle mental minimal plus les briques qui existent vraiment en production : workflows, jobs, steps, cache, secrets, matrix builds, et les erreurs qui font mentir les checks verts ou allongent chaque run à 12 minutes.
Pas de visite du marketplace. Pas de checklist "activez toutes les features". Un chemin CI que vous pouvez copier et durcir.
Ce que vous configurez vraiment
Trois couches :
| Couche | Rôle | Fichier / endroit |
|---|---|---|
| Workflow | Pipeline nommé déclenché par des événements | .github/workflows/*.yml |
| Job | Unité de travail sur un runner (VM ou conteneur) | jobs.<id>: |
| Step | Commande shell ou Action réutilisable | sous steps: |
Un workflow peut lancer plusieurs jobs. Les jobs tournent en parallèle ou s'attendent avec needs. Les steps d'un job s'exécutent toujours dans l'ordre sur la même machine, donc les suivants voient fichiers et env des précédents.
C'est pour ça qu'on met "checkout, puis install, puis test" dans un seul job, pas dans trois jobs qui repartent à froid.
Le plus petit workflow utile
Créez .github/workflows/ci.yml :
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
- name: Install
run: npm ci
- name: Test
run: npm test
Ce que ça fait :
- Tourne sur chaque PR et chaque push vers
main. - Démarre un runner Ubuntu neuf.
- Checkout le dépôt au commit sous test.
- Installe Node 20 et restaure un cache npm si possible.
- Installe les deps avec
npm ci(strict sur le lockfile) et lance les tests.
Si le step de test sort non zéro, le job échoue et le check de la PR échoue. C'est tout le contrat.
Triggers : on sans le bruit
Événements courants :
on:
pull_request:
paths:
- "src/**"
- "package-lock.json"
- ".github/workflows/ci.yml"
push:
branches: [main]
workflow_dispatch: # "Run workflow" manuel dans l'UI
schedule:
- cron: "0 6 * * 1" # lundis 06:00 UTC
Notes qui évitent des re-runs inutiles :
- Les filtres
pathssautent le workflow quand le changement est purement docs (ou ce que vous excluez). Des globs ratés répondent souvent à "pourquoi le CI n'a pas tourné ?" workflow_dispatchpermet de relancer après correction de secrets sans commit factice.schedulene tourne que sur la branche par défaut, et GitHub peut retarder les crons peu prioritaires sous charge. N'utilisez pas le cron seul pour "doit tourner avant chaque release."- Les PR de forks ont un accès limité aux secrets. C'est voulu. Traitez-le comme frontière de sécurité, pas comme bug.
Jobs, runners et needs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run lint
test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
needs: lint fait attendre test jusqu'au succès de lint. Si lint échoue, test et build sont sautés (sauf if: contraire).
Chaque job est une nouvelle machine. Pas de filesystem partagé entre lint et test sauf artifacts ou réinstall. D'où les graphes multi-job naïfs qui réinstallent trois fois.
Quand découper les jobs :
- OS ou versions de langage différents (matrix).
- Suites lentes indépendantes à paralléliser.
- Un job de deploy qui ne doit pas tourner avant les tests, avec secrets en plus.
Quand garder un seul job :
- Petits dépôts où l'install domine le wall-clock.
- Steps qui partagent un cache disque local (node_modules, target/, .venv).
Steps : run vs uses
run:shell sur le runner. Sous Linux, bash par défaut. Pour les scripts du projet déjà de confiance.uses:une Action publiée (ou locale./.github/actions/...). Préférez des pins de version (@v4ou un SHA complet sur les chemins sensibles).
steps:
- uses: actions/checkout@v4
- name: Run unit tests
run: |
set -euo pipefail
npm ci
npm test -- --coverage
actions/checkout est presque toujours le premier step. Sans lui, le workspace du runner est vide.
Pour les Actions tierces qui touchent secrets ou credentials de deploy, pinez un SHA de commit et lisez le code de l'Action. Les tags bougent.
Cache des dépendances
Le cache évite de payer l'install complet à chaque run. Préférez le cache intégré des setup Actions quand il existe :
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm # package-lock.json comme graine de clé
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip # requirements.txt / pyproject selon config
- uses: actions/setup-go@v5
with:
go-version: "1.22"
cache: true
Cache manuel si besoin :
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
Règles de base :
- La clé doit changer quand le lockfile change. Une clé périmée installe le mauvais arbre ou saute des updates.
- Le cache est best-effort. Un miss est normal après 7 jours d'inactivité ou avec une nouvelle clé. Le job doit marcher à froid.
- Ne cachez pas secrets ni sorties de build que vous ne mettriez pas en artifact de PR. Le cache sert aux deps et toolchains, pas à "sauter les tests si on a le binaire d'hier."
- Restore-keys sont des préfixes : un hit partiel restaure un cache plus vieux puis empile dessus. Ça accélère l'install, mais peut masquer des erreurs de lockfile si l'install n'est pas strict.
Les artifacts sont autre chose : actions/upload-artifact / download-artifact passent des sorties entre jobs du même run. Le cache traverse les runs ; les artifacts restent dans un run (avec la rétention configurée).
Secrets et variables d'environnement
Les secrets repo ou org vivent dans les réglages GitHub. Les workflows les lisent ainsi :
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # optionnel : règles de protection, secrets par env
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
APP_ENV: production
run: ./scripts/deploy.sh
Règles dures :
- N'écrivez jamais les secrets dans les logs. GitHub masque les valeurs de secrets connues, pas chaque dérivé. Afficher
API_TOKENen base64 ou un sous-string peut fuiter. - Ne commitez pas de secrets "juste pour le CI" dans le dépôt ou le YAML.
pull_requestdepuis un fork n'a pas les mêmes droits write secrets quepull_request_target. Préférezpull_requestpour le code non de confiance. N'utilisezpull_request_targetque si vous comprenez checkout et privilèges ; le mauvais usage est un classique de la supply chain.- OIDC + rôles cloud (AWS, GCP, Azure) battent les clés longue durée quand c'est possible. Des tokens courts via
permissions+ fédération laissent moins de résidu. - Bornez
permissions:en tête de workflow ou par job. Les droits par défaut du token se sont resserrés ; déclarez quand même le besoin :
permissions:
contents: read
pull-requests: write # seulement si un bot doit commenter
Matrix builds
La matrix décline une définition de job en plusieurs runs :
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: ["18", "20", "22"]
exclude:
- os: windows-latest
node: "18"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
Points de vigilance :
- Coût et file d'attente suivent le produit des dimensions. Trois OS fois quatre versions = douze jobs.
fail-fast: true(défaut) annule les frères quand une cellule échoue. Mettezfalsepour voir tout le tableau d'échecs sur une PR.include/excludegardent la grille honnête sans lister chaque combinaison à la main.- Différences de chemins et de shell par OS apparaissent d'abord ici (
\vs/, PowerShell vs bash). Préférez des scripts projet qui l'abstraient.
Un exemple multi-langages plus serré
Service avec front Node et tests Python :
name: CI
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: web/package-lock.json
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: api
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: api/requirements.txt
- run: pip install -r requirements.txt
- run: pytest -q
docker:
runs-on: ubuntu-latest
needs: [frontend, backend]
steps:
- uses: actions/checkout@v4
- run: docker build -t app:ci .
concurrency annule un run en cours sur la même branche quand vous poussez à nouveau. Ça tient la facture de minutes Actions et la confusion "quel run est le dernier ?"
Pièges fréquents (les chers)
1. Tester un autre commit que celui que vous croyez
Checkout toujours le head de la PR pour du CI de PR non de confiance. Un ref mal configuré (ou pull_request_target avec le merge ref à la légère) peut exécuter un workflow de confiance sur du code contrôlé par un attaquant. actions/checkout@v4 par défaut sur pull_request est la base sûre pour la plupart des équipes.
2. npm install au lieu de npm ci en CI
npm install peut réécrire le comportement du lockfile et diverger de ce que les devs ont installé. Le CI doit suivre le chemin strict (npm ci, pnpm install --frozen-lockfile, yarn install --immutable, etc.).
3. Cacher node_modules directement
Ça marche jusqu'au jour où ça casse (deps optionnelles, addons natifs, biais d'OS). Préférez le store du package manager + install propre. Le cache: npm de setup-node est le défaut ennuyeux et correct pour npm.
4. Secrets dans des steps run: echo de "debug"
Quelqu'un laisse echo "token=$API_TOKEN" "cinq minutes." Les logs survivent à la PR. Déboguez en local ou partez du principe que les logs sont lisibles pour qui a le droit de lecture du run.
5. Pas de concurrency, deploys qui se chevauchent
Deux push sur main peuvent déployer dans le désordre. Groupes de concurrency sur les workflows de deploy, ou environment avec reviewers / timers.
6. Explosion de matrix sans fail-fast: false en debug
Vous voyez une croix rouge, les frères sont annulés, et vous ratez Windows+Node 22 aussi cassé. Basculez fail-fast le temps de stabiliser ; remettez-le quand la suite est fiable.
7. Se fier aux tags latest pour des Actions critiques
uses: some-org/some-action@main peut changer un mardi. Pinez au moins les majors (@v4). Pinez des SHA pour deploy et release.
8. Oublier les required checks sur les branches protégées
Le workflow existe, la PR merge avec une croix rouge ignorée. Branch protection (ou rulesets) doit exiger les noms de job qui comptent. Renommez un job sans mettre à jour le check requis et les merges passent libres.
9. Surprise de permissions GITHUB_TOKEN
Un step qui ouvre une issue ou pousse un tag échoue en 403 après un changement de défauts. permissions explicites, et PAT ou GitHub App seulement si le token par défaut ne suffit pas.
10. Tests flaky accusés sur Actions
Relancer tout le workflow cache des bugs produit. Corrigez les flakes ou mettez-les en quarantaine listée. "Re-run jobs" en boucle n'est pas une stratégie de stabilité.
Expressions, contexts et if
- name: Publish
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./scripts/publish.sh
Contexts utiles : github, env, secrets, matrix, needs, runner, steps.
- name: Upload coverage
if: always() && steps.test.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
if: always() tourne même si un step précédent a échoué (toujours soumis à l'annulation du job). À utiliser avec parcimonie pour cleanup ou uploads vraiment voulus en échec.
Itérer en local sans brûler des minutes
- Lancez les mêmes scripts qu'Actions :
npm ci && npm test. Si ça casse en local, ça cassera en CI. - Act approxime les workflows avec Docker. Ce n'est pas identique aux runners hébergés GitHub (images, logiciels, réseau).
- Utilisez
workflow_dispatchavec inputs pour tester un deploy staging sans merger.
on:
workflow_dispatch:
inputs:
target:
description: "deploy target"
required: true
default: staging
Checklist d'un défaut raisonnable
Avant de dire que le CI est "fini" :
- Workflow sur
pull_requestetpushvers la branche par défaut. - Installs stricts au lockfile.
- Setup Action du langage avec cache intégré.
permissions: contents: readexplicite (ajoutez le reste seulement si besoin).concurrencysur les branches de PR.- Required status checks sur branches protégées avec les noms exacts de job.
- Secrets uniquement via
${{ secrets.* }}/ OIDC, jamais dans git. - Majors pinés pour les Actions first-party ; SHA pour le deploy.
- Un chemin documenté "re-run / manual dispatch" pour les releases.
- Budget temps : si install+test dépasse ~10 minutes, profilez avant d'ajouter des cellules matrix.
À quoi ressemble le succès
Une PR s'ouvre, les checks démarrent en moins d'une minute, l'install est surtout des cache hits, les tests collent à ce que les devs lancent, et une croix rouge veut dire "ce commit est faux," pas "le runner a eu une mauvaise journée" ni "on a testé le mauvais ref."
Parte de monojob Node (ou Python) ci-dessus. Ajoutez la matrix quand vous supportez plusieurs runtimes. Découpez les jobs quand le wall-clock parallèle ou l'isolement du deploy valent le coût de réinstall. Cachez les deps, pas les excuses.
Quand c'est vert et que vous ne faites toujours pas confiance, le bug est presque toujours dans ce que vous lancez ou quel commit vous lancez, pas dans l'indentation YAML.
