Les trois environnements passent sur enervision-g3.duckdns.org, rec. et dev. : noms publics qui visent l'IP privée de la VM, donc résolus sans /etc/hosts sur le réseau de l'école et injoignables ailleurs (ADR 0018). - infra/front : nginx sur le réseau de l'hôte, seul exposé en 80 et 443. Aiguille par SNI vers la stack visée sans déchiffrer le TLS, et lui transmet l'IP du client en PROXY protocol. - Proxy de stack : écouteur 4443 en PROXY protocol, real_ip_header ; sans lui, limit_req et get_client_ip() compteraient tous les postes comme un seul. PROXY_FRONT_PORT le publie sur 127.0.0.1. - make tls-duckdns : Let's Encrypt par défi DNS-01 via l'API DuckDNS (acme.sh 3.1.6), rejouable, rejoué à chaque déploiement et chaque nuit. - provision-host.sh fait foi pour l'adressage et les secrets : un .env existant garde ses secrets, reçoit ceux qui manquent (supervision) et voit hôte et ports réalignés. Planifie le renouvellement des certificats. - deploy.yml : nouvelles URL, sonde prod sur 10443, front-up en prod. - Terraform : variable domaine. CI : validation du frontal.
226 lines
8.2 KiB
YAML
226 lines
8.2 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"
|
|
- "infra/front/**"
|
|
- "monitoring/**"
|
|
- ".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"
|
|
- "Makefile"
|
|
- ".env.example"
|
|
- ".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
|