From 18307e9be31ba3f2834be6411d6a2de33daf223f Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Wed, 23 Sep 2026 09:16:08 +0200 Subject: [PATCH] ci: rassemble la CI dans un orchestrateur unique et retire le doublon Sonar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é. --- .github/actionlint.yaml | 3 + .github/workflows/airflow.yml | 42 ++---- .github/workflows/backend.yml | 62 ++++---- .github/workflows/ci.yml | 195 +++++++++++++++++++++++++ .github/workflows/dast.yml | 10 +- .github/workflows/frontend.yml | 92 ++++++------ .github/workflows/infra.yml | 73 ++++++--- .github/workflows/ml.yml | 83 +++++------ .github/workflows/sonarqube.yml | 172 ---------------------- apps/frontend/sonar-project.properties | 18 --- sonar-project.properties | 16 +- 11 files changed, 396 insertions(+), 370 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/sonarqube.yml delete mode 100644 apps/frontend/sonar-project.properties diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..4823b8b --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - eni-g3 diff --git a/.github/workflows/airflow.yml b/.github/workflows/airflow.yml index 0d652d9..be08a67 100644 --- a/.github/workflows/airflow.yml +++ b/.github/workflows/airflow.yml @@ -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" \ No newline at end of file + && env -u VIRTUAL_ENV uv run --no-sync python -m app.etl.mock_api_import --help" diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 84c0664..4941beb 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fc4c99f --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 + diff --git a/.github/workflows/dast.yml b/.github/workflows/dast.yml index 095781e..6208e2e 100644 --- a/.github/workflows/dast.yml +++ b/.github/workflows/dast.yml @@ -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 diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 1ba4010..075cfc7 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -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 diff --git a/.github/workflows/infra.yml b/.github/workflows/infra.yml index a49088e..a7e6a38 100644 --- a/.github/workflows/infra.yml +++ b/.github/workflows/infra.yml @@ -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 diff --git a/.github/workflows/ml.yml b/.github/workflows/ml.yml index 4145c4e..c016b0f 100644 --- a/.github/workflows/ml.yml +++ b/.github/workflows/ml.yml @@ -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 diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml deleted file mode 100644 index 4544c4e..0000000 --- a/.github/workflows/sonarqube.yml +++ /dev/null @@ -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 }} diff --git a/apps/frontend/sonar-project.properties b/apps/frontend/sonar-project.properties deleted file mode 100644 index 46a6cce..0000000 --- a/apps/frontend/sonar-project.properties +++ /dev/null @@ -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 \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index 8f9ebed..284fc60 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -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/**