Aucun parcours n'était vérifié de bout en bout : les tests unitaires du frontend simulent l'API, ceux du backend n'ouvrent jamais de navigateur. tests/e2e, paquet npm autonome, 18 parcours dans Chromium : - authentification, premier login, réinitialisation du mot de passe par Mailpit ; - rôles : lecteur, opérateur, administrateur ; - sites, recommandations, fil d'alertes. e2e.yml, appelé par ci.yml, démarre db, mailpit, backend, frontend et proxy avec docker-compose.prod.yml sur https://localhost, sème demo.sql, crée les comptes et joue la suite. Il construit au passage les images backend et frontend, que la CI ne construisait jamais. Un seul worker et une session par fichier : la zone auth de nginx admet 30 connexions par minute, et rejouer un cookie de refresh dans un second contexte révoque toute la session. make e2e-install, e2e-prepare et e2e pour le poste ; make help affiche désormais les cibles dont le nom contient un chiffre. Closes #46
222 lines
8.1 KiB
YAML
222 lines
8.1 KiB
YAML
# 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' }}
|
|
e2e: ${{ github.event_name != 'pull_request' || steps.filtre.outputs.ci == 'true' || steps.filtre.outputs.e2e == '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/**"
|
|
e2e:
|
|
- "apps/frontend/**"
|
|
- "apps/backend/app/**"
|
|
- "apps/backend/alembic/**"
|
|
- "apps/backend/Dockerfile"
|
|
- "apps/backend/pyproject.toml"
|
|
- "apps/backend/uv.lock"
|
|
- "infra/proxy/**"
|
|
- "docker-compose*.yml"
|
|
- "db/**"
|
|
- "tests/**"
|
|
- "scripts/comptes-test.sh"
|
|
- "scripts/tls-selfsigned.sh"
|
|
- ".github/workflows/e2e.yml"
|
|
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' }}
|
|
|
|
e2e:
|
|
name: E2E
|
|
needs: changes
|
|
if: needs.changes.outputs.e2e == 'true'
|
|
uses: ./.github/workflows/e2e.yml
|
|
|
|
# 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, e2e, 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
|
|
|
|
deploy:
|
|
name: Déploiement
|
|
needs: ci-ok
|
|
if: ${{ !cancelled() && needs.ci-ok.result == 'success' && github.event_name == 'push' }}
|
|
uses: ./.github/workflows/deploy.yml
|