Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

14.1 — Pipelines CI/CD

🎯 Objectif : qu’un push sur main déclenche tout — typecheck, lint, tests, build, scan sécu, déploiement — sans intervention humaine. Et qu’une PR génère un environnement de preview pour relire visuellement.

À l'issue de cet axe, tu sauras :

  • Distinguer Continuous Integration, Delivery et Deployment
  • Construire un workflow GitHub Actions multi-jobs (lint/test/build/deploy)
  • Mettre en place un environnement éphémère par PR (preview)
  • Sécuriser les secrets via OIDC plutôt que des tokens longs vivent
  • Optimiser le temps d'exécution avec cache, matrice et reusable workflows

Confirmé 10 min prérequis : axes 4 et 8 lus

Les acronymes se mélangent. Définitions claires :

SiglePhrase qui résume
CI — Continuous IntegrationChaque push est testé automatiquement
CD — Continuous DeliveryChaque commit prêt à partir en prod, déclenchement manuel
CD — Continuous DeploymentChaque commit qui passe la CI part automatiquement en prod

Tu peux faire CI sans CD. Tu ne peux pas faire CD sans CI.

La majorité des équipes B2B font Continuous Delivery : pipeline auto, dernier clic manuel pour la prod. Les équipes mûres (Deezer, Etsy, GitLab) font Continuous Deployment : 50+ déploiements/jour.


push ┐
┌──────────┐ → ┌──────┐ → ┌─────┐ → ┌───────┐ → ┌─────┐ → ┌────────┐
│ checkout │ │ lint │ │test │ │ build │ │scan │ │ deploy │
└──────────┘ └──────┘ └─────┘ └───────┘ └─────┘ └────────┘
│ │
└─ artifact ┘
ÉtapeQuoiOutils typiques
CheckoutCloner le codeactions/checkout
SetupRuntime + cacheactions/setup-node, setup-python, setup-go
LintStyle, conventionsESLint v9, Biome, Ruff, golangci-lint
TypecheckCohérence typestsc --noEmit, mypy, phpstan
TestUnitaires + intégrationVitest, pytest, Pest, Playwright
BuildBundle / imageVite, Next, Docker, esbuild
Scan sécuVulnérabilités, secretsnpm audit, Snyk, Trivy, gitleaks
DeployPousser l’artefactVercel CLI, Fly CLI, kubectl, AWS CLI, Cloudflare Wrangler

Règle d’or : chaque étape doit être reproductible localement. Si seule la CI sait builder, tu as un problème.


name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --reporter=verbose

Trois mécaniques importantes :

  1. concurrency + cancel-in-progress : si tu pushes 2× de suite, le 1ᵉʳ run est annulé.
  2. cache: npm intégré à setup-node : restore + save le node_modules cache.
  3. timeout-minutes : tu paies à la minute, plafonne tes runs.
jobs:
lint:
runs-on: ubuntu-latest
steps: [...]
test:
runs-on: ubuntu-latest
steps: [...]
build:
needs: [lint, test] # ne tourne que si lint ET test ont réussi
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 24, cache: npm }
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- run: ./deploy.sh

Architecture typique :

  • Parallèle ce qui peut l’être (lint, typecheck, test).
  • Séquentiel sur la chaîne de dépendances (build après tests).
  • Conditionnel sur main pour le deploy.

Tester sur plusieurs Node, Postgres, OS :

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node }}, cache: npm }
- run: npm ci && npm test

fail-fast: false = on continue les autres combos même si un échoue (utile pour debug).

jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_PASSWORD: testpw
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready"
--health-interval 5s
--health-timeout 3s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:testpw@localhost:5432/postgres
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run db:migrate && npm test

Sans cache, chaque run réinstalle tout = 1-3 min perdues. Avec cache :

TypeActionGain
node_modulessetup-node avec cache: npm-60 à -90 %
.next/cache (Next.js build)actions/cache@v4-30 à -70 % du build
Docker layersdocker/build-push-action avec cache-from/cache-to-50 à -80 %
pip / pnpm / bunsetup-python / setup-pnpm / setup-bunidem
- uses: actions/cache@v4
with:
path: |
.next/cache
~/.cache/pip
key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-

env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Problème : le token vit indéfiniment dans GitHub Secrets, et un commit malveillant dans une PR peut tenter de l’exfiltrer.

GitHub Actions signe un JWT court vivant prouvant « ce job vient bien du repo org/repo sur la branche main ». Ton cloud (AWS, GCP, Azure) le valide et délivre des credentials temporaires (15 min).

permissions:
id-token: write # nécessaire pour OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/gha-deployer
aws-region: eu-west-3
- run: aws s3 sync ./dist s3://my-bucket

Avantages :

  • Aucun secret stocké côté GitHub.
  • Credentials éphémères (impossible à exfiltrer durablement).
  • Restriction par branche côté IAM (rôle utilisable seulement depuis main).

Disponible sur AWS, GCP, Azure, HashiCorp Vault, Cloudflare. À privilégier en 2026.


L’idée : chaque PR a sa propre URL de preview. Les reviewers cliquent et voient.

Aucun workflow à écrire — c’est natif. Connecte le repo, à chaque PR tu reçois https://my-app-pr-42.vercel.app.

Idem en 1 clic via leurs intégrations GitHub.

Plus complexe : un job CI déploie un namespace dédié pr-42, post-déploiement il commente la PR avec l’URL :

- name: Deploy preview
run: helmfile apply --namespace=pr-${{ github.event.number }}
- name: Comment URL on PR
uses: thollander/actions-comment-pull-request@v3
with:
message: '🚀 Preview: https://pr-${{ github.event.number }}.preview.example.com'

À la fermeture de la PR, un workflow closed détruit le namespace.


Quand 5 services partagent les mêmes étapes, factorise :

.github/workflows/_reusable-test.yml
on:
workflow_call:
inputs:
node-version: { type: string, default: '24' }
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ inputs.node-version }}, cache: npm }
- run: npm ci && npm test
# Dans un autre workflow
jobs:
test:
uses: ./.github/workflows/_reusable-test.yml
with: { node-version: '24' }

Avec paths ou un outil comme Turborepo / Nx, tu n’exécutes que les jobs touchés :

on:
push:
paths:
- 'apps/web/**'
- 'packages/ui/**'
- '.github/workflows/web.yml'

Pour aller plus loin, Turborepo Remote Cache partage les artefacts entre devs et CI : si un build a déjà tourné quelque part, on télécharge le résultat plutôt que de le rejouer.


RisqueParade
Action tierce malveillantePin sur SHA : actions/checkout@8f4b... plutôt que @v4 (immutable)
Workflow injecté via PR forkéepull_request_target à éviter sauf cas précis
Secret leakage dans logsmask:: automatique, mais ne jamais echo $SECRET
Élévation de privilègespermissions: minimum (contents: read par défaut)
Compromission du compte GitHub d’un dev2FA obligatoire org-wide, branch protection + required reviews
Action publique abandonnéeAudit régulier des SHA pinned, Dependabot Actions
permissions:
contents: read # default minimal
id-token: write # explicite si OIDC

  • CI verte = code parfait : non, juste « pas pire que la barre actuelle ».
  • Tests qui flake : tolérer un flake = enseigner aux devs à ignorer la CI. Quarantaine ou fix.
  • Pipeline > 15 min : les devs n’attendent plus, ils mergent à l’aveugle.
  • Tout dans un seul job : pas de parallélisme, pas de réutilisation.
  • if: always() partout : masque les vrais problèmes.
  • Aucun cache : 2× plus de minutes facturées.

Tu pushes 3 commits coup sur coup sur la même branche. Sans concurrency, combien de runs vont s'exécuter en parallèle ?
Tu déploies vers AWS depuis GitHub Actions. Quelle approche est la plus sûre en 2026 ?
Ton job test prend 8 minutes : 6 minutes de `npm ci`, 2 minutes de tests. Quel est le levier le plus impactant ?
Sur une PR forkée, GitHub envoie-t-il les secrets ?


Suite : 14.2 — Infrastructure as Code pour provisionner et versionner ton infra.