Durée estimée : 6h
Prérequis : Git, GitHub, bases Bash ou PowerShell (TPs précédents)
Environnement : tout OS avec Git installé et un compte GitHub actif
À la fin de ce TP vous saurez lire et écrire des workflows GitHub Actions, construire un pipeline CI complet (lint + tests), gérer des secrets, automatiser un déploiement sur Azure, mettre en place des hooks locaux pour accélérer votre boucle de feedback, et sécuriser votre repo avec Dependabot, CODEOWNERS et les branch protection rules.
⏱️ Cette section prend environ 20 minutes. Faites-la soigneusement — un environnement mal configuré rend tous les exercices suivants douloureux.
Un fork crée une copie personnelle du repo sur votre compte GitHub. Vous avez tous les droits dessus : configurer des secrets, des environnements, des branch protection rules — exactement comme en entreprise.
- Allez sur le repo du TP fourni par le formateur
- Cliquez sur le bouton Fork en haut à droite
- Laissez les paramètres par défaut et cliquez Create fork
- Vous êtes maintenant sur
https://github.com/<votre-compte>/tp-github-actions
Pourquoi forker plutôt que cloner directement ? Sur un clone direct, vous n'avez pas les droits de push. Le fork vous donne votre propre repo avec vos propres Actions, secrets et environments — indispensable pour ce TP.
Option 1 — Directement depuis GitHub (recommandé pour débutants)
- Sur votre fork GitHub, cliquez le bouton vert Code
- Sélectionnez l'onglet Local → Open with Visual Studio Code
- VS Code s'ouvre et vous propose de cloner — acceptez
- Choisissez un dossier sur votre machine (ex :
Documents/formation-devops/) - Cliquez Open quand VS Code propose d'ouvrir le repo cloné
Option 2 — Via le terminal
# Remplacez <votre-compte> par votre nom d'utilisateur GitHub
git clone https://github.com/<votre-compte>/tp-github-actions
cd tp-github-actions
# Ouvrir dans VS Code
code .Installez ces extensions (cherchez leur nom dans l'onglet Extensions, icône en carré sur le panneau gauche) :
| Extension | Utilité dans ce TP |
|---|---|
| GitHub Actions (GitHub) | Autocomplétion YAML, visualisation des runs directement dans VS Code |
| Python (Microsoft) | Coloration, linting, exécution des tests |
| YAML (Red Hat) | Validation et autocomplétion des fichiers .yml |
| GitLens (GitKraken) | Voir l'historique Git ligne par ligne, inspecter les branches |
Astuce VS Code : le fichier
.github/workflows/ci.ymls'ouvre avec coloration et autocomplétion si l'extension GitHub Actions est installée. Vous verrez les erreurs YAML en temps réel sans avoir à pusher.
# Vérifier si Git connaît votre identité
git config --global user.name
git config --global user.email
# Si vide, configurez-le (même email que votre compte GitHub)
git config --global user.name "Prénom Nom"
git config --global user.email "votre@email.com"Dans VS Code, vous pouvez aussi faire Ctrl+Shift+P → taper Git: Open Settings pour configurer via l'interface.
Ouvrez un terminal intégré dans VS Code : menu Terminal → New Terminal (ou Ctrl+`).
# Vérifier que vous êtes bien dans le bon dossier
pwd # doit afficher le chemin vers tp-github-actions
ls # doit lister README.md, ressources/, .github/, etc.
# Installer les dépendances Python
pip install -r ressources/requirements.txt
# Vérifier que les tests passent en local AVANT de pusher
cd ressources
pytest -v
cd ..Résultat attendu : 5 tests passent avec des PASSED en vert.
# Créer un fichier pour déclencher votre premier push
echo "# Mon TP GitHub Actions" >> notes.md
# Commiter
git add notes.md
git commit -m "feat: premier commit — activation de la CI"
# Pusher vers votre fork
git push origin mainVérification : allez dans l'onglet Actions de votre fork GitHub. Vous devriez voir votre premier workflow en cours d'exécution (cercle orange → coche verte si tout va bien).
Si le workflow n'apparaît pas, vérifiez que vous avez bien pushé sur votre fork et non sur le repo original.
Avec l'extension GitHub Actions installée :
- Cliquez sur l'icône GitHub Actions dans le panneau gauche (icône en forme de cercle avec une flèche)
- Connectez-vous à votre compte GitHub si demandé
- Vous voyez maintenant vos workflows et leurs statuts directement dans VS Code — sans ouvrir le navigateur
Le dossier ressources/ contient une mini API Flask — NexaCloud API — que vous utiliserez comme cible de votre pipeline CI/CD tout au long du TP.
🎯 Pourquoi on fait ça ? Tout le monde a déjà vu un workflow GitHub Actions échouer sans comprendre pourquoi. Avant d'en écrire un, il faut savoir lire ce que GitHub exécute. Un workflow est un fichier YAML versionné dans le repo — il se comporte exactement comme du code : il peut avoir des bugs, des effets de bord, et il doit être relu. Cette étape vous donne les repères pour ne plus subir la CI mais la comprendre.
Un workflow GitHub Actions est un fichier .yml dans .github/workflows/. Il décrit quand se déclencher, où s'exécuter et quoi faire.
.github/
└── workflows/
└── mon-workflow.yml
Structure d'un workflow :
name: Nom du workflow # affiché dans l'onglet Actions
on: # déclencheurs
push:
branches: [main]
pull_request:
branches: [main]
jobs: # un ou plusieurs jobs (s'exécutent en parallèle par défaut)
mon-job:
runs-on: ubuntu-latest # le runner (machine virtuelle GitHub)
steps: # étapes du job (s'exécutent en séquence)
- name: Checkout
uses: actions/checkout@v4 # action réutilisable du Marketplace
- name: Dire bonjour
run: echo "Bonjour depuis GitHub Actions"Les concepts clés :
| Terme | Rôle |
|---|---|
on |
Définit quand le workflow se déclenche (push, PR, schedule, manuel…) |
jobs |
Unité de travail — chaque job tourne sur sa propre VM |
runs-on |
Le système d'exploitation du runner (ubuntu-latest, windows-latest, macos-latest) |
steps |
Les étapes séquentielles d'un job |
uses |
Utilise une action publiée sur le Marketplace (ex : actions/checkout@v4) |
run |
Exécute une commande shell directement |
Ouvrez le fichier .github/workflows/ci.yml déjà présent dans ce repo.
Répondez aux questions suivantes sans modifier le fichier :
- Sur quelle(s) branche(s) ce workflow se déclenche-t-il ?
- Combien de jobs contient-il ?
- Sur quel système d'exploitation tourne-t-il ?
- Quelle action installe Python ?
- Quelle commande lance les tests ?
Vérifiez vos réponses en allant dans l'onglet Actions de votre repo GitHub après votre premier push.
Créez le fichier .github/workflows/hello.yml :
name: Hello NexaCloud
on:
push:
branches: [main]
workflow_dispatch: # permet de déclencher manuellement depuis l'interface GitHub
jobs:
salutation:
runs-on: ubuntu-latest
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Informations sur l'environnement
run: |
echo "Repo : ${{ github.repository }}"
echo "Branche : ${{ github.ref_name }}"
echo "Commit : ${{ github.sha }}"
echo "Acteur : ${{ github.actor }}"
- name: Lister les fichiers du repo
run: ls -laCommitez et pushez. Observez l'exécution dans l'onglet Actions.
✏️ À vous
Ajoutez un step qui affiche la date et l'heure du runner avec
date.
Puis déclenchez le workflow manuellement depuis l'interface GitHub (bouton "Run workflow").
✅ Correction
name: Hello NexaCloud
on:
push:
branches: [main]
workflow_dispatch:
jobs:
salutation:
runs-on: ubuntu-latest
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Informations sur l'environnement
run: |
echo "Repo : ${{ github.repository }}"
echo "Branche : ${{ github.ref_name }}"
echo "Commit : ${{ github.sha }}"
echo "Acteur : ${{ github.actor }}"
- name: Lister les fichiers du repo
run: ls -la
- name: Date et heure du runner
run: date💡 Voir les pistes d'approfondissement
# Déclencher uniquement sur certains dossiers
on:
push:
paths:
- "ressources/**" # se déclenche seulement si un fichier de ressources/ change
# Déclencher sur un planning (cron)
on:
schedule:
- cron: "0 8 * * 1-5" # chaque jour ouvré à 8h UTC
# Exécuter un step seulement si une condition est remplie
- name: Alerter si branche main
if: github.ref_name == 'main'
run: echo "On est sur main !"🎯 Pourquoi on fait ça ? Sans CI, chaque développeur valide son code uniquement sur sa machine — et "ça marche chez moi" n'est pas suffisant. Un pipeline CI exécute automatiquement le lint (vérification du style) et les tests à chaque push. Si quelqu'un casse quelque chose, l'équipe le sait en 2 minutes, pas en 2 jours. C'est la première ligne de défense qualité d'une équipe DevOps.
Avant d'écrire le workflow, il faut comprendre ce qu'il va exécuter — et pourquoi ces deux outils sont complémentaires plutôt que redondants.
Le lint — vérifier le style et la forme
Le lint analyse votre code sans l'exécuter. Il cherche des problèmes de forme : lignes trop longues, espaces superflus, imports inutilisés, variables non définies… L'outil utilisé ici est flake8.
# Exemple de code qui passe les tests mais ÉCHOUE au lint
import os # ← flake8 signale : import non utilisé (F401)
def calcul(x,y): # ← flake8 signale : espace manquant après la virgule (E231)
result=x+y # ← flake8 signale : pas d'espace autour de = (E225)
return resultPourquoi ça compte ? Un code illisible est un code qui accumule les bugs silencieusement. Le lint force une discipline de style commune dans toute l'équipe — sans débat en code review sur les espaces et les indentations.
Les tests — vérifier le comportement
Les tests exécutent votre code avec des entrées connues et vérifient que la sortie est celle attendue. L'outil utilisé ici est pytest.
# Un test simple : appeler la route /health et vérifier la réponse
def test_health(client):
response = client.get("/health")
assert response.status_code == 200 # ← le serveur répond bien
assert response.get_json()["status"] == "healthy" # ← la valeur est correctePourquoi ça compte ? Le lint ne peut pas détecter qu'une fonction retourne
Noneau lieu d'un dictionnaire, ou qu'une route renvoie 500 sous certaines conditions. Les tests vérifient le comportement réel — et ils le revérifient à chaque modification du code.
Lint + tests dans la CI : la combinaison
git push
│
├─► job lint (10s) → est-ce que le code est propre ?
│
└─► job test (30s) → est-ce que le code fonctionne ?
Les deux jobs tournent en parallèle — un problème de style n'empêche pas les tests de tourner. Si l'un échoue, la PR est bloquée. C'est le filet de sécurité minimum avant tout déploiement.
Le Marketplace GitHub Actions fournit des milliers d'actions prêtes à l'emploi. Les plus utilisées pour Python :
| Action | Rôle |
|---|---|
actions/checkout@v4 |
Clone le repo dans le runner |
actions/setup-python@v5 |
Installe Python (version choisie) |
actions/cache@v4 |
Met en cache pip pour accélérer les builds |
Les contextes ${{ }} :
${{ github.sha }} # hash du commit courant
${{ github.ref_name }} # nom de la branche
${{ secrets.MON_SECRET }} # valeur d'un secret GitHub
${{ matrix.python }} # valeur courante dans une matrice de buildCréez .github/workflows/ci.yml (remplacez le fichier existant) :
name: CI — NexaCloud API
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# TODO: ajouter un step "Setup Python" avec actions/setup-python@v5
# version : "3.11"
# TODO: ajouter un step "Installer les dépendances"
# commande : pip install -r ressources/requirements.txt
# TODO: ajouter un step "Lancer les tests"
# commande : pytest ressources/ -vPushez et observez le résultat dans l'onglet Actions.
✅ Correction
name: CI — NexaCloud API
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer les dépendances
run: pip install -r ressources/requirements.txt
- name: Lancer les tests
run: pytest ressources/ -vAjoutez un second job lint dans votre ci.yml. Les deux jobs tournent en parallèle.
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer flake8
run: pip install flake8
# TODO: ajouter un step "Lint avec flake8"
# commande : flake8 ressources/ --config ressources/.flake8✅ Correction
name: CI — NexaCloud API
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer flake8
run: pip install flake8
- name: Lint avec flake8
run: flake8 ressources/ --config ressources/.flake8
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer les dépendances
run: pip install -r ressources/requirements.txt
- name: Lancer les tests
run: pytest ressources/ -vC'est l'exercice le plus important de l'étape.
- Introduisez une erreur de style dans
ressources/app.py: ajoutez une ligne avec des espaces superflus en fin de ligne ou une ligne trop longue (> 100 caractères). Commitez et pushez. - Observez : quel job échoue ? Que dit le message d'erreur dans les logs ?
- Corrigez l'erreur, pushez à nouveau.
- Introduisez une erreur dans un test : modifiez
test_app.pypour qu'un assert soit faux (ex :assert data["info"] == 999). Commitez et pushez. - Observez : cette fois quel job échoue ?
- Corrigez et vérifiez que la CI repasse au vert.
💡 Le but n'est pas de ne jamais casser la CI — c'est de savoir lire les logs et corriger rapidement. Cette compétence s'acquiert en cassant volontairement.
💡 Voir les pistes d'approfondissement
# Ajouter la couverture de tests
- name: Tests avec couverture
run: pytest ressources/ -v --cov=ressources --cov-report=term-missing
# Uploader le rapport de couverture comme artefact téléchargeable
- name: Générer le rapport HTML
run: pytest ressources/ --cov=ressources --cov-report=html
- name: Upload du rapport
uses: actions/upload-artifact@v4
with:
name: rapport-couverture
path: htmlcov/
# Mettre en cache les dépendances pip (accélère les builds suivants)
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('ressources/requirements.txt') }}🎯 Pourquoi on fait ça ? Un mot de passe ou une clé API codé en dur dans un workflow est visible par tous ceux qui ont accès au repo — et reste dans l'historique Git pour toujours. Les secrets GitHub sont chiffrés, masqués dans les logs et jamais exposés en clair. C'est la pratique minimale de sécurité que tout DevOps doit maîtriser avant de toucher à un environnement de production.
Secrets vs Variables :
| Secrets | Variables | |
|---|---|---|
| Valeur visible | ❌ jamais | ✅ oui |
| Usage | Mots de passe, clés API, tokens | URLs, noms de config non sensibles |
| Syntaxe | ${{ secrets.NOM }} |
${{ vars.NOM }} |
Créer un secret : Settings → Secrets and variables → Actions → New repository secret
Les environnements permettent de protéger les déploiements : un revieweur doit approuver avant que le job parte en production.
- Dans votre repo GitHub : Settings → Secrets and variables → Actions
- Créez un secret nommé
API_KEYavec la valeurnexacloud-secret-42 - Créez
.github/workflows/secrets.yml:
name: Demo Secrets
on:
workflow_dispatch:
jobs:
demo:
runs-on: ubuntu-latest
steps:
- name: Utiliser le secret
run: |
echo "La clé existe : ${{ secrets.API_KEY != '' }}"
# ⚠️ Cette ligne sera masquée dans les logs :
echo "Valeur : ${{ secrets.API_KEY }}"Déclenchez manuellement. Observez que GitHub masque automatiquement la valeur du secret dans les logs (remplacée par ***).
✅ Correction
name: Demo Secrets
on:
workflow_dispatch:
jobs:
demo:
runs-on: ubuntu-latest
env:
API_KEY: ${{ secrets.API_KEY }} # injection du secret comme variable d'environnement
steps:
- name: Vérifier que le secret est défini
run: |
if [ -z "$API_KEY" ]; then
echo "❌ Le secret API_KEY n'est pas défini"
exit 1
fi
echo "✅ Le secret API_KEY est défini (${#API_KEY} caractères)"
- name: Simuler un appel API authentifié
run: |
echo "Appel à l'API avec Authorization: Bearer ***"
# En vrai : curl -H "Authorization: Bearer $API_KEY" https://api.example.com- Dans votre repo : Settings → Environments → New environment
- Créez
staging(sans protection) - Créez
productionavec Required reviewers → ajoutez votre propre compte - Créez
.github/workflows/deploy.yml:
name: Deploy
on:
workflow_dispatch:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # utilise l'environnement staging
steps:
- name: Déployer en staging
run: echo "Déploiement en staging..."
deploy-production:
runs-on: ubuntu-latest
environment: production # TODO: ajouter la dépendance sur deploy-staging
needs: deploy-staging
steps:
- name: Déployer en production
run: echo "Déploiement en production !"Déclenchez manuellement. Observez que le job production est bloqué en attente de votre approbation.
✅ Correction
name: Deploy
on:
workflow_dispatch:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Déployer en staging
run: |
echo "✅ Déploiement en staging réussi"
echo "URL : https://staging.nexacloud.example.com"
deploy-production:
runs-on: ubuntu-latest
environment: production
needs: deploy-staging # attend que staging soit terminé ET approuvé
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Déployer en production
run: |
echo "🚀 Déploiement en production réussi"
echo "URL : https://nexacloud.example.com"🎯 Pourquoi ? Un
workflow_dispatchpeut exposer des inputs : des paramètres que l'opérateur renseigne au moment du déclenchement manuel. C'est utile pour choisir dynamiquement une cible de déploiement sans modifier le YAML à chaque fois.
Modifiez deploy.yml pour ajouter un input environment avec les choix staging et production. GitHub affichera un menu déroulant au moment du déclenchement.
Syntaxe des inputs workflow_dispatch :
on:
workflow_dispatch:
inputs:
mon_input:
description: "Description affichée dans l'UI"
required: true
default: "valeur_par_défaut"
type: choice
options:
- choix_1
- choix_2Objectif : n'avoir qu'un seul job deploy qui adapte son message selon ${{ inputs.environment }}.
✅ Correction
name: Deploy
on:
workflow_dispatch:
inputs:
environment:
description: "Environnement cible"
required: true
default: "staging"
type: choice
options:
- staging
- production
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }} # bloqué si production nécessite une approbation
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Déployer sur ${{ inputs.environment }}
run: |
if [ "${{ inputs.environment }}" = "production" ]; then
echo "🚀 Déploiement en PRODUCTION"
echo "URL : https://nexacloud.example.com"
else
echo "✅ Déploiement en STAGING"
echo "URL : https://staging.nexacloud.example.com"
fiCe qui se passe :
- GitHub affiche un menu déroulant
staging / productionau déclenchement manuel - La valeur choisie est injectée via
${{ inputs.environment }} - Si l'environnement
productiona des Required reviewers, GitHub bloque le job en attente d'approbation — même avec un seul job
💡 Voir les pistes d'approfondissement
# Définir des variables d'environnement au niveau du job
jobs:
build:
runs-on: ubuntu-latest
env:
APP_ENV: production
API_URL: ${{ vars.API_URL }} # variable non sensible
API_KEY: ${{ secrets.API_KEY }} # secret chiffré
# Partager des outputs entre jobs
jobs:
build:
outputs:
version: ${{ steps.version.outputs.tag }}
steps:
- id: version
run: echo "tag=1.2.3" >> $GITHUB_OUTPUT
deploy:
needs: build
steps:
- run: echo "Déploiement de la version ${{ needs.build.outputs.version }}"🎯 Pourquoi on fait ça ? La CI vérifie que le code est correct. Le CD (Continuous Deployment) va plus loin : il déploie automatiquement le code validé sur un serveur réel. Sans CD, le déploiement est une opération manuelle, risquée et souvent évitée — ce qui crée des releases rares et stressantes. Avec CD, déployer devient aussi banal qu'un
git push, et les problèmes sont détectés immédiatement en environnement réel.
Le pipeline complet CI → CD :
git push
│
▼
┌─────────────┐ ✅ ok ┌──────────────┐ ✅ approuvé ┌──────────────┐
│ lint + test │ ──────────▶ │ staging │ ────────────▶ │ production │
└─────────────┘ └──────────────┘ └──────────────┘
│ ❌ fail │ ❌ fail
▼ ▼
bloqué rollback
Action de déploiement Azure App Service :
- name: Déployer sur Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: "nexacloud-api"
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: .Si vous n'avez pas d'accès Azure actif, passez directement à l'exercice 4.2 et simulez le déploiement avec un
run: echo.
💶 Plan B1 recommandé (~13€/mois) Le plan gratuit (F1) n'a pas l'option "Always On" : l'app s'arrête après quelques minutes d'inactivité et redémarre avec un cold start de 5 à 10 secondes. Pour ce TP, utilisez le plan B1 qui garantit une app stable et disponible en permanence. Pensez à supprimer le resource group à la fin du TP pour arrêter la facturation :
az group delete --name rg-nexacloud-tp.
⚠️ Prérequis Azure — authentification basique Les nouveaux App Services Azure désactivent l'authentification basique par défaut depuis 2023, ce qui rend le publish profile inutilisable. Avant de configurer le secret GitHub, activez-la : App Service → Settings → Configuration → General settings → SCM Basic Auth Publishing Credentials → On → Save Puis re-téléchargez le publish profile (il est regénéré après cette modification).
# Dans votre terminal (Azure CLI doit être installé)
az login
RESOURCE_GROUP="rg-nexacloud-tp"
APP_NAME="nexacloud-api-$RANDOM" # nom unique obligatoire
LOCATION="francecentral"
# Créer le resource group
az group create --name "$RESOURCE_GROUP" --location "$LOCATION"
# Créer le plan App Service (B1 — nécessaire pour Always On et la stabilité)
az appservice plan create \
--name "plan-nexacloud" \
--resource-group "$RESOURCE_GROUP" \
--sku B1 \
--is-linux
# Créer l'App Service Python
az webapp create \
--name "$APP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--plan "plan-nexacloud" \
--runtime "PYTHON:3.11"
# Récupérer le publish profile (à coller dans les secrets GitHub)
az webapp deployment list-publishing-profiles \
--name "$APP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--xmlCopiez la sortie XML et créez un secret GitHub AZURE_WEBAPP_PUBLISH_PROFILE avec cette valeur.
Créez .github/workflows/cicd.yml — le pipeline complet en 3 jobs chainés :
name: CI/CD — NexaCloud API
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# ── Job 1 : Qualité ────────────────────────────────────────────────
qualite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r ressources/requirements.txt
- name: Lint
run: flake8 ressources/ --config ressources/.flake8
- name: Tests
run: pytest ressources/ -v --cov=ressources
# ── Job 2 : Staging ───────────────────────────────────────────────
staging:
runs-on: ubuntu-latest
needs: qualite # attend que le job qualite réussisse
environment: staging
if: github.ref_name == 'main' # uniquement sur la branche main
steps:
- uses: actions/checkout@v4
# TODO: ajouter le step de déploiement sur Azure App Service
# (remplacez app-name par votre nom d'application)
# ── Job 3 : Production ────────────────────────────────────────────
production:
runs-on: ubuntu-latest
needs: staging
environment: production
if: github.ref_name == 'main'
steps:
- uses: actions/checkout@v4
# TODO: même chose pour la production✅ Correction
name: CI/CD — NexaCloud API
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
qualite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer les dépendances
run: pip install -r ressources/requirements.txt
- name: Lint
run: flake8 ressources/ --config ressources/.flake8
- name: Tests avec couverture
run: pytest ressources/ -v --cov=ressources --cov-report=term-missing
staging:
runs-on: ubuntu-latest
needs: qualite
environment: staging
if: github.ref_name == 'main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer les dépendances
run: pip install -r ressources/requirements.txt
- name: Déployer sur Azure App Service (staging)
uses: azure/webapps-deploy@v3
with:
app-name: "nexacloud-api-staging"
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_STAGING }}
package: ressources/
production:
runs-on: ubuntu-latest
needs: staging
environment: production
if: github.ref_name == 'main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Installer les dépendances
run: pip install -r ressources/requirements.txt
- name: Déployer sur Azure App Service (production)
uses: azure/webapps-deploy@v3
with:
app-name: "nexacloud-api"
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ressources/💡 Voir les pistes d'approfondissement
# Smoke test post-déploiement : vérifier que l'app répond avant de valider
- name: Smoke test post-déploiement
run: |
sleep 30 # attendre que l'app démarre
curl --fail https://nexacloud-api.azurewebsites.net/health || exit 1
# Si ce test échoue, le job échoue → on peut déclencher un rollback
# Notifier Discord en cas d'échec (if: failure() = s'exécute seulement si un step précédent a échoué)
# Créer le webhook : channel Discord → ⚙️ Edit Channel → Integrations → Webhooks → New Webhook
- name: Notifier Discord
if: failure()
run: |
curl -X POST ${{ secrets.DISCORD_WEBHOOK_URL }} \
-H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "❌ Déploiement échoué",
"color": 15158332,
"fields": [
{"name": "Repo", "value": "${{ github.repository }}", "inline": true},
{"name": "Branche", "value": "${{ github.ref_name }}", "inline": true},
{"name": "Déclenché par", "value": "${{ github.actor }}", "inline": true}
],
"url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}]
}'🎯 Pourquoi on fait ça ? Attendre que la CI échoue pour découvrir une erreur de lint, c'est perdre 3 minutes à chaque fois — sans compter que le commit est déjà dans l'historique. Un hook
pre-commitexécute les mêmes vérifications avant le commit, en 2 secondes, directement sur votre machine. Le principe s'appelle "shift left" : détecter les problèmes le plus tôt possible dans le cycle. En équipe, c'est aussi une façon de standardiser les pratiques sans effort : tout le monde a les mêmes hooks, activés automatiquement.
La boucle de feedback :
Écrire du code
│
▼
git commit ──► hook pre-commit (2s, local) ──► ❌ bloqué si erreur
│
▼
git push ──► CI GitHub Actions (3 min, remote) ──► ❌ bloqué si erreur
│
▼
merge PR ──► CD déploiement (staging → prod)
Le framework pre-commit gère les hooks via un fichier .pre-commit-config.yaml versionné dans le repo — tout le monde a les mêmes hooks.
# Installer pre-commit (une seule fois sur votre machine)
pip install pre-commit
# Vérifier l'installation
pre-commit --versionCréez .pre-commit-config.yaml à la racine du repo :
repos:
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
args: [--config, ressources/.flake8]
files: ressources/.*\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace # supprime les espaces en fin de ligne
- id: end-of-file-fixer # ajoute un saut de ligne en fin de fichier
- id: check-yaml # valide les fichiers YAML (vos workflows !)
- id: check-merge-conflict # bloque si des marqueurs de conflit Git traînent# Activer les hooks dans votre repo local (à faire une seule fois)
pre-commit install
# Tester sur tous les fichiers existants
pre-commit run --all-files✏️ À vous
Introduisez un espace en fin de ligne dans
ressources/app.py, puis tentez ungit commit. Observez le hook bloquer le commit et corriger automatiquement le fichier.
✅ Correction
# .pre-commit-config.yaml — version complète
repos:
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
args: [--config, ressources/.flake8]
files: ressources/.*\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
- id: check-added-large-files
args: [--maxkb=500]
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort # trie automatiquement les imports Python
files: ressources/.*\.py$L'objectif est que les mêmes règles s'appliquent en local (hook) et dans la CI (workflow).
Comparez votre .pre-commit-config.yaml avec votre ci.yml :
| Vérification | Hook local | CI GitHub Actions |
|---|---|---|
| Espaces en fin de ligne | trailing-whitespace |
— |
| YAML valide | check-yaml |
— |
| Lint Python (flake8) | flake8 |
job lint |
| Tests | — | job test |
✏️ À vous
Les tests ne tournent pas en hook pre-commit (trop lents pour un commit). Mais vous pouvez les ajouter en hook
pre-push— qui se déclenche uniquement lors d'ungit push. Ajoutez cette configuration :
# Créer manuellement le hook pre-push
cat > .git/hooks/pre-push << 'EOF'
#!/bin/bash
echo "[pre-push] Lancement des tests..."
cd ressources
pytest -q
EXIT_CODE=$?
cd ..
if [ $EXIT_CODE -ne 0 ]; then
echo "❌ Tests échoués — push bloqué"
exit 1
fi
echo "✅ Tests passés — push autorisé"
EOF
chmod +x .git/hooks/pre-push✅ Correction — script setup-hooks.sh
#!/bin/bash
# setup-hooks.sh — Installe les hooks locaux (à lancer une fois par développeur)
set -e
echo "=== Installation des hooks locaux NexaCloud ==="
# 1. Installer pre-commit
if ! command -v pre-commit &>/dev/null; then
echo "Installation de pre-commit..."
pip install pre-commit --quiet
fi
# 2. Activer les hooks pre-commit
pre-commit install
echo "✅ Hooks pre-commit activés"
# 3. Installer le hook pre-push
cat > .git/hooks/pre-push << 'EOF'
#!/bin/bash
echo "[pre-push] Lancement des tests..."
cd ressources && pytest -q
EXIT_CODE=$?
cd ..
[ $EXIT_CODE -ne 0 ] && echo "❌ Tests échoués — push bloqué" && exit 1
echo "✅ Tests passés — push autorisé"
EOF
chmod +x .git/hooks/pre-push
echo "✅ Hook pre-push installé"
echo ""
echo "=== Hooks installés avec succès ==="
echo " pre-commit : flake8 + trailing-whitespace + check-yaml"
echo " pre-push : pytest"💡 Voir les pistes d'approfondissement
# Hook sur le message de commit (conventional commits)
# Force un format standardisé : feat:, fix:, ci:, docs:, chore:…
# Ça rend l'historique Git lisible et permet de générer des changelogs automatiques
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.2.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
# Format attendu : feat: ajouter la route /logs
# fix: corriger le calcul de pourcentage
# ci: mettre à jour le workflow de déploiement# Désactiver temporairement les hooks (à utiliser avec parcimonie)
# --no-verify bypasse TOUS les hooks — à éviter en équipe
git commit --no-verify -m "wip: travail en cours"
git push --no-verify🎯 Pourquoi on fait ça ? Un pipeline CI/CD qui fonctionne mais dont les dépendances ont 6 mois de retard, dont n'importe qui peut merger sans review, et dont une clé API traîne dans l'historique Git — c'est un repo qui donne une fausse impression de sécurité. Cette étape couvre les pratiques de gouvernance qu'une équipe DevOps met en place une fois pour toutes sur un repo : mises à jour automatiques, propriété des fichiers, règles de merge et détection des secrets oubliés.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Dependabot │ │ CODEOWNERS │ │Branch Protection│ │ Secret Scanning │
│ │ │ │ │ Rules │ │ │
│ Dépendances │ │ Qui doit │ │ CI obligatoire │ │ Détecte les │
│ toujours à jour │ │ reviewer quoi │ │ avant merge │ │ credentials │
│ via des PRs auto│ │ selon les │ │ + reviews │ │ accidentellement│
│ │ │ fichiers touchés│ │ obligatoires │ │ commités │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
Dependabot surveille vos dépendances et ouvre automatiquement des Pull Requests quand une nouvelle version est disponible. Cela couvre deux niveaux :
- Les actions GitHub utilisées dans vos workflows (
actions/checkout@v4,actions/setup-python@v5…) - Les packages Python de votre projet (
flask,pytest…)
Créez .github/dependabot.yml :
version: 2
updates:
# ── Mettre à jour les actions GitHub ──────────────────────────────
- package-ecosystem: "github-actions"
directory: "/" # cherche dans .github/workflows/
schedule:
interval: "weekly" # vérifie chaque semaine
labels:
- "dependencies"
- "github-actions"
# ── Mettre à jour les dépendances Python ──────────────────────────
- package-ecosystem: "pip"
directory: "/ressources" # cherche requirements.txt ici
schedule:
interval: "weekly"
labels:
- "dependencies"
- "python"
open-pull-requests-limit: 5 # max 5 PRs ouvertes en même tempsCommitez et pushez. Allez dans Insights → Dependency graph → Dependabot pour vérifier que Dependabot est activé.
✏️ À vous
Observez la liste des dépendances détectées dans l'onglet Dependabot. Si une mise à jour est disponible, Dependabot crée une PR automatiquement — allez voir son contenu. Que contient-elle ? Pourquoi est-ce utile en équipe ?
✅ Correction et explication
# .github/dependabot.yml — configuration complète commentée
version: 2
updates:
# GitHub Actions : surveille les "uses: action/nom@version" dans les workflows
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly" # "daily" ou "monthly" aussi possible
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "ci" # les commits Dependabot auront le préfixe "ci:"
# pip : surveille requirements.txt dans /ressources
- package-ecosystem: "pip"
directory: "/ressources"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "python"
open-pull-requests-limit: 5
commit-message:
prefix: "chore" # les commits auront le préfixe "chore:"Ce que contient une PR Dependabot :
- Le fichier modifié (
requirements.txtou le workflow.yml) - Le diff : ancienne version → nouvelle version
- Les release notes de la librairie
- Le résultat de la CI sur cette PR (vos tests tournent automatiquement)
En équipe : personne n'a à surveiller manuellement les CVE ou les changelogs. La PR Dependabot passe en revue comme n'importe quelle autre PR — si la CI est verte, on merge.
Le fichier .github/CODEOWNERS définit qui est automatiquement ajouté comme reviewer sur une PR selon les fichiers modifiés. Sans CODEOWNERS, quelqu'un peut modifier la CI ou les workflows de déploiement sans que personne de compétent ne le remarque.
Créez .github/CODEOWNERS :
# Syntaxe : <pattern de fichier> <@utilisateur ou @org/équipe>
# Par défaut : tout changement requiert une review de ces personnes
* @<votre-compte-github>
# Les workflows CI/CD ne peuvent être modifiés que par le lead DevOps
.github/workflows/ @<votre-compte-github>
# Le fichier de dépendances requiert une validation technique
ressources/requirements.txt @<votre-compte-github>
# Les fichiers de sécurité requièrent une double validation
.github/dependabot.yml @<votre-compte-github>
.github/CODEOWNERS @<votre-compte-github>
⚠️ Remplacez<votre-compte-github>par votre vrai nom d'utilisateur GitHub.
Commitez et pushez. Pour tester, créez une branche, modifiez un workflow, ouvrez une PR vers main — votre compte sera automatiquement ajouté comme reviewer requis.
✅ Correction et cas d'usage réel
# .github/CODEOWNERS — exemple équipe NexaCloud
# Règle par défaut (tout fichier non couvert par une règle plus précise)
* @lead-dev
# Infrastructure et CI/CD : validation obligatoire du lead DevOps
.github/ @lead-devops
terraform/ @lead-devops
docker/ @lead-devops
# Code applicatif : validation d'un dev senior
ressources/app.py @dev-senior
ressources/test_app.py @dev-senior
# Documentation : n'importe quel membre de l'équipe
*.md @lead-dev @dev-senior
# Fichiers sensibles : double validation obligatoire
.github/CODEOWNERS @lead-devops @lead-dev
ressources/requirements.txt @lead-devops @dev-senior
Comment ça fonctionne réellement :
- Quelqu'un ouvre une PR qui modifie
.github/workflows/ci.yml - GitHub voit que ce chemin est couvert par la règle
.github/→@lead-devops @lead-devopsest automatiquement ajouté comme Required reviewer- La PR ne peut pas être mergée sans son approbation — même si la CI est verte
Couplé aux branch protection rules (exercice suivant), CODEOWNERS devient contraignant pour de vrai.
Sans branch protection, tout ce que vous avez construit peut être contourné : quelqu'un peut pusher directement sur main, merger une PR sans review, ou merger même si la CI est rouge. Les branch protection rules rendent ces garde-fous obligatoires et font du workflow CI/CD une contrainte réelle, pas une suggestion.
Étape 1 — Activer la protection (interface GitHub)
- Allez dans Settings → Branches de votre repo
- Cliquez Add branch protection rule
- Dans Branch name pattern : tapez
main - Activez exactement ces options :
| Option | Pourquoi |
|---|---|
| ✅ Require a pull request before merging | Interdit tout push direct sur main — tout passe par une PR |
| ✅ Require approvals → 1 | Au moins 1 reviewer doit approuver — ce sera votre binôme à l'étape 6.5 |
| ✅ Dismiss stale reviews when new commits are pushed | Une review est invalidée si on repousse du code — évite d'approuver puis de modifier |
| ✅ Require status checks to pass before merging | La CI doit être verte avant merge |
| ✅ Require branches to be up to date before merging | La branche doit être à jour avec main |
| ✅ Do not allow bypassing the above settings | Même les admins respectent les règles |
- Cliquez Save changes
Étape 2 — Sélectionner le bon status check
Après avoir activé "Require status checks", GitHub vous demande quel job doit passer. Ce n'est pas automatique.
- Dans le champ de recherche qui apparaît, tapez
validate - Sélectionnez le job
validate(votre CI de validation)
⚠️ Sivalidaten'apparaît pas dans la liste, c'est que le CI n'a jamais tourné sur ce repo. Faites un push quelconque pour déclencher une première exécution, puis revenez sélectionner le check.
Étape 3 — Vérifier que les règles s'appliquent
# Tentative de push direct sur main → doit être bloqué
git push origin main
# → remote: error: GH006: Protected branch update failed for refs/heads/main.
# La bonne pratique à adopter désormais :
git checkout -b feat/ma-modification
# ... faire des modifications ...
git add .
git commit -m "feat: description de la modification"
git push origin feat/ma-modification
# → Ouvrir une PR sur GitHub → attendre la CI → demander une review → merger✅ Correction et comportements attendus
Flux protégé complet :
┌─────────────────────────────────────────────────────────────────┐
│ git push origin feat/... │
│ │ │
│ ▼ │
│ Ouvrir une Pull Request sur GitHub │
│ │ │
│ ├──► CI se déclenche automatiquement (job "validate") │
│ │ │ │
│ │ ❌ rouge → merge BLOQUÉ (status check requis) │
│ │ ✅ vert → merge possible si review aussi ok │
│ │ │
│ ├──► Review demandée (CODEOWNERS assigne automatiquement)│
│ │ │ │
│ │ ⏳ en attente → merge BLOQUÉ │
│ │ ✅ approuvée → merge possible si CI aussi ok │
│ │ │
│ └──► Les deux ✅ → bouton Merge devient actif │
└─────────────────────────────────────────────────────────────────┘
Note sur les repos personnels : sans collaborateurs, vous êtes à la fois auteur et seul reviewer possible. GitHub vous empêche d'approuver votre propre PR. Pour tester seul, activez "Allow self-review" dans les paramètres de la règle — ou mieux : passez à l'exercice 6.5 en binôme, qui est la vraie condition de test.
Maintenant que la branche est protégée et la CI active, faites une vraie modification qui passe par le flux complet : branche → code → test → push → PR → CI → merge.
Ce que vous allez ajouter : un endpoint /logs/stats qui retourne le total de toutes les entrées de log.
Étape 1 — Créer une branche
git checkout -b feat/endpoint-statsÉtape 2 — Ajouter l'endpoint dans ressources/app.py
Ajoutez cette fonction avant le bloc if __name__ == "__main__": :
@app.route("/logs/stats")
def logs_stats():
total = sum(LOG_SUMMARY.values())
return jsonify({
"total": total,
"breakdown": LOG_SUMMARY
})Étape 3 — Écrire le test dans ressources/test_app.py
def test_logs_stats(client):
"""La route /logs/stats retourne le total et le détail."""
response = client.get("/logs/stats")
assert response.status_code == 200
data = response.get_json()
assert "total" in data
assert "breakdown" in data
assert data["total"] == 185 # 142 + 28 + 12 + 3Étape 4 — Vérifier en local avant de pusher
cd ressources
pytest -v # tous les tests doivent passer, dont le nouveau
flake8 . --config .flake8 # aucune erreur de lint
cd ..💡 C'est exactement ce que le hook pre-commit fait automatiquement si vous l'avez installé à l'étape 5. Vous devriez voir les checks passer avant même de commiter.
Étape 5 — Commiter et pusher
git add ressources/app.py ressources/test_app.py
git commit -m "feat: ajouter endpoint /logs/stats"
git push origin feat/endpoint-statsÉtape 6 — Ouvrir la Pull Request sur GitHub
- GitHub affiche une bannière "Compare & pull request" → cliquez dessus
- Titre :
feat: ajouter endpoint /logs/stats - Description : expliquez en une phrase ce que fait l'endpoint et pourquoi
- Cliquez Create pull request
Étape 7 — Observer la CI
Dans la PR, vous voyez le status check validate démarrer. Observez :
- Le cercle orange pendant l'exécution
- Le détail des étapes si vous cliquez sur Details
- La coche verte (ou croix rouge) au bout de ~1 minute
Le bouton Merge pull request reste grisé tant que la CI n'est pas verte et qu'aucune review n'a été approuvée.
✏️ À vous
Introduisez volontairement une erreur dans votre test (changez
185par999), committez et pushez. Observez la CI passer au rouge et le merge se bloquer. Corrigez puis pushez à nouveau pour voir la CI repasser au vert.
✅ Correction — endpoint et test complets
ressources/app.py — section ajoutée :
@app.route("/logs/stats")
def logs_stats():
"""Retourne le total de toutes les entrées et le détail par niveau."""
total = sum(LOG_SUMMARY.values())
return jsonify({
"total": total,
"breakdown": LOG_SUMMARY
})ressources/test_app.py — test ajouté :
def test_logs_stats(client):
"""La route /logs/stats retourne le total et le détail des niveaux."""
response = client.get("/logs/stats")
assert response.status_code == 200
data = response.get_json()
# Structure
assert "total" in data
assert "breakdown" in data
# Valeurs : 142 + 28 + 12 + 3 = 185
assert data["total"] == 185
assert data["breakdown"]["critical"] == 3Vérification locale complète :
cd ressources
pytest -v
# test_app.py::test_index_status PASSED
# test_app.py::test_health PASSED
# test_app.py::test_logs_summary_structure PASSED
# test_app.py::test_logs_summary_values PASSED
# test_app.py::test_logs_critical_alerte PASSED
# test_app.py::test_logs_stats PASSED ← nouveau
# 6 passed in 0.32sC'est l'exercice le plus proche des conditions réelles. Chaque étudiant travaille sur son propre fork et valide la PR de son binôme — exactement comme en équipe.
Étape 1 — Trouver votre binôme
Formez des paires. Chaque étudiant a son propre fork avec son URL GitHub : https://github.com/<son-compte>/tp-github-actions
Étape 2 — S'ajouter mutuellement comme collaborateur
Sur votre repo :
- Settings → Collaborators → Add people
- Cherchez le nom GitHub de votre binôme
- Choisissez le rôle Write (peut pusher et créer des PRs, mais pas modifier les settings)
- Votre binôme reçoit une invitation par email — il doit l'accepter
Étape 3 — Mettre à jour CODEOWNERS pour inclure votre binôme
Sur votre repo, modifiez .github/CODEOWNERS :
# Tout changement requiert une approbation de votre binôme
* @<votre-compte> @<compte-binome>
# Les workflows ne peuvent être modifiés qu'avec validation des deux
.github/workflows/ @<votre-compte> @<compte-binome>
# Le code applicatif
ressources/ @<votre-compte> @<compte-binome>
Committez et pushez directement sur main — c'est une modification de gouvernance.
⚠️ Après ce commit, vous ne pourrez plus pusher surmaindirectement (branch protection). Assurez-vous que CODEOWNERS est correct avant.
Étape 4 — Faire la review croisée
Votre binôme ouvre une PR sur son repo (l'exercice 6.4 avec l'endpoint /logs/stats).
En tant que reviewer sur son repo :
-
Allez sur
https://github.com/<binome>/tp-github-actions/pulls -
Ouvrez sa PR
-
Cliquez Files changed — lisez le diff du code et du test
-
Cliquez Review changes puis :
- Comment : si vous avez des questions sans bloquer
- Request changes : si quelque chose doit être corrigé avant merge
- Approve : si tout est correct
-
Une fois approuvée et la CI verte, votre binôme peut merger
Ce que vous vérifiez en tant que reviewer :
□ Le nouvel endpoint retourne le bon status code (200)
□ Le test vérifie la structure ET les valeurs
□ Le code ne casse pas les tests existants (CI verte)
□ Pas d'import inutile, pas de ligne trop longue (lint vert)
□ Le nom de la branche et le message de commit sont lisibles
□ La description de la PR explique le changement
✏️ À vous
Laissez un commentaire sur la PR de votre binôme — même si tout est correct, notez ce que vous avez vérifié. En entreprise, une review sans commentaire est une review suspecte.
✅ Exemple de review bien rédigée
Commentaire global (onglet "Review changes") :
✅ Approve
J'ai vérifié :
- L'endpoint /logs/stats répond 200 avec la bonne structure (total + breakdown)
- Le calcul 142 + 28 + 12 + 3 = 185 est correct
- Le test couvre la structure et les valeurs
- La CI est verte : lint et 6 tests passent
- Pas d'effet de bord sur les autres routes
Suggestion mineure (non bloquante) : on pourrait ajouter un assert sur
data["breakdown"]["info"] == 142 pour couvrir tous les niveaux.
Commentaire inline sur une ligne de code :
# Sur la ligne : assert data["total"] == 185
# Commentaire : bien de tester la valeur exacte — mais si LOG_SUMMARY change
# un jour, ce test cassera silencieusement. Envisager de calculer le total
# dynamiquement : assert data["total"] == sum(LOG_SUMMARY.values())GitHub scanne automatiquement les commits à la recherche de patterns reconnus comme des credentials (clés AWS, tokens GitHub, clés Azure, mots de passe courants…). Si un secret est détecté, GitHub envoie une alerte et, pour certains fournisseurs, révoque le token automatiquement.
Activer secret scanning :
- Settings → Code security and analysis
- Activez Secret scanning (peut nécessiter un repo public ou GitHub Advanced Security)
- Activez aussi Push protection — bloque le push si un secret est détecté avant même qu'il arrive dans l'historique
Tester la détection (avec un faux secret) :
⚠️ Utilisez uniquement des faux secrets au format reconnu — jamais de vraies clés.
# Créer un fichier avec un faux token GitHub (format reconnu par le scanner)
echo "GITHUB_TOKEN=ghp_azertyuiopqsdfghjklmwxcvbn123456789" > test-secret.txt
git add test-secret.txt
git commit -m "test: vérification du secret scanning"
git push origin mainSi Push protection est activé, GitHub bloque le push avec un message explicite. Sinon, une alerte apparaît dans Security → Secret scanning alerts.
# Nettoyer après le test
git rm test-secret.txt
git commit -m "chore: suppression du fichier de test"
git push origin main💡 Important : supprimer le fichier ne supprime pas le secret de l'historique Git. En production, si un vrai secret est commité, il faut le révoquer immédiatement côté fournisseur (GitHub, AWS, Azure…) — la rotation du secret est prioritaire sur le nettoyage de l'historique.
✅ Correction — Checklist sécurité complète d'un repo
Voici la checklist qu'une équipe DevOps applique à chaque nouveau repo :
Sécurité dépendances
☑ .github/dependabot.yml configuré (github-actions + pip/npm)
☑ Dependabot activé dans Settings → Code security
Gouvernance des changements
☑ .github/CODEOWNERS configuré avec les bons responsables
☑ Branch protection sur main :
- Pull request obligatoire
- 1 approbation minimum
- CI obligatoire avant merge
- Branches à jour avant merge
- Bypass interdit même pour les admins
Détection des secrets
☑ Secret scanning activé
☑ Push protection activé
☑ .gitignore inclut .env, *.key, *.pem, credentials*
Bonnes pratiques
☑ Pas de secrets dans le code — uniquement via ${{ secrets.NOM }}
☑ Actions épinglées à une version précise (ex: @v4 et non @main)
☑ Principe du moindre privilège sur les tokens (write-all désactivé)
Épingler les actions à un hash de commit (niveau avancé) :
# Moins pratique mais plus sûr : un tag peut être déplacé, un hash non
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2💡 Voir les pistes d'approfondissement
# Ajouter CodeQL pour l'analyse de sécurité du code Python
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1" # chaque lundi matin
jobs:
analyse:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Initialiser CodeQL
uses: github/codeql-action/init@v3
with:
languages: python
- name: Analyser
uses: github/codeql-action/analyze@v3# Ajouter un badge de statut CI dans le README
# Copiez cette ligne en haut de votre README.md :

Cette section nécessite d'avoir terminé les étapes 1 à 5.
name: Matrix CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
fail-fast: false # continuer même si une version échoue
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r ressources/requirements.txt
- run: pytest ressources/ -vGitHub lancera 3 jobs en parallèle, un par version Python. Très utile pour s'assurer de la compatibilité.
Un workflow réutilisable évite de copier-coller les mêmes steps dans plusieurs workflows.
Créez .github/workflows/reusable-tests.yml :
name: Tests réutilisables
on:
workflow_call: # ce workflow est appelé par d'autres workflows
inputs:
python-version:
required: true
type: string
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- run: pip install -r ressources/requirements.txt
- run: pytest ressources/ -vAppelez-le depuis votre cicd.yml :
jobs:
qualite:
uses: ./.github/workflows/reusable-tests.yml
with:
python-version: "3.11"Cette section reprend exactement la même progression que les étapes 1 à 6, mais avec un projet Node.js/Express au lieu de Python/Flask. Elle peut être faite à la place ou en complément.
Le dossier
ressources-node/contient la même API NexaCloud, réécrite en Express.
| Python | Node.js |
|---|---|
| Flask | Express |
| pytest | Jest + Supertest |
| flake8 | ESLint |
requirements.txt |
package.json |
pip install |
npm install |
| pre-commit (flake8 hook) | Husky + lint-staged |
⏱️ Cette section prend environ 10 minutes.
# Vérifier que Node est installé (version 18 minimum recommandée)
node --version
npm --version
# Aller dans le dossier Node
cd ressources-node
# Installer les dépendances
npm install
# Lancer les tests pour vérifier que tout fonctionne
npm test
# Lancer le lint
npm run lintRésultat attendu : 4 tests passent (un par route), lint sans erreur.
🎯 Pourquoi on fait ça ? En Python on utilisait
pip installdans la CI — en Node l'équivalent estnpm ci. La différence n'est pas anodine :npm installpeut résoudre de nouvelles versions à la volée,npm ciinstalle exactement ce qui est verrouillé danspackage-lock.json. En CI, on veut de la reproductibilité, pas des surprises. Cette étape vous fait construire un pipeline Node avec les deux outils phares de l'écosystème : ESLint (lint) et Jest (tests), en deux jobs parallèles exactement comme pour Python.
ESLint est l'équivalent de flake8 pour JavaScript. Il vérifie le style et détecte les erreurs courantes sans exécuter le code :
// Exemple de code qui passe les tests mais ÉCHOUE au lint
const unused = 'test' // ← ESLint : no-unused-vars (error)
const x=1 // ← ESLint : no space around = (error)
var ancien = true // ← ESLint : prefer const (warn)Jest est le framework de tests Node. Supertest permet d'appeler les routes Express dans les tests sans démarrer de vrai serveur :
// Un test Jest avec Supertest
it('répond 200', async () => {
const res = await request(app).get('/health');
expect(res.statusCode).toBe(200);
expect(res.body.status).toBe('healthy');
});npm ci vs npm install dans la CI :
npm install |
npm ci |
|
|---|---|---|
| Résout les versions | Oui (peut upgrader) | Non (respecte package-lock.json) |
| Reproductible | ❌ | ✅ |
| Vitesse en CI | Moyen | Rapide |
| Usage recommandé | Dev local | CI/CD |
Créez .github/workflows/ci-node.yml :
name: CI — NexaCloud API (Node)
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: ressources-node/package-lock.json
# TODO: ajouter un step "Installer les dépendances"
# commande : npm ci (dans working-directory: ressources-node)
# TODO: ajouter un step "Lint avec ESLint"
# commande : npm run lint (dans working-directory: ressources-node)
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: ressources-node/package-lock.json
# TODO: ajouter les steps pour installer et lancer les testsPushez et observez les deux jobs tourner en parallèle dans l'onglet Actions.
✏️ À vous
- Introduisez une variable non utilisée dans
ressources-node/app.js(ex :const unused = 'test';). Commitez et pushez. Quel job échoue ? Que dit le message d'erreur ?- Corrigez l'erreur, pushez à nouveau.
- Modifiez un
expectdansapp.test.jspour qu'il soit faux. Quel job échoue cette fois ?
✅ Correction
name: CI — NexaCloud API (Node)
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: ressources-node/package-lock.json
- name: Installer les dépendances
run: npm ci
working-directory: ressources-node
- name: Lint avec ESLint
run: npm run lint
working-directory: ressources-node
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: ressources-node/package-lock.json
- name: Installer les dépendances
run: npm ci
working-directory: ressources-node
- name: Tests avec couverture
run: npm test
working-directory: ressources-nodeErreur ESLint typique dans les logs :
/home/runner/work/.../app.js
7:7 error 'unused' is defined but never used no-unused-vars
✖ 1 problem (1 error, 0 warnings)
Erreur Jest typique dans les logs :
● GET /health › retourne le statut healthy
expect(received).toBe(expected)
Expected: "broken"
Received: "ok"
Le comportement est identique à la CI Python : les deux jobs tournent en parallèle, chacun peut échouer indépendamment.
💡 Voir les pistes d'approfondissement
# Uploader le rapport de couverture Jest comme artefact
- name: Générer le rapport de couverture
run: npm test -- --coverage --coverageReporters=html
working-directory: ressources-node
- name: Upload du rapport
uses: actions/upload-artifact@v4
with:
name: rapport-couverture-node
path: ressources-node/coverage/
# Mettre en cache node_modules (plus agressif que le cache npm)
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ressources-node/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('ressources-node/package-lock.json') }}🎯 Pourquoi on fait ça ? Une API Node déployée en production tourne sur une version précise de Node. Mais vos collègues ont peut-être Node 18 sur leur machine, le runner CI utilise Node 20, et Azure App Service supporte Node 22. Un code qui fonctionne sur une version peut silencieusement casser sur une autre — à cause de changements dans les APIs natives, de comportements légèrement différents, ou de dépendances qui n'ont pas été testées. Le matrix build lance le même pipeline en parallèle sur plusieurs versions Node : c'est votre filet de sécurité de compatibilité.
Ajoutez un job test-matrix dans ci-node.yml :
# TODO: créer un job test-matrix avec strategy.matrix sur node-version: ["18", "20", "22"]
# et fail-fast: false
# Les steps sont identiques au job test existant,
# sauf que node-version utilise ${{ matrix.node-version }}✅ Correction
test-matrix:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ["18", "20", "22"]
fail-fast: false # continuer même si une version échoue
steps:
- uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Installer les dépendances
run: npm ci
working-directory: ressources-node
- name: Tests Node ${{ matrix.node-version }}
run: npm test
working-directory: ressources-nodeGitHub lance 3 jobs en parallèle. Dans l'onglet Actions vous voyez :
test-matrix (18) ✅
test-matrix (20) ✅
test-matrix (22) ✅
Équivalent exact du matrix build Python avec python-version: ["3.10", "3.11", "3.12"].
💡 Voir les pistes d'approfondissement
# Tester sur plusieurs OS en plus des versions Node
strategy:
matrix:
node-version: ["18", "20"]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
# → 4 combinaisons (2 versions × 2 OS)
# Utile si votre app doit tourner sur Windows Server (Azure Windows App Service)🎯 Pourquoi on fait ça ? Le fichier
package.jsonliste vos dépendances avec des versions précises — mais ces versions vieillissent. Une dépendance non mise à jour pendant 6 mois peut exposer des CVE connues, ou créer des incompatibilités avec d'autres packages. Sans automatisation, personne ne surveille ça. Dependabot surveillepackage.jsonetpackage-lock.json, et ouvre des PRs de mise à jour automatiquement. La différence avec Python : npm a deux niveaux de dépendances (dependenciesetdevDependencies) et un fichier de lock plus complexe — Dependabot gère les deux.
Ouvrez .github/dependabot.yml (celui que vous avez créé à l'étape 6.1) et ajoutez ce bloc à la suite de la config pip :
# TODO: ajouter un bloc package-ecosystem: "npm"
# directory: "/ressources-node"
# schedule weekly
# labels: dependencies + node
# open-pull-requests-limit: 5✏️ À vous
Les versions dans
package.jsonsont volontairement légèrement en retard. Dependabot devrait ouvrir des PRs dans les jours suivant l'activation. Regardez le diff d'une PR Dependabot npm : quels fichiers sont modifiés ? Pourquoipackage-lock.jsonchange-t-il aussi ?
✅ Correction
# ── Mettre à jour les dépendances npm ─────────────────────────────
- package-ecosystem: "npm"
directory: "/ressources-node"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "node"
open-pull-requests-limit: 5
commit-message:
prefix: "chore"Ce que Dependabot modifie dans une PR npm :
package.json: la version de la dépendance (ex :"express": "4.18.2"→"express": "4.19.2")package-lock.json: le graph complet des dépendances transitives recalculé
Pourquoi package-lock.json change ? Parce qu'Express lui-même dépend d'autres packages. Une mise à jour d'Express peut tirer des nouvelles versions de body-parser, path-to-regexp, etc. — tout ça est recalculé et verrouillé dans le lock file.
En équipe : la PR Dependabot passe en CI comme n'importe quelle autre PR. Si Jest + ESLint sont verts, on merge. Si une mise à jour casse les tests, on le voit immédiatement — avant que ça arrive en production.
💡 Voir les pistes d'approfondissement
# Ignorer les mises à jour majeures (breaking changes) sur certains packages
- package-ecosystem: "npm"
directory: "/ressources-node"
schedule:
interval: "weekly"
ignore:
- dependency-name: "express"
update-types: ["version-update:semver-major"]
# Dependabot proposera les mises à jour mineures et patches d'Express,
# mais ignorera les versions majeures (ex: Express 4 → Express 5)
# qui nécessitent une migration manuelle🎯 Pourquoi on fait ça ? En Python vous avez utilisé
pre-commitpour bloquer les commits avec des erreurs de style avant qu'elles n'arrivent dans la CI. En Node l'écosystème utilise deux outils complémentaires : Husky gère les hooks Git (commepre-commitle fait), et lint-staged n'exécute le lint que sur les fichiers modifiés par le commit en cours — pas sur l'ensemble du projet. C'est crucial sur un grand codebase : linter 500 fichiers à chaque commit serait trop lent. lint-staged cible exactement les fichiers touchés.
git commit
│
▼
Hook pre-commit (Husky)
│
▼
lint-staged ──► ESLint --fix sur les fichiers .js modifiés
Jest --findRelatedTests sur les tests liés
│
❌ erreur → commit bloqué
✅ ok → commit créé
La différence avec pre-commit Python :
pre-commit (Python) |
Husky + lint-staged (Node) | |
|---|---|---|
| Config | .pre-commit-config.yaml |
package.json + .husky/ |
| Scope | Tous les fichiers | Fichiers modifiés seulement |
| Auto-fix | Selon le hook | eslint --fix intégré |
| Installation | pre-commit install |
npx husky init |
cd ressources-node
# Installer les outils
npm install --save-dev husky lint-staged
# Initialiser Husky (crée .husky/ et ajoute le script prepare dans package.json)
npx husky initCréez/remplacez .husky/pre-commit avec ce contenu :
#!/bin/sh
cd ressources-node
npx lint-stagedAjoutez la clé lint-staged dans package.json :
{
"lint-staged": {
"*.js": [
"eslint --fix",
"jest --bail --findRelatedTests --passWithNoTests"
]
}
}# Rendre le hook exécutable
chmod +x .husky/pre-commit
# Tester sur tous les fichiers
npx lint-staged✏️ À vous
Introduisez un espace manquant à la fin d'une ligne dans
app.js, puis tentez ungit commit. Observez ESLint corriger automatiquement le fichier (grâce à--fix) et le commit réussir après la correction. C'est différent depre-commitPython qui bloque et demande de corriger manuellement.
✅ Correction — package.json complet avec lint-staged
{
"name": "nexacloud-api",
"version": "1.1.0",
"scripts": {
"start": "node app.js",
"test": "jest --coverage",
"lint": "eslint . --ext .js",
"prepare": "husky"
},
"dependencies": {
"express": "4.18.2"
},
"devDependencies": {
"eslint": "8.44.0",
"husky": "9.0.11",
"jest": "29.5.0",
"lint-staged": "15.2.2",
"supertest": "6.3.3"
},
"jest": {
"testEnvironment": "node"
},
"lint-staged": {
"*.js": [
"eslint --fix",
"jest --bail --findRelatedTests --passWithNoTests"
]
}
}Ce qui se passe lors d'un commit :
- Husky déclenche le hook
pre-commit - lint-staged identifie les fichiers
.jsmodifiés (ex :app.js) eslint --fixcorrige automatiquement les problèmes de style (espaces, quotes…)jest --findRelatedTestslance uniquement les tests qui importentapp.js- Si Jest échoue → commit bloqué. Si tout passe → commit créé.
Avantage sur Python : eslint --fix corrige les erreurs de style automatiquement — vous n'avez pas besoin de corriger manuellement avant de re-commiter.
💡 Voir les pistes d'approfondissement
# Ajouter un hook pre-push pour lancer tous les tests (pas seulement les tests liés)
# .husky/pre-push
#!/bin/sh
cd ressources-node
echo "[pre-push] Lancement de tous les tests..."
npm test// Étendre lint-staged à d'autres types de fichiers
"lint-staged": {
"*.js": ["eslint --fix", "jest --bail --findRelatedTests --passWithNoTests"],
"*.json": ["prettier --write"],
"*.md": ["prettier --write"]
}🎯 Pourquoi on fait ça ? Vous avez un pipeline CI qui vérifie le code — mais il ne déploie rien. Le CD (Continuous Deployment) enchaîne automatiquement : code validé → déploiement en staging → approbation → déploiement en production. Pour Node.js sur Azure App Service, la mécanique est identique à Python : même action
azure/webapps-deploy@v3, même séquence staging → production avec environnements protégés. Seule différence : le runtime Azure passe dePYTHON:3.11àNODE:20-lts, et la commande de démarrage estnode app.jsau lieu degunicorn.
Azure App Service Node démarre l'app avec la commande définie dans package.json :
"scripts": {
"start": "node app.js" // ← Azure exécute npm start
}Le pipeline complet reste identique à Python :
git push
│
▼
┌──────────┐ ✅ ┌─────────┐ ✅ approuvé ┌────────────┐
│ qualité │ ───► │ staging │ ────────────► │ production │
└──────────┘ └─────────┘ └────────────┘
Si vous n'avez pas d'accès Azure actif, passez directement à l'exercice N5.2 et simulez le déploiement avec un
run: echo.
APP_NAME="nexacloud-node-$RANDOM"
RESOURCE_GROUP="rg-nexacloud-tp"
# Créer l'App Service Node (le plan existe déjà depuis l'étape 4)
az webapp create \
--name "$APP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--plan "plan-nexacloud" \
--runtime "NODE:20-lts"
# Récupérer le publish profile
az webapp deployment list-publishing-profiles \
--name "$APP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--xmlCréez un secret GitHub AZURE_WEBAPP_PUBLISH_PROFILE_NODE avec la sortie XML.
⚠️ Pensez à activer l'authentification basique comme à l'étape 4 : App Service → Settings → Configuration → General settings → SCM Basic Auth Publishing Credentials → On → Save
Créez .github/workflows/cicd-node.yml :
name: CI/CD — NexaCloud API (Node)
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
qualite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: ressources-node/package-lock.json
- name: Installer les dépendances
run: npm ci
working-directory: ressources-node
- name: Lint
run: npm run lint
working-directory: ressources-node
- name: Tests avec couverture
run: npm test
working-directory: ressources-node
staging:
runs-on: ubuntu-latest
needs: qualite
environment: staging
if: github.ref_name == 'main'
steps:
- uses: actions/checkout@v4
# TODO: ajouter le step de déploiement azure/webapps-deploy@v3
# app-name: "nexacloud-node-staging"
# publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_NODE_STAGING }}
# package: ressources-node/
production:
runs-on: ubuntu-latest
needs: staging
environment: production
if: github.ref_name == 'main'
steps:
- uses: actions/checkout@v4
# TODO: même chose pour la production✅ Correction
name: CI/CD — NexaCloud API (Node)
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
qualite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: ressources-node/package-lock.json
- name: Installer les dépendances
run: npm ci
working-directory: ressources-node
- name: Lint
run: npm run lint
working-directory: ressources-node
- name: Tests avec couverture
run: npm test
working-directory: ressources-node
staging:
runs-on: ubuntu-latest
needs: qualite
environment: staging
if: github.ref_name == 'main'
steps:
- uses: actions/checkout@v4
- name: Déployer sur Azure App Service (staging)
uses: azure/webapps-deploy@v3
with:
app-name: "nexacloud-node-staging"
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_NODE_STAGING }}
package: ressources-node/
production:
runs-on: ubuntu-latest
needs: staging
environment: production
if: github.ref_name == 'main'
steps:
- uses: actions/checkout@v4
- name: Déployer sur Azure App Service (production)
uses: azure/webapps-deploy@v3
with:
app-name: "nexacloud-node"
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_NODE }}
package: ressources-node/Ce qui change par rapport au pipeline Python :
actions/setup-python@v5→actions/setup-node@v4pip install -r requirements.txt→npm ciflake8→npm run lint(ESLint)pytest→npm test(Jest)- Le runtime Azure :
PYTHON:3.11→NODE:20-lts - La commande de démarrage :
gunicorn→npm start
La structure du pipeline (3 jobs chainés, environments protégés, needs:) est identique.
💡 Voir les pistes d'approfondissement
# Smoke test post-déploiement Node
- name: Smoke test
run: |
sleep 20
curl --fail https://nexacloud-node.azurewebsites.net/health || exit 1
# Notification Discord en cas d'échec (identique à Python)
- name: Notifier Discord
if: failure()
run: |
curl -X POST ${{ secrets.DISCORD_WEBHOOK_URL }} \
-H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "❌ Déploiement Node échoué",
"color": 15158332,
"fields": [
{"name": "Repo", "value": "${{ github.repository }}", "inline": true},
{"name": "Branche", "value": "${{ github.ref_name }}", "inline": true}
]
}]
}'🎯 Pourquoi on fait ça ? Même exercice qu'en Python (étape 6.4) — mais en JavaScript. L'objectif est double : pratiquer le flux complet branche → PR → CI → merge avec du code Node, et comparer la syntaxe Express/Jest avec Flask/pytest sur une tâche identique. Voir les deux implémentations côte à côte est l'un des meilleurs moyens de comprendre ce qui est propre à l'outillage et ce qui relève de la logique métier.
Étape 1 — Créer une branche
git checkout -b feat/node-endpoint-statsÉtape 2 — Ajouter l'endpoint dans ressources-node/app.js
// TODO: ajouter la route GET /logs/stats
// Elle doit retourner { total: <somme de toutes les valeurs>, breakdown: LOG_SUMMARY }
// Utiliser Object.values() et reduce() pour calculer le totalÉtape 3 — Écrire le test dans ressources-node/app.test.js
// TODO: ajouter un describe('GET /logs/stats', ...) avec un test qui vérifie :
// - statusCode 200
// - body.total === 185 (142 + 28 + 12 + 3)
// - body.breakdown.critical === 3Étape 4 — Vérifier en local
cd ressources-node
npm test # tous les tests doivent passer, dont le nouveau
npm run lint # aucune erreur de lintÉtape 5 — Commiter, pusher, ouvrir la PR
git add ressources-node/app.js ressources-node/app.test.js
git commit -m "feat: ajouter endpoint /logs/stats (Node)"
git push origin feat/node-endpoint-statsOuvrez la PR sur GitHub et observez la CI se déclencher.
✅ Correction
ressources-node/app.js — route à ajouter avant module.exports :
app.get('/logs/stats', (req, res) => {
const total = Object.values(LOG_SUMMARY).reduce((a, b) => a + b, 0);
res.json({ total, breakdown: LOG_SUMMARY });
});ressources-node/app.test.js — test à ajouter :
describe('GET /logs/stats', () => {
it('retourne le total et le détail', async () => {
const res = await request(app).get('/logs/stats');
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('total', 185); // 142 + 28 + 12 + 3
expect(res.body).toHaveProperty('breakdown');
expect(res.body.breakdown.critical).toBe(3);
});
});Résultat des tests :
PASS app.test.js
GET /
✓ répond 200 avec le bon service
GET /health
✓ retourne le statut healthy
GET /logs/summary
✓ retourne les 4 niveaux de log
GET /logs/critical
✓ détecte une alerte critique
GET /logs/stats
✓ retourne le total et le détail ← nouveau
Test Suites: 1 passed
Tests: 5 passed
Comparaison Python ↔ Node sur cet endpoint :
# Python / Flask
@app.route("/logs/stats")
def logs_stats():
total = sum(LOG_SUMMARY.values())
return jsonify({"total": total, "breakdown": LOG_SUMMARY})// Node / Express
app.get('/logs/stats', (req, res) => {
const total = Object.values(LOG_SUMMARY).reduce((a, b) => a + b, 0);
res.json({ total, breakdown: LOG_SUMMARY });
});La logique métier est identique. Seule la syntaxe change.
| Critère | Points |
|---|---|
| CI Node avec lint ESLint + tests Jest en deux jobs parallèles | 3 |
Dependabot npm configuré dans dependabot.yml |
1 |
| Pipeline CI/CD Node complet (qualité → staging → production) | 3 |
| Hooks Husky + lint-staged fonctionnels | 2 |
Endpoint /logs/stats Node avec test → PR → CI verte → merge |
1 |
Formation DevSecOps Azure — Simplon