ci: rassemble la CI dans un orchestrateur unique et retire le doublon Sonar

Chaque workflow se déclenchait sur push (toutes branches) et sur pull_request : chaque commit
de PR jouait tout deux fois. sonarqube.yml reconstruisait et retestait front, back et ML en
parallèle des workflows qui le faisaient déjà, et son test backend tournait sans uv sync.

ci.yml devient le seul point d'entrée (pull_request, push sur dev et main) :
- paths-filter choisit les composants à jouer sur une PR, tout est rejoué sur dev et main ;
- backend, frontend, ml, airflow et infra passent en workflow_call ;
- le job sonar reprend les couvertures versées par ces jobs au lieu de tout rejouer ;
- « CI ok » agrège le résultat, seul check à exiger dans les règles de branche.

Au passage :
- npm run test:ci au lieu de npm test --watch=false, option que npm gardait pour lui ;
- uv sync --locked au lieu de --frozen, pour qu'un verrou périmé casse la CI ;
- setup-uv et sonarqube-scan-action épinglés sur un SHA (règle S7637), timeout sur chaque job ;
- frontend : un seul npm ci pour la construction et les tests ;
- infra : validation des fichiers Compose et actionlint sur les workflows ;
- exclusions Sonar en globs, doublon apps/frontend/sonar-project.properties supprimé.
This commit is contained in:
Johan LEROY
2026-09-23 09:16:08 +02:00
parent 59f050ec5e
commit 18307e9be3
11 changed files with 396 additions and 370 deletions
+3
View File
@@ -0,0 +1,3 @@
self-hosted-runner:
labels:
- eni-g3
+11 -31
View File
@@ -6,42 +6,20 @@ name: Airflow
# Docker, dans son propre environnement (cf. etl/airflow/Dockerfile). # Docker, dans son propre environnement (cf. etl/airflow/Dockerfile).
# #
# Piège : l'image COPY les fichiers de dépendances et le code de ml/ et de apps/backend/. Une # Piège : l'image COPY les fichiers de dépendances et le code de ml/ et de apps/backend/. Une
# modification de l'un ou de l'autre peut donc casser sa construction, d'où ces chemins dans # modification de l'un ou de l'autre peut donc casser sa construction : le filtre `airflow` de
# les déclencheurs, alors même que ce workflow ne teste ni le modèle ni l'API. # ci.yml, qui appelle ce workflow, inclut ces chemins alors qu'il ne teste ni le modèle ni l'API.
on: on:
push: workflow_call:
paths:
- "etl/airflow/**"
- "ml/pyproject.toml"
- "ml/uv.lock"
- "ml/enervision_ml/**"
- "apps/backend/pyproject.toml"
- "apps/backend/uv.lock"
- "apps/backend/app/**"
- ".github/workflows/airflow.yml"
pull_request:
paths:
- "etl/airflow/**"
- "ml/pyproject.toml"
- "ml/uv.lock"
- "ml/enervision_ml/**"
- "apps/backend/pyproject.toml"
- "apps/backend/uv.lock"
- "apps/backend/app/**"
- ".github/workflows/airflow.yml"
permissions: permissions:
contents: read contents: read
concurrency:
group: airflow-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
verification: verification:
name: Lint et intégrité des DAGs name: Lint et intégrité des DAGs
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
defaults: defaults:
run: run:
working-directory: etl/airflow working-directory: etl/airflow
@@ -50,17 +28,19 @@ jobs:
- name: Récupère le dépôt - name: Récupère le dépôt
uses: actions/checkout@v7 uses: actions/checkout@v7
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: etl/airflow/uv.lock cache-dependency-glob: etl/airflow/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version - name: Installe l'interpréteur déclaré par .python-version
run: uv python install run: uv python install
- name: Synchronise les dépendances sans dévier du verrou - name: Synchronise les dépendances sur le verrou
run: uv sync --all-groups --frozen run: uv sync --all-groups --locked
- name: Vérifie le formatage - name: Vérifie le formatage
run: uv run ruff format --check . run: uv run ruff format --check .
@@ -76,6 +56,7 @@ jobs:
image: image:
name: Construction de l'image name: Construction de l'image
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 25
steps: steps:
- name: Récupère le dépôt - name: Récupère le dépôt
@@ -93,7 +74,6 @@ jobs:
# `--help` sort par argparse avant `get_settings()` : ni base ni secret requis, et # `--help` sort par argparse avant `get_settings()` : ni base ni secret requis, et
# l'import des modules prouve que l'environnement /opt/backend est complet. # l'import des modules prouve que l'environnement /opt/backend est complet.
# Les commandes des DAGs `alertes`, historique et API Mock sont couvertes.
- name: Vérifie que les quatre commandes backend s'importent sans réseau - name: Vérifie que les quatre commandes backend s'importent sans réseau
run: > run: >
docker run --rm --network none enervision-airflow:ci docker run --rm --network none enervision-airflow:ci
@@ -101,4 +81,4 @@ jobs:
&& env -u VIRTUAL_ENV uv run --no-sync python -m app.detection.internal_alerts --help && env -u VIRTUAL_ENV uv run --no-sync python -m app.detection.internal_alerts --help
&& env -u VIRTUAL_ENV uv run --no-sync python -m app.cli generate-recommendations --help && env -u VIRTUAL_ENV uv run --no-sync python -m app.cli generate-recommendations --help
&& env -u VIRTUAL_ENV uv run --no-sync python -m app.etl.historical_import --help && env -u VIRTUAL_ENV uv run --no-sync python -m app.etl.historical_import --help
&& env -u VIRTUAL_ENV uv run --no-sync python -m app.etl.mock_api_import --help" && env -u VIRTUAL_ENV uv run --no-sync python -m app.etl.mock_api_import --help"
+35 -27
View File
@@ -2,28 +2,20 @@ name: Backend
# Piège : la version de Python vient de apps/backend/.python-version, et elle doit rester # Piège : la version de Python vient de apps/backend/.python-version, et elle doit rester
# en 3.14. Le code utilise le PEP 758, qu'un interpréteur 3.13 refuse de compiler. # en 3.14. Le code utilise le PEP 758, qu'un interpréteur 3.13 refuse de compiler.
# Pourquoi : aucun déclencheur propre. ci.yml appelle ce workflow quand le backend change, et
# Sonar y reprend la couverture versée par le job `verification` (ADR 0014).
on: on:
push: workflow_call:
paths:
- "apps/backend/**"
- ".github/workflows/backend.yml"
pull_request:
paths:
- "apps/backend/**"
- ".github/workflows/backend.yml"
permissions: permissions:
contents: read contents: read
concurrency:
group: backend-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
verification: verification:
name: Lint, typage et tests name: Lint, typage et tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
defaults: defaults:
run: run:
working-directory: apps/backend working-directory: apps/backend
@@ -32,17 +24,20 @@ jobs:
- name: Récupère le dépôt - name: Récupère le dépôt
uses: actions/checkout@v7 uses: actions/checkout@v7
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: apps/backend/uv.lock cache-dependency-glob: apps/backend/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version - name: Installe l'interpréteur déclaré par .python-version
run: uv python install run: uv python install
- name: Synchronise les dépendances sans dévier du verrou # `--locked` et non `--frozen` : un verrou qui ne suit plus pyproject.toml doit casser ici.
run: uv sync --all-groups --frozen - name: Synchronise les dépendances sur le verrou
run: uv sync --all-groups --locked
- name: Vérifie le formatage - name: Vérifie le formatage
run: uv run ruff format --check . run: uv run ruff format --check .
@@ -55,14 +50,21 @@ jobs:
# Le marqueur `integration` est exclu par défaut, donc aucune base n'est nécessaire ici. # Le marqueur `integration` est exclu par défaut, donc aucune base n'est nécessaire ici.
- name: Tests et couverture - name: Tests et couverture
run: uv run pytest --cov-fail-under=85 run: uv run pytest --cov-fail-under=85 --cov-report=xml
# Piège : l'image est celle de docker-compose.yml, pas une image `postgres` nue. La première - name: Verse la couverture pour Sonar
# migration (`5353c0e4f094`) échoue volontairement si l'extension TimescaleDB manque, et un uses: actions/upload-artifact@v7
# écart d'image entre la CI et le poste rendrait ce job vert sur une base qui n'est pas la nôtre. with:
name: backend-coverage
path: apps/backend/coverage.xml
if-no-files-found: error
# Piège : même image que docker-compose.yml, pas un `postgres` nu. La première migration refuse
# de s'appliquer sans TimescaleDB, et une autre image testerait une base qui n'est pas la nôtre.
integration: integration:
name: Tests exigeant une base name: Tests exigeant une base
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
defaults: defaults:
run: run:
working-directory: apps/backend working-directory: apps/backend
@@ -93,16 +95,17 @@ jobs:
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: apps/backend/uv.lock cache-dependency-glob: apps/backend/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version - name: Installe l'interpréteur déclaré par .python-version
run: uv python install run: uv python install
- name: Synchronise les dépendances sans dévier du verrou - name: Synchronise les dépendances sur le verrou
run: uv sync --all-groups --frozen run: uv sync --all-groups --locked
# Sur le poste, c'est db/init/110-test-database.sql qui pose l'extension. Ce fichier n'est # Sur le poste, c'est db/init/110-test-database.sql qui pose l'extension. Ce fichier n'est
# pas monté ici, et sans lui `alembic upgrade head` s'arrête sur la garde de la révision 1. # pas monté ici, et sans lui `alembic upgrade head` s'arrête sur la garde de la révision 1.
@@ -112,14 +115,15 @@ jobs:
- name: Applique les migrations - name: Applique les migrations
run: uv run alembic upgrade head run: uv run alembic upgrade head
# `-m` en ligne de commande écrase celui d'`addopts`. La couverture est désactivée : ce job # Couverture désactivée : ce job ne joue qu'une partie de la suite, son taux n'aurait
# ne joue qu'une partie de la suite, son taux n'aurait aucun sens face au seuil de 85 %. # aucun sens face au seuil de 85 %.
- name: Tests d'intégration - name: Tests d'intégration
run: uv run pytest -m integration --no-cov run: uv run pytest -m integration --no-cov
security-audit: security-audit:
name: Audit des dépendances name: Audit des dépendances
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
defaults: defaults:
run: run:
working-directory: apps/backend working-directory: apps/backend
@@ -129,21 +133,23 @@ jobs:
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: apps/backend/uv.lock cache-dependency-glob: apps/backend/uv.lock
prune-cache: false
# L'audit porte sur le verrou, pas sur l'environnement : sinon pip-audit auditerait # L'audit porte sur le verrou, pas sur l'environnement : sinon pip-audit auditerait
# aussi les paquets que son propre `--with` injecte, hors dépendances du projet. # aussi les paquets que son propre `--with` injecte, hors dépendances du projet.
- name: Audite les dépendances livrées - name: Audite les dépendances livrées
# Piège : sans `shell: bash`, un échec de `uv export` serait masqué par le pipe. # Piège : sans `shell: bash`, un échec de `uv export` serait masqué par le pipe.
shell: bash shell: bash
run: uv export --frozen --no-dev --no-emit-project --no-hashes | uvx pip-audit --requirement /dev/stdin --no-deps run: uv export --locked --no-dev --no-emit-project --no-hashes | uvx pip-audit --requirement /dev/stdin --no-deps
sast: sast:
name: Analyse statique de sécurité name: Analyse statique de sécurité
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
defaults: defaults:
run: run:
working-directory: apps/backend working-directory: apps/backend
@@ -155,7 +161,9 @@ jobs:
# Pourquoi : pas de cache ici. uvx n'installe pas le projet, le verrou n'alimente donc # Pourquoi : pas de cache ici. uvx n'installe pas le projet, le verrou n'alimente donc
# aucune clé de cache ; la seule roue téléchargée est celle de Bandit. # aucune clé de cache ; la seule roue téléchargée est celle de Bandit.
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: false
# Pourquoi : le périmètre est `app`, le code livré. Les tests emploient légitimement des # Pourquoi : le périmètre est `app`, le code livré. Les tests emploient légitimement des
# secrets factices et des `assert` que Bandit signalerait sans qu'aucun n'atteigne la prod. # secrets factices et des `assert` que Bandit signalerait sans qu'aucun n'atteigne la prod.
+195
View File
@@ -0,0 +1,195 @@
# Pourquoi : un seul point d'entrée pour toute la CI (ADR 0014) - workflow CI. Chaque composant
# ne tourne que si ses fichiers changent, Sonar reprend les couvertures déjà produites au lieu de
# tout rejouer, et le déploiement ne part que d'un commit dont la CI est verte.
# Piège : le seul check à exiger dans les règles de branche est « CI ok ». Un job sauté par son
# filtre ne publie pas les checks de son workflow, qui resteraient en attente s'ils étaient exigés.
# Piège : sur un push vers dev ou main, tous les filtres valent vrai. paths-filter comparerait
# sinon à la base de fusion avec main, et Sonar n'analyserait qu'une partie de la branche.
# Piège : pas d'annulation des runs de push. Un run coupé en plein `make stack-up` laisserait la
# stack à moitié redémarrée ; le groupe par SHA évite aussi de mettre `dev` en file derrière lui.
name: CI
on:
pull_request:
push:
branches: [dev, main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
changes:
name: Périmètre modifié
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
backend: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.backend == 'true' }}
frontend: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.frontend == 'true' }}
ml: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.ml == 'true' }}
airflow: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.airflow == 'true' }}
terraform: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.terraform == 'true' }}
compose: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.compose == 'true' }}
workflows: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.workflows == 'true' }}
sonar: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.sonar == 'true' }}
steps:
# Sur une PR, la liste des fichiers vient de l'API : ni checkout ni historique requis.
- name: Calcule le périmètre de la PR
id: filtre
if: github.event_name == 'pull_request'
uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
with:
filters: |
ci:
- ".github/workflows/ci.yml"
backend:
- "apps/backend/**"
- ".github/workflows/backend.yml"
frontend:
- "apps/frontend/**"
- ".github/workflows/frontend.yml"
ml:
- "ml/**"
- "apps/backend/alembic/**"
- "apps/backend/app/models/**"
- "apps/backend/tests/test_chaine_ml_api.py"
- "apps/backend/pyproject.toml"
- "apps/backend/uv.lock"
- ".github/workflows/ml.yml"
airflow:
- "etl/airflow/**"
- "ml/pyproject.toml"
- "ml/uv.lock"
- "ml/enervision_ml/**"
- "apps/backend/pyproject.toml"
- "apps/backend/uv.lock"
- "apps/backend/app/**"
- ".github/workflows/airflow.yml"
terraform:
- "infra/terraform/**"
- ".github/workflows/infra.yml"
compose:
- "docker-compose*.yml"
- ".env.example"
- ".github/workflows/infra.yml"
workflows:
- ".github/**"
sonar:
- "apps/backend/**"
- "apps/frontend/**"
- "ml/**"
- "etl/airflow/**"
- "sonar-project.properties"
backend:
name: Backend
needs: changes
if: needs.changes.outputs.backend == 'true'
uses: ./.github/workflows/backend.yml
frontend:
name: Frontend
needs: changes
if: needs.changes.outputs.frontend == 'true'
uses: ./.github/workflows/frontend.yml
ml:
name: ML
needs: changes
if: needs.changes.outputs.ml == 'true'
uses: ./.github/workflows/ml.yml
airflow:
name: Airflow
needs: changes
if: needs.changes.outputs.airflow == 'true'
uses: ./.github/workflows/airflow.yml
infra:
name: Infra
needs: changes
if: >-
needs.changes.outputs.terraform == 'true'
|| needs.changes.outputs.compose == 'true'
|| needs.changes.outputs.workflows == 'true'
uses: ./.github/workflows/infra.yml
with:
terraform: ${{ needs.changes.outputs.terraform == 'true' }}
compose: ${{ needs.changes.outputs.compose == 'true' }}
workflows: ${{ needs.changes.outputs.workflows == 'true' }}
# Ni dependabot[bot] ni une PR de fork ne reçoivent SONAR_TOKEN : le scan échouerait sans rien
# analyser. Tests et couverture restent joués par leurs jobs.
sonar:
name: SonarQube
needs: [changes, backend, frontend, ml]
if: >-
always() && !cancelled()
&& !contains(needs.*.result, 'failure')
&& needs.changes.outputs.sonar == 'true'
&& github.actor != 'dependabot[bot]'
&& (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Récupère le dépôt
uses: actions/checkout@v7
with:
fetch-depth: 0
# Un téléchargement par rapport : backend et ML nomment tous deux le leur `coverage.xml`.
- name: Couverture du backend
if: needs.backend.result == 'success'
uses: actions/download-artifact@v8
with:
name: backend-coverage
path: apps/backend
- name: Couverture du pipeline ML
if: needs.ml.result == 'success'
uses: actions/download-artifact@v8
with:
name: ml-coverage
path: ml
- name: Couverture du frontend
if: needs.frontend.result == 'success'
uses: actions/download-artifact@v8
with:
name: frontend-coverage
path: apps/frontend/coverage/frontend
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Analyse SonarQube
uses: SonarSource/sonarqube-scan-action@ba9859eae8dd6bd29e412f25ddbbef3d032000f4 # v8.2.2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ci-ok:
name: CI ok
needs: [changes, backend, frontend, ml, airflow, infra, sonar]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Refuse si un job a échoué ou a été annulé
env:
RESULTATS: ${{ toJSON(needs.*.result) }}
run: |
echo "$RESULTATS"
if grep -qE '"(failure|cancelled)"' <<<"$RESULTATS"; then
echo "::error::Au moins un job de la CI a échoué ou a été annulé."
exit 1
fi
+6 -4
View File
@@ -97,7 +97,7 @@ jobs:
# `apps/backend`, comme dans son Dockerfile. Les `uv run` suivants portent `--frozen # `apps/backend`, comme dans son Dockerfile. Les `uv run` suivants portent `--frozen
# --no-sync` pour ne rien résoudre ni reconstruire (règle S8544). # --no-sync` pour ne rien résoudre ni reconstruire (règle S8544).
- name: Synchronise les dépendances sans dévier du verrou - name: Synchronise les dépendances sans dévier du verrou
run: uv sync --frozen --no-dev --no-install-project --no-build run: uv sync --locked --no-dev --no-install-project --no-build
working-directory: apps/backend working-directory: apps/backend
- name: Active TimescaleDB sur la base du scan - name: Active TimescaleDB sur la base du scan
@@ -299,9 +299,11 @@ jobs:
if: always() if: always()
run: | run: |
if [ -f zap-out/zap-report.md ]; then if [ -f zap-out/zap-report.md ]; then
awk '/^## Alert Detail/{exit} {print}' zap-out/zap-report.md >> "$GITHUB_STEP_SUMMARY" {
echo "" >> "$GITHUB_STEP_SUMMARY" awk '/^## Alert Detail/{exit} {print}' zap-out/zap-report.md
echo "Rapport complet (HTML/JSON/Markdown) dans l'artefact \`zap-report\`." >> "$GITHUB_STEP_SUMMARY" echo ""
echo "Rapport complet (HTML/JSON/Markdown) dans l'artefact \`zap-report\`."
} >> "$GITHUB_STEP_SUMMARY"
else else
echo "Aucun rapport ZAP produit, voir le journal du job." >> "$GITHUB_STEP_SUMMARY" echo "Aucun rapport ZAP produit, voir le journal du job." >> "$GITHUB_STEP_SUMMARY"
fi fi
+49 -43
View File
@@ -1,64 +1,70 @@
name: Frontend name: Frontend
# Pourquoi : aucun déclencheur propre. ci.yml appelle ce workflow quand le frontend change, et
# Sonar y reprend la couverture versée par le job `verification` (ADR 0014).
on: on:
push: workflow_call:
paths:
- "apps/frontend/**"
- ".github/workflows/frontend.yml"
pull_request:
paths:
- "apps/frontend/**"
- ".github/workflows/frontend.yml"
permissions: permissions:
contents: read contents: read
jobs: jobs:
build: # Un seul `npm ci` pour la construction et les tests : un job de plus ne ferait que le rejouer.
verification:
name: Construction et tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: apps/frontend
steps: steps:
- uses: actions/checkout@v7 - name: Récupère le dépôt
- uses: actions/setup-node@v7 uses: actions/checkout@v7
- name: Installe Node
uses: actions/setup-node@v7
with: with:
node-version: 26 node-version: 26
cache: npm cache: npm
cache-dependency-path: apps/frontend/package-lock.json cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci - name: Installe les dépendances
working-directory: apps/frontend run: npm ci
- run: npm run build
working-directory: apps/frontend
security-audit: - name: Construit l'application
name: Audit des dépendances run: npm run build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26
# Seuil high : une vulnérabilité moderate de devDependency ne doit pas bloquer une livraison.
- run: npm audit --audit-level=high --package-lock-only
working-directory: apps/frontend
test: # Piège : `npm test --watch=false` garde l'option pour npm, `ng test` ne la reçoit jamais.
needs: build # La couverture lcov vient d'angular.json (`coverage: true`).
runs-on: ubuntu-latest - name: Tests et couverture
steps: run: npm run test:ci
- uses: actions/checkout@v7
- uses: actions/setup-node@v7 - name: Verse la couverture pour Sonar
with:
node-version: 26
cache: npm
cache-dependency-path: apps/frontend/package-lock.json
- name : Installation des dépendances (Front)
run: npm ci
working-directory: apps/frontend
- name : Lancement des tests et génénration du rapport de couverture (Front)
run: npm test --watch=false --code-coverage --coverageReporters=lcov
working-directory: apps/frontend
- name: Upload coverage
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: frontend-coverage name: frontend-coverage
path: apps/frontend/coverage/frontend/lcov.info path: apps/frontend/coverage/frontend/lcov.info
if-no-files-found: error
security-audit:
name: Audit des dépendances
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: apps/frontend
steps:
- name: Récupère le dépôt
uses: actions/checkout@v7
- name: Installe Node
uses: actions/setup-node@v7
with:
node-version: 26
# Seuil high : une vulnérabilité moderate de devDependency ne doit pas bloquer une livraison.
- name: Audite le verrou
run: npm audit --audit-level=high --package-lock-only
+55 -18
View File
@@ -1,38 +1,40 @@
name: Infra name: Infra
# Pourquoi : le Terraform du dépôt est resté cassé sans que rien ne le dise, faute de job qui le # Pourquoi : rien de ce qui décrit l'infrastructure ne s'exécute avant le déploiement. Terraform est
# joue. Ce workflow n'applique rien : il vérifie le formatage et la validité de chaque racine. # resté cassé sans que rien ne le dise, faute de job qui le joue : ce workflow n'applique rien, il
# Piège : la boucle parcourt `environments/*`, pour qu'une racine ajoutée soit couverte sans # vérifie le Terraform, les fichiers Compose et les workflows eux-mêmes - jobs terraform, compose,
# toucher à ce fichier. # workflows. ci.yml choisit par ses entrées ceux qui tournent (ADR 0014).
# Piège : la boucle Terraform parcourt `environments/*`, pour qu'une racine ajoutée soit couverte
# sans toucher à ce fichier.
on: on:
push: workflow_call:
paths: inputs:
- "infra/terraform/**" terraform:
- ".github/workflows/infra.yml" type: boolean
pull_request: default: false
paths: compose:
- "infra/terraform/**" type: boolean
- ".github/workflows/infra.yml" default: false
workflows:
type: boolean
default: false
permissions: permissions:
contents: read contents: read
concurrency:
group: infra-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
terraform: terraform:
name: Formatage et validation Terraform name: Formatage et validation Terraform
if: inputs.terraform
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- name: Récupère le dépôt - name: Récupère le dépôt
uses: actions/checkout@v7 uses: actions/checkout@v7
# Action tierce, donc epinglee sur un SHA de commit et pas sur un tag mobile : un tag se # Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
# redeplace, et ce workflow tourne avec les droits du depot (regle Sonar githubactions:S7637).
- name: Installe Terraform - name: Installe Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with: with:
@@ -50,3 +52,38 @@ jobs:
terraform -chdir="${racine}" validate terraform -chdir="${racine}" validate
echo "::endgroup::" echo "::endgroup::"
done done
compose:
name: Validation des fichiers Compose
if: inputs.compose
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Récupère le dépôt
uses: actions/checkout@v7
# Compose interpole tout le fichier : les `:?` exigent une valeur, pas un vrai secret.
- name: Prépare un .env d'exemple
run: cp .env.example .env
- name: Valide la stack de développement
run: docker compose config --quiet
- name: Valide la stack déployée, profils compris
run: docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile acme config --quiet
workflows:
name: Analyse des workflows
if: inputs.workflows
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Récupère le dépôt
uses: actions/checkout@v7
# Image épinglée par tag, comme les images des fichiers Compose. Elle embarque shellcheck,
# qui analyse aussi les blocs `run:`.
- name: actionlint
run: docker run --rm -v "$PWD:/repo" --workdir /repo rhysd/actionlint:1.7.12 -color
+35 -48
View File
@@ -2,46 +2,20 @@ name: ML
# Piège : la version de Python vient de ml/.python-version, et doit rester en 3.14 (cf. # Piège : la version de Python vient de ml/.python-version, et doit rester en 3.14 (cf.
# .github/workflows/backend.yml, même contrainte). # .github/workflows/backend.yml, même contrainte).
# Pourquoi : aucun déclencheur propre. ci.yml l'appelle aussi quand les migrations ou les modèles
# du backend changent, dont dépend le job `integration` (ADR 0014).
on: on:
push: workflow_call:
paths:
- "ml/**"
- ".github/workflows/ml.yml"
# Le job `integration` monte son schema avec les migrations du backend et joue le test de
# chaine qui vit dans ses tests : sans ces chemins, une migration modifiee ne declencherait
# rien et le schema deriverait du SQL du pipeline sans que rien ne casse. Meme raisonnement
# que le filtre d'airflow.yml, qui inclut deja des chemins de ml/ et de apps/backend/.
- "apps/backend/alembic/**"
- "apps/backend/app/models/**"
- "apps/backend/tests/test_chaine_ml_api.py"
- "apps/backend/pyproject.toml"
- "apps/backend/uv.lock"
pull_request:
paths:
- "ml/**"
- ".github/workflows/ml.yml"
# Le job `integration` monte son schema avec les migrations du backend et joue le test de
# chaine qui vit dans ses tests : sans ces chemins, une migration modifiee ne declencherait
# rien et le schema deriverait du SQL du pipeline sans que rien ne casse. Meme raisonnement
# que le filtre d'airflow.yml, qui inclut deja des chemins de ml/ et de apps/backend/.
- "apps/backend/alembic/**"
- "apps/backend/app/models/**"
- "apps/backend/tests/test_chaine_ml_api.py"
- "apps/backend/pyproject.toml"
- "apps/backend/uv.lock"
permissions: permissions:
contents: read contents: read
concurrency:
group: ml-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
verification: verification:
name: Lint, typage et tests name: Lint, typage et tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
defaults: defaults:
run: run:
working-directory: ml working-directory: ml
@@ -50,17 +24,19 @@ jobs:
- name: Récupère le dépôt - name: Récupère le dépôt
uses: actions/checkout@v7 uses: actions/checkout@v7
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: ml/uv.lock cache-dependency-glob: ml/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version - name: Installe l'interpréteur déclaré par .python-version
run: uv python install run: uv python install
- name: Synchronise les dépendances sans dévier du verrou - name: Synchronise les dépendances sur le verrou
run: uv sync --all-groups --frozen run: uv sync --all-groups --locked
- name: Vérifie le formatage - name: Vérifie le formatage
run: uv run ruff format --check . run: uv run ruff format --check .
@@ -71,17 +47,24 @@ jobs:
- name: Typage - name: Typage
run: uv run mypy enervision_ml tests run: uv run mypy enervision_ml tests
# Les tests exigeant une base portent le marqueur `integration`, ecarte par defaut et # Les tests exigeant une base portent le marqueur `integration`, écarté par défaut et
# joue par le job `integration` ci-dessous. # joué par le job `integration` ci-dessous.
- name: Tests - name: Tests et couverture
run: uv run pytest run: uv run pytest --cov-report=xml
# Le seul job du depot qui dispose a la fois des deux environnements uv et d'une base. Piege : - name: Verse la couverture pour Sonar
# le schema de la base ML est celui du backend (apps/backend/alembic, proprietaire du schema). uses: actions/upload-artifact@v7
# Le reconstruire ici a la main rendrait ce job vert sur une base qui n'est pas la notre. with:
name: ml-coverage
path: ml/coverage.xml
if-no-files-found: error
# Piège : le schéma de la base ML est celui du backend (apps/backend/alembic, propriétaire du
# schéma). Le reconstruire ici à la main rendrait ce job vert sur une base qui n'est pas la nôtre.
integration: integration:
name: ML - DB et chaîne ML - DB - API name: ML - DB et chaîne ML - DB - API
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
services: services:
db: db:
@@ -112,26 +95,27 @@ jobs:
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with: with:
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
ml/uv.lock ml/uv.lock
apps/backend/uv.lock apps/backend/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version - name: Installe l'interpréteur déclaré par .python-version
working-directory: ml working-directory: ml
run: uv python install run: uv python install
- name: Synchronise le pipeline ML sans dévier du verrou - name: Synchronise le pipeline ML sur le verrou
working-directory: ml working-directory: ml
run: uv sync --all-groups --frozen run: uv sync --all-groups --locked
# Le backend est installé ici parce qu'il porte les migrations, seule source du schéma, et # Le backend est installé ici parce qu'il porte les migrations, seule source du schéma, et
# le test de chaîne, qui interroge l'API. # le test de chaîne, qui interroge l'API.
- name: Synchronise le backend sans dévier du verrou - name: Synchronise le backend sur le verrou
working-directory: apps/backend working-directory: apps/backend
run: uv sync --all-groups --frozen run: uv sync --all-groups --locked
# db/init/110-test-database.sql n'est pas monté ici, et sans l'extension la première # db/init/110-test-database.sql n'est pas monté ici, et sans l'extension la première
# révision Alembic refuse de s'appliquer. # révision Alembic refuse de s'appliquer.
@@ -142,8 +126,8 @@ jobs:
working-directory: apps/backend working-directory: apps/backend
run: uv run alembic upgrade head run: uv run alembic upgrade head
# `-m` en ligne de commande écrase celui d'addopts. Couverture désactivée : ce job ne joue # Couverture désactivée : ce job ne joue qu'une partie de la suite, son taux n'aurait pas
# qu'une partie de la suite, son taux n'aurait pas de sens (même raison que backend.yml). # de sens (même raison que backend.yml).
- name: Tests ML exigeant une base - name: Tests ML exigeant une base
working-directory: ml working-directory: ml
run: uv run pytest -m integration --no-cov run: uv run pytest -m integration --no-cov
@@ -159,6 +143,7 @@ jobs:
sast: sast:
name: Analyse statique de sécurité name: Analyse statique de sécurité
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
defaults: defaults:
run: run:
working-directory: ml working-directory: ml
@@ -170,7 +155,9 @@ jobs:
# Pourquoi : pas de cache ici. uvx n'installe pas le projet, le verrou n'alimente donc # Pourquoi : pas de cache ici. uvx n'installe pas le projet, le verrou n'alimente donc
# aucune clé de cache ; la seule roue téléchargée est celle de Bandit. # aucune clé de cache ; la seule roue téléchargée est celle de Bandit.
- name: Installe uv - name: Installe uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: false
- name: Analyse le code livré (bloquant à partir de MEDIUM) - name: Analyse le code livré (bloquant à partir de MEDIUM)
run: uvx bandit==1.9.4 --recursive enervision_ml --severity-level medium --confidence-level medium run: uvx bandit==1.9.4 --recursive enervision_ml --severity-level medium --confidence-level medium
-172
View File
@@ -1,172 +0,0 @@
name: SonarQube
on:
push:
paths:
- "apps/frontend/**"
- "apps/backend/**"
- "ml/**"
- "etl/airflow/**"
- ".github/workflows/sonarqube.yml"
pull_request:
paths:
- "apps/frontend/**"
- "apps/backend/**"
- "ml/**"
- "etl/airflow/**"
- ".github/workflows/sonarqube.yml"
# Build l'ensemble du projet, puis lance les tests
# Génère les rapports de couverture, puis lance l'analyse SonarQube
jobs:
build-front:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26
cache: npm
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci
working-directory: apps/frontend
- run: npm run build
working-directory: apps/frontend
test-front:
needs: build-front
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26
cache: npm
cache-dependency-path: apps/frontend/package-lock.json
- name : Installation des dépendances (Front)
run: npm ci
working-directory: apps/frontend
- name : Lancement des tests et génénration du rapport de couverture (Front)
run: npm test --watch=false --code-coverage --coverageReporters=lcov
working-directory: apps/frontend
- name: Upload coverage
uses: actions/upload-artifact@v7
with:
name: frontend-coverage
path: apps/frontend/coverage/frontend/lcov.info
build-back:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
- name: Installe l'interpréteur déclaré par .python-version
run: uv python install
working-directory: apps/backend
- name: Synchronise les dépendances sans dévier du verrou
run: uv sync --all-groups --frozen
working-directory: apps/backend
- name: Vérifie le formatage
run: uv run ruff format --check .
working-directory: apps/backend
- name: Analyse statique
run: uv run ruff check --output-format=github .
working-directory: apps/backend
- name: Typage
run: uv run mypy app
working-directory: apps/backend
test-back:
needs: build-back
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
- name : Lancement des tests et génénration du rapport de couverture (Back)
run: uv run pytest --cov-fail-under=85 --cov-report=xml
working-directory: apps/backend
- name: Upload coverage
uses: actions/upload-artifact@v7
with:
name: backend-coverage
path: apps/backend/coverage.xml
test-ml:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: ml/uv.lock
- name: Installe l'interpréteur déclaré par .python-version
run: uv python install
working-directory: ml
- name: Synchronise les dépendances sans dévier du verrou
run: uv sync --all-groups --frozen
working-directory: ml
- name: Lancement des tests et génération du rapport de couverture (ML)
run: uv run pytest --cov-report=xml
working-directory: ml
- name: Upload coverage
uses: actions/upload-artifact@v7
with:
name: ml-coverage
path: ml/coverage.xml
sonarqube:
needs: [build-front, build-back, test-front, test-back, test-ml]
name: SonarQube
# Pourquoi : GitHub ne fournit pas les secrets aux workflows lancés par dependabot[bot].
# Sans SONAR_TOKEN le scan échoue sans rien analyser ; build et tests restent joués.
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Téléchargement du rapport de couverture (Front)
uses: actions/download-artifact@v8
with:
name: frontend-coverage
path: apps/frontend/coverage/frontend
- name: Téléchargement du rapport de couverture (Back)
uses: actions/download-artifact@v8
with:
name: backend-coverage
path: apps/backend
- name: Téléchargement du rapport de couverture (ML)
uses: actions/download-artifact@v8
with:
name: ml-coverage
path: ml
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v8
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-18
View File
@@ -1,18 +0,0 @@
sonar.projectKey=ProjetPiscine_EnerVision
sonar.organization=groupe3-ener-vision
sonar.sourceEncoding=UTF-8
# Dossier contenant le code source
sonar.sources=apps/frontend/src,apps/backend/app
sonar.tests=apps/backend/tests
# Liste des fichiers et dossiers à exclure de l'analyse
# Liste des fichiers et dossiers à exclure de l'analyse
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.js,**/*.test.js,github,db,ml,docker-compose.yml,**/**/Dockerfile,**/**/proxy.conf.json,**/**/package.json,**/**/angular.json
# Chemin vers le rapport de couverture de code
# Fichier généré par Vitest
# Chemin vers le rapport de couverture de code
# Fichier généré par Vitest
sonar.javascript.lcov.reportPaths=apps/frontend/coverage/frontend/lcov.info
sonar.python.coverage.reportPaths=apps/backend/cov.info
+7 -9
View File
@@ -2,18 +2,16 @@ sonar.projectKey=ProjetPiscine_EnerVision
sonar.organization=groupe3-ener-vision sonar.organization=groupe3-ener-vision
sonar.sourceEncoding=UTF-8 sonar.sourceEncoding=UTF-8
# Dossier contenant le code source
sonar.sources=apps/frontend/src,apps/backend,ml,etl/airflow sonar.sources=apps/frontend/src,apps/backend,ml,etl/airflow
# Dossier contenant les tests
sonar.tests=apps/frontend/src,apps/backend/tests,ml/tests,etl/airflow/tests sonar.tests=apps/frontend/src,apps/backend/tests,ml/tests,etl/airflow/tests
sonar.test.inclusions=**/*.spec.ts,**/*.test.ts,**/*test_*.py,**/*test.py sonar.test.inclusions=**/*.spec.ts,**/*.test.ts,**/test_*.py,**/*_test.py
# Liste des fichiers et dossiers à exclure de l'analyse # Piège : un motif sans `**` (`alembic`, `tests`) ne vise que la racine du dépôt. Les dossiers de
sonar.exclusions=.pytest_cache,.venv,.airflow_home,alembic,tests,ml/data/**,ml/models/**,ml/mlruns/**,ml/mlartifacts/**,**/*/node_modules/**,**/*/dist/**,**/*/build/**,**/*.spec.ts,**/*.test.ts,**/*test_*.py,**/*test.py,**/*.spec.ts # tests restent donc exclus des sources ici, sans quoi leurs fixtures comptent en code non couvert.
sonar.exclusions=**/.pytest_cache/**,**/.venv/**,**/.airflow_home/**,**/node_modules/**,**/dist/**,**/build/**,**/htmlcov/**,**/coverage/**,**/tests/**,**/alembic/**,ml/data/**,ml/models/**,ml/mlruns/**,ml/mlartifacts/**,**/*.spec.ts,**/*.test.ts,**/test_*.py,**/*_test.py
# Chemin vers le rapport de couverture de code # Rapports versés par les jobs de backend.yml, ml.yml et frontend.yml, repris par le job sonar de ci.yml.
# Fichier généré par Pytest
sonar.python.coverage.reportPaths=apps/backend/coverage.xml,ml/coverage.xml sonar.python.coverage.reportPaths=apps/backend/coverage.xml,ml/coverage.xml
# Les DAGs n'ont pas de couverture mesurable : leurs tests ne font que les charger (DagBag)
sonar.coverage.exclusions=etl/airflow/**
sonar.javascript.lcov.reportPaths=apps/frontend/coverage/frontend/lcov.info sonar.javascript.lcov.reportPaths=apps/frontend/coverage/frontend/lcov.info
# Les DAGs n'ont pas de couverture mesurable : leurs tests ne font que les charger (DagBag).
sonar.coverage.exclusions=etl/airflow/**