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).
#
# 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
# les déclencheurs, alors même que ce workflow ne teste ni le modèle ni l'API.
# modification de l'un ou de l'autre peut donc casser sa construction : le filtre `airflow` de
# ci.yml, qui appelle ce workflow, inclut ces chemins alors qu'il ne teste ni le modèle ni l'API.
on:
push:
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"
workflow_call:
permissions:
contents: read
concurrency:
group: airflow-${{ github.ref }}
cancel-in-progress: true
jobs:
verification:
name: Lint et intégrité des DAGs
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: etl/airflow
@@ -50,17 +28,19 @@ jobs:
- name: Récupère le dépôt
uses: actions/checkout@v7
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
cache-dependency-glob: etl/airflow/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version
run: uv python install
- name: Synchronise les dépendances sans dévier du verrou
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
run: uv run ruff format --check .
@@ -76,6 +56,7 @@ jobs:
image:
name: Construction de l'image
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- 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
# 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
run: >
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.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.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
# 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:
push:
paths:
- "apps/backend/**"
- ".github/workflows/backend.yml"
pull_request:
paths:
- "apps/backend/**"
- ".github/workflows/backend.yml"
workflow_call:
permissions:
contents: read
concurrency:
group: backend-${{ github.ref }}
cancel-in-progress: true
jobs:
verification:
name: Lint, typage et tests
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: apps/backend
@@ -32,17 +24,20 @@ jobs:
- name: Récupère le dépôt
uses: actions/checkout@v7
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version
run: uv python install
- name: Synchronise les dépendances sans dévier du verrou
run: uv sync --all-groups --frozen
# `--locked` et non `--frozen` : un verrou qui ne suit plus pyproject.toml doit casser ici.
- name: Synchronise les dépendances sur le verrou
run: uv sync --all-groups --locked
- name: Vérifie le formatage
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.
- 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
# migration (`5353c0e4f094`) échoue volontairement si l'extension TimescaleDB manque, et un
# écart d'image entre la CI et le poste rendrait ce job vert sur une base qui n'est pas la nôtre.
- name: Verse la couverture pour Sonar
uses: actions/upload-artifact@v7
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:
name: Tests exigeant une base
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: apps/backend
@@ -93,16 +95,17 @@ jobs:
uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version
run: uv python install
- name: Synchronise les dépendances sans dévier du verrou
run: uv sync --all-groups --frozen
- name: Synchronise les dépendances sur le verrou
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
# 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
run: uv run alembic upgrade head
# `-m` en ligne de commande écrase celui d'`addopts`. La couverture est désactivée : ce job
# ne joue qu'une partie de la suite, son taux n'aurait aucun sens face au seuil de 85 %.
# Couverture désactivée : ce job ne joue qu'une partie de la suite, son taux n'aurait
# aucun sens face au seuil de 85 %.
- name: Tests d'intégration
run: uv run pytest -m integration --no-cov
security-audit:
name: Audit des dépendances
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: apps/backend
@@ -129,21 +133,23 @@ jobs:
uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
cache-dependency-glob: apps/backend/uv.lock
prune-cache: false
# 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.
- name: Audite les dépendances livrées
# Piège : sans `shell: bash`, un échec de `uv export` serait masqué par le pipe.
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:
name: Analyse statique de sécurité
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
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
# aucune clé de cache ; la seule roue téléchargée est celle de Bandit.
- 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
# 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
# --no-sync` pour ne rien résoudre ni reconstruire (règle S8544).
- 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
- name: Active TimescaleDB sur la base du scan
@@ -299,9 +299,11 @@ jobs:
if: always()
run: |
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"
echo "Rapport complet (HTML/JSON/Markdown) dans l'artefact \`zap-report\`." >> "$GITHUB_STEP_SUMMARY"
{
awk '/^## Alert Detail/{exit} {print}' zap-out/zap-report.md
echo ""
echo "Rapport complet (HTML/JSON/Markdown) dans l'artefact \`zap-report\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "Aucun rapport ZAP produit, voir le journal du job." >> "$GITHUB_STEP_SUMMARY"
fi
+49 -43
View File
@@ -1,64 +1,70 @@
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:
push:
paths:
- "apps/frontend/**"
- ".github/workflows/frontend.yml"
pull_request:
paths:
- "apps/frontend/**"
- ".github/workflows/frontend.yml"
workflow_call:
permissions:
contents: read
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
timeout-minutes: 15
defaults:
run:
working-directory: apps/frontend
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- name: Récupère le dépôt
uses: actions/checkout@v7
- name: Installe Node
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
- name: Installe les dépendances
run: npm ci
security-audit:
name: Audit des dépendances
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
- name: Construit l'application
run: npm run build
test:
needs: build
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
# Piège : `npm test --watch=false` garde l'option pour npm, `ng test` ne la reçoit jamais.
# La couverture lcov vient d'angular.json (`coverage: true`).
- name: Tests et couverture
run: npm run test:ci
- name: Verse la couverture pour Sonar
uses: actions/upload-artifact@v7
with:
name: frontend-coverage
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
# Pourquoi : le Terraform du dépôt est resté cassé sans que rien ne le dise, faute de job qui le
# joue. Ce workflow n'applique rien : il vérifie le formatage et la validité de chaque racine.
# Piège : la boucle parcourt `environments/*`, pour qu'une racine ajoutée soit couverte sans
# toucher à ce fichier.
# Pourquoi : rien de ce qui décrit l'infrastructure ne s'exécute avant le déploiement. Terraform est
# resté cassé sans que rien ne le dise, faute de job qui le joue : ce workflow n'applique rien, il
# vérifie le Terraform, les fichiers Compose et les workflows eux-mêmes - jobs terraform, compose,
# 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:
push:
paths:
- "infra/terraform/**"
- ".github/workflows/infra.yml"
pull_request:
paths:
- "infra/terraform/**"
- ".github/workflows/infra.yml"
workflow_call:
inputs:
terraform:
type: boolean
default: false
compose:
type: boolean
default: false
workflows:
type: boolean
default: false
permissions:
contents: read
concurrency:
group: infra-${{ github.ref }}
cancel-in-progress: true
jobs:
terraform:
name: Formatage et validation Terraform
if: inputs.terraform
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Récupère le dépôt
uses: actions/checkout@v7
# Action tierce, donc epinglee sur un SHA de commit et pas sur un tag mobile : un tag se
# redeplace, et ce workflow tourne avec les droits du depot (regle Sonar githubactions:S7637).
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
@@ -50,3 +52,38 @@ jobs:
terraform -chdir="${racine}" validate
echo "::endgroup::"
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.
# .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:
push:
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"
workflow_call:
permissions:
contents: read
concurrency:
group: ml-${{ github.ref }}
cancel-in-progress: true
jobs:
verification:
name: Lint, typage et tests
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: ml
@@ -50,17 +24,19 @@ jobs:
- name: Récupère le dépôt
uses: actions/checkout@v7
# Action tierce, épinglée sur le commit du tag (règle Sonar githubactions:S7637).
- name: Installe uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
cache-dependency-glob: ml/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version
run: uv python install
- name: Synchronise les dépendances sans dévier du verrou
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
run: uv run ruff format --check .
@@ -71,17 +47,24 @@ jobs:
- name: Typage
run: uv run mypy enervision_ml tests
# Les tests exigeant une base portent le marqueur `integration`, ecarte par defaut et
# joue par le job `integration` ci-dessous.
- name: Tests
run: uv run pytest
# Les tests exigeant une base portent le marqueur `integration`, écarté par défaut et
# joué par le job `integration` ci-dessous.
- name: Tests et couverture
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 :
# le schema de la base ML est celui du backend (apps/backend/alembic, proprietaire du schema).
# Le reconstruire ici a la main rendrait ce job vert sur une base qui n'est pas la notre.
- name: Verse la couverture pour Sonar
uses: actions/upload-artifact@v7
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:
name: ML - DB et chaîne ML - DB - API
runs-on: ubuntu-latest
timeout-minutes: 20
services:
db:
@@ -112,26 +95,27 @@ jobs:
uses: actions/checkout@v7
- name: Installe uv
uses: astral-sh/setup-uv@v7
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
cache-dependency-glob: |
ml/uv.lock
apps/backend/uv.lock
prune-cache: false
- name: Installe l'interpréteur déclaré par .python-version
working-directory: ml
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
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 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
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
# révision Alembic refuse de s'appliquer.
@@ -142,8 +126,8 @@ jobs:
working-directory: apps/backend
run: uv run alembic upgrade head
# `-m` en ligne de commande écrase celui d'addopts. Couverture désactivée : ce job ne joue
# qu'une partie de la suite, son taux n'aurait pas de sens (même raison que backend.yml).
# Couverture désactivée : ce job ne joue qu'une partie de la suite, son taux n'aurait pas
# de sens (même raison que backend.yml).
- name: Tests ML exigeant une base
working-directory: ml
run: uv run pytest -m integration --no-cov
@@ -159,6 +143,7 @@ jobs:
sast:
name: Analyse statique de sécurité
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: ml
@@ -170,7 +155,9 @@ jobs:
# 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.
- 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)
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.sourceEncoding=UTF-8
# Dossier contenant le code source
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.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
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
# Piège : un motif sans `**` (`alembic`, `tests`) ne vise que la racine du dépôt. Les dossiers de
# 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
# Fichier généré par Pytest
# Rapports versés par les jobs de backend.yml, ml.yml et frontend.yml, repris par le job sonar de ci.yml.
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
# Les DAGs n'ont pas de couverture mesurable : leurs tests ne font que les charger (DagBag).
sonar.coverage.exclusions=etl/airflow/**