Cours NestJS · Module 12: Conteneurs et CI/CD

CI/CD avancé - matrix builds, artifacts et cache

6 min de lecture
Dans cette leçon6

Les tests passent chez toi. Pas chez ton collègue - il a une autre version de Node. Pas sur le serveur non plus, parce que quelqu'un a oublié de reconstruire l'image. Chacune de ces situations coûte une demi-journée, et toutes trois ont une cause commune : la vérification dépend de qui l'a lancée et où.

L'intégration continue supprime cette dépendance : c'est la construction et le test automatiques du code après chaque push. Ni déploiement manuel hebdomadaire, ni surveillance de la mémoire, ni sauvegardes de base - seulement une vérification, toujours identique, sur une machine que personne n'a configurée à sa façon.

Quand la lancer

Un workflow commence par nommer les événements qui le réveillent :

1name: CI
2
3on:
4  push:
5    branches: [main]
6  pull_request:

on: pull_request lance le workflow à la création d'une pull request - et c'est le moment qui compte le plus, car la vérification a lieu avant que le changement n'entre dans la branche principale.

on: push réagit à l'envoi de code ; on emploie d'ordinaire les deux ensemble. D'autres déclencheurs existent mais font autre chose : on: schedule lance un workflow à une heure fixée, et on: deployment lors d'un événement de déploiement. Aucun ne se déclenche quand quelqu'un ouvre une pull request.

L'ordre des jobs

Un pipeline se divise en jobs, et leur ordre n'est pas arbitraire :

  1. Lint - vérification du formatage.
  2. Build - compilation TypeScript.
  3. Test - tests unitaires.
  4. Docker build & push - construction et envoi de l'image.

La règle qui ordonne tout cela : le moins cher et le plus rapide d'abord. Le lint s'achève en quelques secondes, inutile donc d'attendre cinq minutes de tests pour apprendre qu'il manque un point-virgule. La compilation précède les tests, car des tests de code non compilé ne démarreront pas. L'image Docker se construit en dernier - c'est l'étape la plus coûteuse et rien ne sert de la commencer si quelque chose a échoué avant.

Étape par étape

Chaque job se compose d'étapes, et la première est toujours la récupération du code :

1jobs:
2  test:
3    runs-on: ubuntu-latest
4    steps:
5      - name: Checkout du code
6        uses: actions/checkout@v4
7
8      - name: Setup Node
9        uses: actions/setup-node@v4
10        with:
11          node-version: 20
12
13      - name: Installation des dependances
14        run: yarn install --frozen-lockfile
15
16      - name: Tests
17        run: yarn test --coverage

La forme d'une étape est fixe : - name: Checkout du code, puis uses:, puis actions/checkout@v4. name est le libellé affiché dans l'interface, uses désigne une action toute prête, et run une commande à exécuter dans le shell.

L'ordre dans le job de test est tout aussi fixe : actions/checkout@v4 récupère le code, actions/setup-node@v4 installe Node, yarn install --frozen-lockfile tire les dépendances, et enfin yarn test --coverage lance les tests.

Le drapeau --frozen-lockfile mérite l'attention : il installe exactement les versions du fichier de verrouillage et s'interrompt si elles ne correspondent pas. Sans lui, le pipeline pourrait installer en silence une dépendance plus récente et tester autre chose que ce que tu lanceras en production.

Les secrets

Un pipeline a besoin de mots de passe - pour le registre d'images, le serveur, la base de test. Nous les conservons dans Settings > Secrets and variables > Actions, dans les réglages du dépôt.

Pas dans un fichier .env du dépôt, pas dans un commentaire de commit, pas dans le README.md - tous ces endroits sont publics pour quiconque a accès au code, et l'historique Git ne s'efface pas si aisément.

Un secret enregistré entre dans le workflow par référence, jamais en clair :

1      - name: Connexion au registre
2        run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}

La valeur est substituée à l'exécution et apparaît masquée dans les journaux.

Trois améliorations

Pour finir, trois choses qui séparent un pipeline qui fonctionne d'un pipeline qu'on supporte d'attendre.

Matrix lance le même job sur plusieurs configurations à la fois :

1    strategy:
2      matrix:
3        node-version: [18, 20, 22]

Trois versions de Node, trois exécutions parallèles, une seule description. C'est la réponse au problème du début de leçon - « chez mon collègue, ça ne marche pas » apparaît désormais au moment de la pull request, non une semaine plus tard.

Cache retient les dépendances téléchargées d'une exécution à l'autre. Sans lui, chaque exécution retélécharge les mêmes paquets ; avec lui, l'installation passe de minutes à secondes.

Artifacts sont les fichiers qu'un job laisse derrière lui - un rapport de couverture, un paquet construit, des captures d'écran de tests. Ils survivent à l'exécution et se téléchargent depuis l'interface, ce qui est parfois le seul moyen de comprendre pourquoi quelque chose n'a échoué que sur la machine de CI.

Récapitulatif

La vérification ne dépend plus de qui la lance :

  • la CI est la construction et le test automatiques du code après chaque push - ni déploiement manuel, ni surveillance mémoire, ni sauvegardes,
  • on: pull_request lance le workflow à la création d'une pull request ; on: schedule se déclenche à une heure fixée, on: deployment lors d'un déploiement,
  • ordre des jobs : Lint → Build → Test → Docker build & push, selon « le moins cher d'abord »,
  • forme d'une étape : - name:, puis uses:, puis le nom de l'action ; run: exécute une commande shell,
  • le job de test dans l'ordre : actions/checkout@v4, actions/setup-node@v4, yarn install --frozen-lockfile, yarn test --coverage,
  • --frozen-lockfile installe exactement les versions du fichier de verrouillage et s'arrête en cas d'écart,
  • les secrets vont dans Settings > Secrets and variables > Actions - ni dans .env, ni dans un commentaire, ni dans le README.md,
  • matrix lance un job sur plusieurs configurations en parallèle, cache raccourcit l'installation des dépendances, artifacts conservent des fichiers après la fin de l'exécution.

Dans la prochaine leçon, nous descendrons des nombres vers une requête isolée - tu découvriras le distributed tracing. En attendant, retiens ceci : un pipeline est une vérification unique, toujours menée de la même façon - et une matrice est ce qui empêche « chez moi ça marche » d'être un argument.

Code de cette leçon : src/github-actions.ts
1// CI/CD avec GitHub Actions - Cursus Publicus
2console.log("=== PIPELINE CI/CD ===\n");
3
4interface PipelineJob {
5  name: string;
6  description: string;
7  steps: string[];
8  runsAfter?: string;
9}
10
11const pipeline: PipelineJob[] = [
12  {
13    name: 'lint',
14    description: 'Inspection du code',
15    steps: ['checkout', 'setup-node', 'yarn install', 'yarn lint'],
16  },
17  {
18    name: 'test',
19    description: 'Tests unitaires',
20    steps: ['checkout', 'setup-node', 'yarn install', 'yarn test --coverage'],
21    runsAfter: 'lint',
22  },
23  {
24    name: 'build',
25    description: 'Construction de l\'application',
26    steps: ['checkout', 'setup-node', 'yarn install', 'yarn build'],
27    runsAfter: 'test',
28  },
29  {
30    name: 'docker',
31    description: 'Build & push de l\'image Docker',
32    steps: ['checkout', 'docker login', 'docker build & push'],
33    runsAfter: 'build',
34  },
35];
36
37pipeline.forEach((job, i) => {
38  const arrow = job.runsAfter ? ` (après : ${job.runsAfter})` : ' (début)';
39  console.log(`Job ${i + 1} : ${job.name}${arrow}`);
40  console.log(`  Description : ${job.description}`);
41  job.steps.forEach((s, j) => {
42    console.log(`  Étape ${j + 1} : ${s}`);
43  });
44  console.log();
45});
46
47// Déclencheurs
48console.log("=== DÉCLENCHEURS DU WORKFLOW ===\n");
49console.log("push sur main     -> pipeline complet + deploy");
50console.log("push sur develop  -> CI sans deploy");
51console.log("pull_request     -> CI (lint + test + build)\n");
52
53// Secrets
54console.log("=== GITHUB SECRETS ===\n");
55console.log("Stocke les mots de passe et clés d'API dans :");
56console.log("  Settings > Secrets > Actions");
57console.log("  Jamais dans le code source !");
58

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Que signifie le CI (Continuous Integration) ?

  2. 2. Quel déclencheur exécute un workflow GitHub Actions lorsqu'une pull request est créée ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonnez les jobs du pipeline CI/CD du premier au dernier :

  • Éditeur de code

    Définissez les étapes de trois jobs : lint (checkout, setup-node, install, lint), test (+ coverage), build

  • Tri horizontal

    Ordonnez les éléments de l'étape de checkout dans GitHub Actions dans le bon ordre :

  • Clique dans l'ordre

    Ordonnez les étapes du job « test » dans GitHub Actions dans le bon ordre :

  • Éditeur de code

    Configurez helmet, compression, CORS, ValidationPipe et ThrottlerGuard dans le bon ordre