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.
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.
Celui-ci suit une version de bout en bout : commit, build, version,
publication npm, publication Docker, déploiement et healthcheck
final.
Le dernier schéma sert à lire le rollback comme une réaction à un
échec de vérification, pas comme une étape normale de livraison.