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/**