← Retour à l'accueil

Cours 05 · bonus

Pipeline CI/CD locale décortiquée

Cette séance revient sur la pipeline finale de tp-cd-deployment. On ne cherche pas à ajouter beaucoup de nouvelles notions : on ouvre le capot, on suit les flux, on lit le YAML, on manipule légèrement les jobs et on apprend à interpréter les sorties.

5 minutes

Carte du terrain

Le TP simule une chaîne complète sur une machine hôte. Le point important est de séparer les rôles : le DevContainer sert à travailler, le moteur Docker de la machine exécute les conteneurs, act crée des runners temporaires, et les services du TP jouent les registres et les serveurs de déploiement.

Chargement de la carte locale…

Définition utile : le moteur Docker est le service qui crée, démarre, arrête et connecte les conteneurs. Selon l'OS, il peut venir de Docker Engine, Docker Desktop, Rancher Desktop, Colima ou d'une autre installation. Le DevContainer n'est pas ce moteur : c'est lui-même un conteneur, avec des outils qui parlent au moteur Docker disponible sur la machine.

30 minutes

Désacralisation du DevContainer et des conteneurs

Le DevContainer est l'environnement de travail reproductible du TP. Il contient Node, npm, Git, SSH, act, Trivy et le client Docker. Le client Docker ne lance pas les conteneurs tout seul : il envoie des ordres au moteur Docker de la machine hôte. C'est aussi pour cela que act pointe vers Docker dans le schéma : act s'appuie sur Docker pour créer un conteneur runner GitHub Actions local.

Chargement du schéma réseau…

.devcontainer/devcontainer.json

Objectif : décrire l'atelier de développement que VS Code doit construire. Il ne décrit pas l'application en production ; il décrit le poste de travail reproductible utilisé pour le TP.

{
  "name": "tp-cd-deployment", // Nom affiché par VS Code pour ce DevContainer.
  "image": "mcr.microsoft.com/devcontainers/typescript-node:24",
  // Image de base du conteneur de travail : Node 24 + TypeScript.

  "customizations": {
    "vscode": {
      "extensions": [
        "nestjs.vscode-nestjs-schematics", // Aide au développement NestJS.
        "dbaeumer.vscode-eslint", // Remonte les erreurs ESLint dans l'IDE.
        "esbenp.prettier-vscode", // Formatage Prettier.
        "firsttris.vscode-jest-runner" // Lancement plus confortable des tests Jest.
      ]
    }
  },

  "forwardPorts": [3000, 4873, 5000, 2222, 2223, 3001, 3002],
  // Ports du DevContainer à exposer vers la machine hôte.
  // Exemple : un service joignable dans le DevContainer sur localhost:3000
  // devient joignable depuis la machine hôte sur localhost:3000.

  "mounts": [
    "source=tp-cd-deployment-npm-cache,target=/home/node/.npm,type=volume"
  ],
  // Volume Docker persistant pour éviter de retélécharger tout le cache npm.

  "postCreateCommand": "bash .devcontainer/setup.sh",
  // Lancé une fois à la création : dépendances, clés SSH, outils, services.
  "postStartCommand": "bash .devcontainer/start-services.sh",
  // Relancé à chaque démarrage : remet les services et relais réseau en état.

  "remoteUser": "node",
  // Utilisateur utilisé dans le conteneur de travail.

  "remoteEnv": {
    "GIT_AUTHOR_NAME": "${localEnv:GIT_AUTHOR_NAME:}",
    "GIT_AUTHOR_EMAIL": "${localEnv:GIT_AUTHOR_EMAIL:}",
    "GIT_COMMITTER_NAME": "${localEnv:GIT_COMMITTER_NAME:}",
    "GIT_COMMITTER_EMAIL": "${localEnv:GIT_COMMITTER_EMAIL:}"
  },
  // Réutilise l'identité Git de la machine locale dans le DevContainer.

  "features": {
    // Les features sont des briques prêtes à l'emploi ajoutées à l'image de base.
    // Elles évitent d'écrire un Dockerfile juste pour installer des outils courants.
    "ghcr.io/devcontainers/features/common-utils:2": {
      // Ajoute des utilitaires système pratiques dans le conteneur de travail.
      "configureZshAsDefaultShell": true,
      // Configure zsh comme shell par défaut pour le confort dans le terminal VS Code.
      "upgradePackages": false
      // Évite de mettre à jour tous les paquets système à la création du conteneur.
      // Le démarrage reste plus rapide et l'image de base reste plus prévisible.
    },
    "ghcr.io/devcontainers/features/git:1": {},
    // Installe/configure Git dans le DevContainer.
    // Le dépôt peut donc être manipulé depuis le terminal du conteneur.
    "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
      // Installe le client Docker dans le DevContainer et le branche sur le moteur Docker hôte.
      // Le DevContainer peut lancer docker ps, docker compose, docker build, act, etc.
      "moby": false
      // Ne réinstalle pas un moteur Docker complet dans le DevContainer.
      // On utilise le moteur Docker déjà disponible sur la machine hôte.
    }
  }
  // Ajoute Git, des utilitaires, et surtout un client Docker qui parle au moteur hôte.
}

Mini-exercice : retirer temporairement un port de forwardPorts et observer ce qui reste accessible depuis l'extérieur du DevContainer.

Succès : VS Code ouvre un environnement homogène pour tout le monde. Échec : le conteneur peut s'ouvrir sans les bons outils, sans accès Docker, ou sans les ports nécessaires aux tests.

.devcontainer/setup.sh

Objectif : préparer le TP au premier démarrage du DevContainer. Ce script installe les dépendances, crée les données locales, prépare SSH, configure act et installe les outils nécessaires à la pipeline locale.

#!/bin/bash
set -ex
# -e arrête le script à la première erreur.
# -x affiche les commandes exécutées, utile pour comprendre le setup.

WORKSPACE="/workspaces/tp-cd-deployment" # Chemin du dépôt dans le DevContainer.
SSH_KEY_PATH="$HOME/.ssh/tp_cd_deployment_key" # Clé utilisée pour les déploiements SSH.

echo "==> Installation des dépendances npm..."
npm ci # Installation reproductible depuis package-lock.json.

echo "==> Création de la base de données SQLite et initialisation des données de démonstration..."
DATABASE_URL="./dev.db" npx ts-node db/seed.ts
# Crée une base locale de développement, indépendante des bases des cibles déployées.

echo "==> Génération de la paire de clés SSH pour les déploiements..."
mkdir -p "$HOME/.ssh" # Crée le dossier SSH s'il n'existe pas.
rm -f "$SSH_KEY_PATH" "$SSH_KEY_PATH.pub" # Supprime une ancienne paire pour repartir proprement.
ssh-keygen -t ed25519 -f "$SSH_KEY_PATH" -N "" -C "tp-cd-deployment"
# Génère une clé privée et une clé publique sans passphrase pour le TP.

echo "==> Création du fichier de secrets pour act..."
{ printf 'SSH_PRIVATE_KEY="'; cat "$SSH_KEY_PATH"; printf '"'; } > "$WORKSPACE/.secrets"
# act lit .secrets pour fournir secrets.SSH_PRIVATE_KEY au workflow local.
chmod 600 "$WORKSPACE/.secrets" # Restreint la lecture du fichier de secrets.

echo "==> Configuration SSH locale..."
cat >> "$HOME/.ssh/config" <<EOF

Host tp-cd-deployment-npm
  HostName localhost
  Port 2222
  User deployer
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  IdentityFile $SSH_KEY_PATH

Host tp-cd-deployment-docker
  HostName localhost
  Port 2223
  User deployer
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  IdentityFile $SSH_KEY_PATH
EOF
# Crée deux alias SSH faciles à utiliser depuis le terminal du DevContainer.
chmod 600 "$HOME/.ssh/config"

bash .devcontainer/start-services.sh
# Lance les registres, cibles SSH, relais réseau et injection de clé publique.

echo "==> Installation de act (exécution locale des GitHub Actions)..."
curl -s https://raw.githubusercontent.com/nektos/act/master/install.sh -o install_act.sh
sudo bash install_act.sh -b /usr/local/bin/
rm install_act.sh
sudo chmod +x /usr/local/bin/act
# Installe act pour exécuter les jobs GitHub Actions localement.

echo "==> Pré-téléchargement de l'image Docker pour act..."
docker pull catthehacker/ubuntu:act-24.04
# Évite de télécharger l'image runner au premier lancement du TP.

echo "==> Installation de Trivy (scan de sécurité)..."
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin
# Installe le scanner de vulnérabilités utilisé par le job security.

Mini-exercice : supprimer temporairement le fichier .secrets, lancer un job de déploiement avec act, puis expliquer pourquoi le secret SSH manque.

Succès : le dépôt est prêt à lancer l'application, les tests et la CI locale. Échec : les dépendances, la base locale, les clés SSH, act ou Trivy peuvent manquer.

Parenthèse : pourquoi créer une clé SSH ?

La pipeline locale doit simuler un vrai déploiement : un runner CI se connecte à une machine cible et exécute des commandes. Pour éviter les mots de passe, le TP crée une paire de clés. La clé privée reste côté DevContainer et est copiée dans .secrets pour que act puisse remplir secrets.SSH_PRIVATE_KEY. La clé publique est injectée dans authorized_keys des deux cibles SSH. Résultat : le runner peut prouver son identité avec la clé privée, et les cibles acceptent la connexion parce qu'elles connaissent la clé publique correspondante.

Clé privée : ~/.ssh/tp_cd_deployment_key
  -> utilisée par le client SSH
  -> copiée dans .secrets pour act

Clé publique : ~/.ssh/tp_cd_deployment_key.pub
  -> copiée dans /home/deployer/.ssh/authorized_keys des cibles
  -> autorise les connexions du détenteur de la clé privée

.devcontainer/start-services.sh

Objectif : remettre l'infrastructure locale du TP dans un état utilisable à chaque démarrage. Ce script peut être relancé après une veille, un redémarrage ou un souci de relais réseau.

#!/bin/bash
set -euo pipefail
# -e arrête au premier échec, -u refuse les variables non définies,
# pipefail propage les erreurs dans les pipelines.

WORKSPACE="/workspaces/tp-cd-deployment" # Chemin du dépôt dans le DevContainer.
NETWORK="tp-cd-deployment_cd-network" # Réseau Docker créé par docker compose.
SSH_KEY_PATH="$HOME/.ssh/tp_cd_deployment_key" # Clé publique à injecter.

echo "==> Démarrage des services locaux (registres + cibles SSH)..."
docker compose -f "$WORKSPACE/docker-compose.yml" up -d --build
# Construit et démarre Verdaccio, registry:2, ssh-npm-target et ssh-docker-target.

echo "==> Connexion du DevContainer au réseau Docker cd-network..."
docker network connect "$NETWORK" "$(hostname)" 2>/dev/null || true
# Ajoute le DevContainer au même réseau que les services, si ce n'est pas déjà fait.

echo "==> Démarrage des relais socat (localhost → services Docker)..."
DEVCONTAINER_ID="$(hostname)" # Dans un conteneur, hostname correspond à son id.
docker exec "$DEVCONTAINER_ID" pkill -f "socat TCP-LISTEN:4873" 2>/dev/null || true
docker exec "$DEVCONTAINER_ID" pkill -f "socat TCP-LISTEN:5000" 2>/dev/null || true
docker exec "$DEVCONTAINER_ID" pkill -f "socat TCP-LISTEN:2222" 2>/dev/null || true
docker exec "$DEVCONTAINER_ID" pkill -f "socat TCP-LISTEN:2223" 2>/dev/null || true
docker exec "$DEVCONTAINER_ID" pkill -f "socat TCP-LISTEN:3001" 2>/dev/null || true
docker exec "$DEVCONTAINER_ID" pkill -f "socat TCP-LISTEN:3002" 2>/dev/null || true
# Nettoie les anciens relais pour éviter les ports déjà occupés.

docker exec -d "$DEVCONTAINER_ID" socat TCP-LISTEN:4873,fork,reuseaddr TCP:verdaccio:4873
docker exec -d "$DEVCONTAINER_ID" socat TCP-LISTEN:5000,fork,reuseaddr TCP:registry:5000
docker exec -d "$DEVCONTAINER_ID" socat TCP-LISTEN:2222,fork,reuseaddr TCP:ssh-npm-target:22
docker exec -d "$DEVCONTAINER_ID" socat TCP-LISTEN:2223,fork,reuseaddr TCP:ssh-docker-target:22
docker exec -d "$DEVCONTAINER_ID" socat TCP-LISTEN:3001,fork,reuseaddr TCP:ssh-npm-target:3000
docker exec -d "$DEVCONTAINER_ID" socat TCP-LISTEN:3002,fork,reuseaddr TCP:ssh-docker-target:3000
# Crée des tunnels locaux : localhost du DevContainer vers les services Docker.

if [ -f "$SSH_KEY_PATH.pub" ]; then
  echo "==> Injection de la clé publique dans les cibles SSH..."
  for target in tp-cd-deployment-ssh-npm-target tp-cd-deployment-ssh-docker-target; do
    docker exec "$target" sh -c "echo '$(cat "$SSH_KEY_PATH.pub")' > /home/deployer/.ssh/authorized_keys && chmod 600 /home/deployer/.ssh/authorized_keys && chown deployer:deployer /home/deployer/.ssh/authorized_keys"
  done
fi
# Autorise les deux cibles SSH à accepter la clé privée générée par setup.sh.

echo "==> Attente du démarrage de Verdaccio..."
until curl -sf http://localhost:4873/-/ping > /dev/null 2>&1; do
  echo "   ... Verdaccio pas encore prêt, attente 2s..."
  sleep 2
done
# Boucle tant que le registre npm local ne répond pas.

echo "==> Attente du démarrage du registry Docker..."
until curl -sf http://localhost:5000/v2/ > /dev/null 2>&1; do
  echo "   ... registry:2 pas encore prêt, attente 2s..."
  sleep 2
done
# Boucle tant que le registre Docker local ne répond pas.

echo "==> Attente des cibles SSH..."
for port in 2222 2223; do
  for i in $(seq 1 15); do
    if ssh -i "$SSH_KEY_PATH" -p "$port" -o StrictHostKeyChecking=no -o ConnectTimeout=3 deployer@localhost "echo ok" > /dev/null 2>&1; then
      echo "   SSH target $port opérationnelle"
      break
    fi
    echo "   ... SSH target $port pas encore prête ($i/15), attente 2s..."
    sleep 2
  done
done
# Vérifie que les deux cibles de déploiement acceptent bien une connexion SSH.

Mini-exercice : arrêter un conteneur de service avec docker stop, relancer bash .devcontainer/start-services.sh, puis vérifier que le service et les relais reviennent.

Succès : registres, cibles SSH et relais locaux répondent. Échec : Docker Compose ne démarre pas, le DevContainer n'est pas sur le bon réseau, les relais socat ne tournent pas, ou les cibles SSH ne reçoivent pas la clé publique.

Mini-exercice : vérifier le terrain

Associer chaque commande à une pièce du schéma : moteur Docker, registre npm, registre Docker, cible SSH ou relais réseau.

docker ps
curl http://localhost:4873/-/ping
curl http://localhost:5000/v2/
ssh tp-cd-deployment-npm "echo ok"
ssh tp-cd-deployment-docker "echo ok"
bash bin/check-relays.sh

Les deux schémas suivants montrent les deux chemins de déploiement du TP. Ils partent du même build, mais terminent dans deux formats différents : un package npm installé avec pm2, et une image Docker lancée comme conteneur applicatif.

Chargement du flux npm…

Chargement du flux Docker…

Ce dernier schéma montre le rôle de act : il lit le workflow GitHub Actions, demande au moteur Docker de créer un runner local, puis ce runner exécute les steps du job comme le ferait un runner GitHub hébergé.

Chargement de la séquence act…

85 minutes

Désacralisation de la pipeline finale

La pipeline se lit comme une chaîne de preuves : le code est vérifié, compilé, scanné, versionné, publié, déployé puis vérifié. Les jobs sont isolés ; quand un job a besoin d'un résultat produit ailleurs, il faut le transmettre explicitement avec un cache, un artefact ou un registre.

Chargement de la pipeline finale…

Job install : préparer les dépendances

Objectif : obtenir un dossier node_modules cohérent avec package-lock.json, ou le restaurer depuis le cache si rien n'a changé.

install: # Identifiant du job, utilisé par needs.
  name: Installation des dépendances # Nom lisible dans l'interface Actions.
  runs-on: ubuntu-latest # Runner Linux utilisé par GitHub ou simulé par act.
  steps: # Liste des actions exécutées dans ce job.
    - uses: actions/checkout@v4 # Récupère le code du dépôt dans le runner.
    - name: Setup Node.js # Prépare Node.
      uses: actions/setup-node@v4 # Action officielle de configuration Node.
      with:
        node-version: '24' # Même version que le DevContainer.
        cache: 'npm' # Active le cache npm global.
    - name: Restaurer le cache node_modules
      id: cache-node-modules # Donne un nom réutilisable à ce step.
      uses: actions/cache@v4 # Restaure ou crée un cache.
      with:
        path: node_modules # Dossier à restaurer.
        key: node-modules-${{ hashFiles('package-lock.json') }} # Cache invalidé si le lockfile change.
    - name: Installer les dépendances
      if: steps.cache-node-modules.outputs.cache-hit != 'true' # Évite npm ci si cache trouvé.
      run: npm ci # Installation reproductible depuis package-lock.json.

Mini-exercice : modifier temporairement une version dans package-lock.json ou supprimer le cache, puis observer si npm ci est relancé.

Succès : les dépendances sont disponibles pour les jobs suivants. Échec : le lockfile est incohérent, le registre npm est inaccessible, ou une dépendance ne s'installe plus.

Job format-lint : vérifier la qualité statique

Objectif : bloquer les changements qui ne respectent pas le formatage ou les règles de qualité avant d'aller aux tests.

format-lint:
  name: Formatage & Lint # Nom affiché du job.
  runs-on: ubuntu-latest # Runner isolé.
  needs: [install] # Ne démarre que si install est vert.
  steps:
    - uses: actions/checkout@v4 # Le runner repart d'un checkout propre.
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '24' # Version Node alignée avec le projet.
    - name: Restaurer le cache node_modules
      uses: actions/cache@v4
      with:
        path: node_modules # Récupère les dépendances préparées.
        key: node-modules-${{ hashFiles('package-lock.json') }}
    - name: Vérifier le formatage (Prettier)
      run: npm run format:check # Vérifie sans réécrire les fichiers.
    - name: Analyser le code (ESLint + SonarJS)
      run: npm run lint # Exécute les règles de lint.

Mini-exercice : ajouter volontairement un espace ou une indentation non conforme dans un fichier TypeScript, lancer le job, puis corriger avec le script de formatage local.

Succès : le code est lisible et conforme aux règles. Échec : le code peut fonctionner, mais il ne passe pas la convention commune de l'équipe.

Job tests : valider la logique applicative

Objectif : exécuter les tests unitaires avec couverture pour vérifier les comportements métier proches du code.

tests:
  name: Tests unitaires # Nom lisible.
  runs-on: ubuntu-latest
  needs: [format-lint] # On teste seulement un code déjà propre.
  steps:
    - uses: actions/checkout@v4 # Récupération du code.
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '24'
    - name: Restaurer le cache node_modules
      uses: actions/cache@v4
      with:
        path: node_modules
        key: node-modules-${{ hashFiles('package-lock.json') }}
    - name: Lancer les tests avec couverture
      run: npm run test:ci # Lance Jest en mode CI avec couverture.

Mini-exercice : changer temporairement l'exigence de couverture dans la configuration Jest ou le script associé, puis observer si le job échoue alors que les assertions passent.

Succès : les comportements testés restent vrais et la couverture minimale est respectée. Échec : une règle métier est cassée, un test est instable ou la couverture devient insuffisante.

Job tests-e2e : valider l'API HTTP

Objectif : démarrer l'application en contexte de test et vérifier les routes HTTP avec Supertest.

tests-e2e:
  name: Tests E2E (Supertest)
  runs-on: ubuntu-latest
  needs: [tests] # Les E2E arrivent après les tests unitaires.
  steps:
    - uses: actions/checkout@v4
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '24'
    - name: Restaurer le cache node_modules
      uses: actions/cache@v4
      with:
        path: node_modules
        key: node-modules-${{ hashFiles('package-lock.json') }}
    - name: Lancer les tests E2E
      run: npm run test:e2e # Exécute les scénarios HTTP.
      env:
        DATABASE_URL: ':memory:' # Base SQLite en mémoire, jetable et isolée.

Mini-exercice : ajouter une attente sur un code HTTP précis dans un test E2E, puis provoquer une divergence dans le contrôleur.

Succès : l'API répond comme attendu côté utilisateur HTTP. Échec : le contrat d'API est cassé, ou l'environnement de test ne démarre plus correctement.

Job build : produire l'artefact compilé

Objectif : compiler TypeScript vers dist/, puis transmettre ce résultat aux jobs de publication.

build:
  name: Build
  runs-on: ubuntu-latest
  needs: [tests-e2e] # On build seulement si les validations passent.
  steps:
    - uses: actions/checkout@v4
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '24'
    - name: Restaurer le cache node_modules
      uses: actions/cache@v4
      with:
        path: node_modules
        key: node-modules-${{ hashFiles('package-lock.json') }}
    - name: Compiler l'application
      run: npm run build # Produit dist/.
    - name: Sauvegarder le build
      uses: actions/upload-artifact@v4 # Stocke un résultat pour d'autres jobs.
      with:
        name: build-dist # Nom de l'artefact.
        path: dist/ # Dossier transmis.

Mini-exercice : changer temporairement le nom de l'artefact en build-dist-test, puis constater que les jobs qui le téléchargent ne le trouvent plus.

Succès : le code est compilable et dist/ est disponible. Échec : erreur TypeScript, script de build cassé ou artefact absent.

Job security : rendre les vulnérabilités visibles

Objectif : scanner le dépôt avec Trivy. Dans ce TP, le scan est pédagogique : il rend l'information visible sans bloquer sur les vulnérabilités.

security:
  name: Scan de sécurité (Trivy)
  runs-on: ubuntu-latest
  needs: [build] # Le scan arrive après un build réussi.
  steps:
    - uses: actions/checkout@v4
    - name: Installer Trivy
      run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b .
      # Télécharge Trivy dans le runner.
    - name: Scanner les dépendances npm
      run: ./trivy fs --exit-code 0 --severity CRITICAL,HIGH --no-progress .
      # --exit-code 0 signifie : afficher les problèmes, mais ne pas faire échouer le job.

Mini-exercice : remplacer temporairement --exit-code 0 par --exit-code 1 et discuter du choix entre scan informatif et scan bloquant.

Succès : le scan a tourné et ses résultats sont lisibles. Échec : Trivy n'a pas pu s'installer, ou le scan est configuré pour bloquer sur les vulnérabilités détectées.

Job release : calculer la version

Objectif : transformer l'historique de commits en version SemVer et tag de release, uniquement sur main.

release:
  name: Release
  runs-on: ubuntu-latest
  needs: [security] # La release attend les vérifications précédentes.
  if: github.ref == 'refs/heads/main' # Pas de publication depuis une branche de travail.
  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0 # Récupère l'historique et les tags nécessaires au versioning.
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '24'
    - name: Restaurer le cache node_modules
      uses: actions/cache@v4
      with:
        path: node_modules
        key: node-modules-${{ hashFiles('package-lock.json') }}
    - name: Configurer git user
      run: |
        git config user.email "ci@example.com" # Auteur du commit de release.
        git config user.name "CI"
    - name: Calculer la version
      run: npx commit-and-tag-version # Met à jour version, changelog et tag dans le runner.

Mini-exercice : comparer l'effet d'un commit fix: et d'un commit feat: sur le bump de version attendu.

Succès : une version cohérente est calculée dans le runner. Échec : l'historique Git est insuffisant, les commits ne respectent pas les conventions, ou le workspace Git n'est pas modifiable.

Job publish-npm : publier le package

Objectif : prendre le build compilé et publier le package dans Verdaccio, le registre npm local du TP.

publish-npm:
  name: Publish npm
  runs-on: ubuntu-latest
  needs: [release] # Publie seulement après calcul de version.
  if: github.ref == 'refs/heads/main' # Publication limitée à main.
  steps:
    - uses: actions/checkout@v4
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '24'
    - name: Restaurer le cache node_modules
      uses: actions/cache@v4
      with:
        path: node_modules
        key: node-modules-${{ hashFiles('package-lock.json') }}
    - name: Telecharger le build
      uses: actions/download-artifact@v4 # Récupère l'artefact produit par build.
      with:
        name: build-dist
        path: dist/
    - name: Configurer Verdaccio
      run: npm set //localhost:4873/:_authToken "dummy-token" # Simule l'auth npm locale.
    - name: Publier le package
      run: npm publish --registry http://localhost:4873 # Envoie le package versionné.

Mini-exercice : changer temporairement l'URL du registry et observer l'erreur, puis vérifier une publication réussie avec npm view.

Succès : la version npm existe dans Verdaccio. Échec : artefact manquant, authentification npm incorrecte, registry inaccessible ou version déjà publiée.

Job publish-docker : publier l'image

Objectif : construire une image Docker versionnée avec le même build dist/, puis la pousser dans le registry local.

publish-docker:
  name: Publish Docker
  runs-on: ubuntu-latest
  needs: [release]
  if: github.ref == 'refs/heads/main'
  steps:
    - uses: actions/checkout@v4
    - name: Telecharger le build
      uses: actions/download-artifact@v4
      with:
        name: build-dist # Même artefact que publish-npm.
        path: dist/
    - name: Calculer le tag Docker
      id: version
      run: |
        VERSION=$(node -p "require('./package.json').version") # Lit la version npm.
        echo "version=${VERSION}" >> "$GITHUB_OUTPUT" # Expose la version aux steps suivants.
    - name: Verifier que le tag Docker n'existe pas
      run: |
        TAG="${{ steps.version.outputs.version }}"
        URL="http://localhost:5000/v2/tp-cd-deployment/manifests/${TAG}"
        if curl -fsI -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "$URL" > /dev/null; then
          exit 1 # Refuse d'écraser un tag déjà présent.
        fi
    - name: Construire l'image
      run: docker build -t localhost:5000/tp-cd-deployment:${{ steps.version.outputs.version }} .
    - name: Publier l'image
      run: docker push localhost:5000/tp-cd-deployment:${{ steps.version.outputs.version }}

Mini-exercice : supprimer temporairement la vérification du tag, discuter de la mutabilité des tags Docker, puis la remettre.

Succès : l'image versionnée est disponible dans registry:2. Échec : build Docker cassé, registry inaccessible, ou tag déjà présent.

Job deploy-npm : installer le package sur la cible npm

Objectif : se connecter en SSH à la cible npm, installer la version publiée depuis Verdaccio, puis démarrer l'application avec pm2.

deploy-npm:
  name: Deploy npm
  runs-on: ubuntu-latest
  needs: [publish-npm] # Attend la publication npm.
  if: github.ref == 'refs/heads/main'
  steps:
    - uses: actions/checkout@v4
    - name: Calculer la version
      id: version
      run: |
        VERSION=$(node -p "require('./package.json').version")
        echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
    - name: Deployer le package npm
      uses: appleboy/ssh-action@v1.0.3 # Exécute le script sur la cible SSH.
      with:
        host: localhost
        port: 2222 # Relais vers ssh-npm-target.
        username: deployer
        key: ${{ secrets.SSH_PRIVATE_KEY }} # Clé préparée dans .secrets pour act.
        script: |
          set -e
          mkdir -p ~/app
          CURRENT=$(node -p "require('/home/deployer/app/node_modules/tp-cd-deployment/package.json').version" 2>/dev/null || echo "")
          echo "$CURRENT" > ~/app/.previous-npm-version # Prépare un rollback.
          npm install --prefix ~/app tp-cd-deployment@${{ steps.version.outputs.version }} --registry http://verdaccio:4873 --ignore-scripts
          pm2 delete tp-cd-deployment-npm 2>/dev/null || true
          PORT=3000 DATABASE_URL=/home/deployer/app/dev.db pm2 start ~/app/node_modules/tp-cd-deployment/dist/src/main.js --name tp-cd-deployment-npm --update-env

Mini-exercice : changer temporairement le port SSH vers 2223 et expliquer pourquoi le script npm n'a plus la bonne cible.

Succès : pm2 exécute la version publiée et l'app sera joignable via le relais 3001. Échec : SSH, secret, Verdaccio, installation npm ou démarrage pm2 pose problème.

Job deploy-docker : lancer l'image sur la cible Docker

Objectif : se connecter à la cible Docker, tirer l'image publiée depuis le registry local et remplacer le conteneur applicatif.

deploy-docker:
  name: Deploy Docker
  runs-on: ubuntu-latest
  needs: [publish-docker] # Attend que l'image existe dans le registry.
  if: github.ref == 'refs/heads/main'
  steps:
    - uses: actions/checkout@v4
    - name: Calculer la version
      id: version
      run: |
        VERSION=$(node -p "require('./package.json').version")
        echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
    - name: Deployer l'image Docker
      uses: appleboy/ssh-action@v1.0.3
      with:
        host: localhost
        port: 2223 # Relais vers ssh-docker-target.
        username: deployer
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          set -e
          docker pull localhost:5000/tp-cd-deployment:${{ steps.version.outputs.version }}
          docker rm -f tp-cd-deployment-app 2>/dev/null || true
          docker run -d \
            --name tp-cd-deployment-app \
            --network container:tp-cd-deployment-ssh-docker-target \
            -e PORT=3000 \
            -e DATABASE_URL=/app/dev.db \
            localhost:5000/tp-cd-deployment:${{ steps.version.outputs.version }}

Mini-exercice : changer temporairement le nom du conteneur dans docker run, puis observer l'effet sur les commandes de nettoyage ou d'inspection.

Succès : un conteneur applicatif exécute l'image versionnée et sera joignable via le relais 3002. Échec : image absente, registry inaccessible, commande Docker impossible ou démarrage applicatif cassé.

Job healthcheck-deployment : prouver que les deux déploiements répondent

Objectif : vérifier la réalité opérationnelle minimale : les deux services déployés répondent sur /health.

healthcheck-deployment:
  name: Healthcheck deployment
  runs-on: ubuntu-latest
  needs: [deploy-npm, deploy-docker] # Attend les deux chemins de déploiement.
  if: github.ref == 'refs/heads/main'
  steps:
    - name: Verifier le service npm
      run: curl -f http://localhost:3001/health # -f échoue sur HTTP 4xx/5xx.
    - name: Verifier le service Docker
      run: curl -f http://localhost:3002/health

Mini-exercice : remplacer temporairement /health par une route inexistante pour observer un échec de smoke test.

Succès : les deux déploiements sont joignables. Échec : le code peut être publié, mais le service réellement déployé ne répond pas comme attendu.

Le schéma suivant isole le passage d'un build compilé vers deux publications. Il sert à comprendre le principe build once, publish many : le job build produit une seule sortie, puis les jobs de publication la retéléchargent.

Chargement des artefacts…

Celui-ci suit une version de bout en bout : commit, build, version, publication npm, publication Docker, déploiement et healthcheck final.

Chargement du cycle de version…

Le dernier schéma sert à lire le rollback comme une réaction à un échec de vérification, pas comme une étape normale de livraison.

Chargement du rollback…

Challenges · 2 heures maximum chacun

Challenges pour aller plus loin

Chaque challenge part de tp-cd-deployment et doit tenir en deux heures maximum. L'objectif est d'aller un cran plus loin autour de la CI/CD : déclencheurs, exécution distante, release, données, observabilité ou workflows manuels.

1. Sortir du 100 % local : exécuter la pipeline sur GitHub

Objectif : pousser le dépôt sur GitHub, déclencher la CI distante et comparer les différences avec act. Livrable : capture ou résumé d'un run GitHub Actions, avec au moins trois différences expliquées : secrets, accès aux registres locaux, réseau, artefacts ou permissions.

2. Explorer les triggers GitHub Actions

Objectif : tester push, pull_request, workflow_dispatch et éventuellement schedule dans un workflow dédié. Livrable : un petit workflow d'observation qui affiche le trigger, la branche, le SHA et les variables GitHub utiles.

3. Mettre en application un workflow précis

Objectif : créer un workflow manuel workflow_dispatch avec un paramètre environment ou version, puis exécuter seulement les étapes nécessaires. Livrable : un workflow simple, documenté, qui montre comment cibler une action ponctuelle sans relancer toute la chaîne.

4. Données de déploiement

Objectif : comprendre pourquoi les bases SQLite des déploiements npm et Docker peuvent paraître vides. Livrable : diagnostic + une solution testée parmi seed contrôlé, volume persistant, migration ou endpoint d'initialisation.

5. Remplacer le release tooling

Objectif : mettre en place release-please ou semantic-release sur le dépôt distant, en gardant l'esprit Conventional Commits. Livrable : une release de test ou un dry-run documenté, avec comparaison claire face à commit-and-tag-version.

6. Ajouter une observabilité minimale

Objectif : ajouter un signal exploitable après déploiement : endpoint /metrics, logs structurés, Prometheus + Grafana, ou un tableau de santé plus simple. Livrable : une preuve visuelle ou textuelle permettant de répondre à « quelle version tourne et est-elle en bonne santé ? ».