Le DAG `alertes` enchaîne `app.detection.internal_alerts` puis `app.cli generate-recommendations`, à la quinzième minute de chaque heure. Le décalage laisse finir `ml_score`, qui écrit à l'heure pile les prédictions dont la règle `anomaly` a besoin, sans créer de dépendance entre les deux DAGs : quatre règles de détection sur cinq ne touchent pas au modèle, et un modèle jamais entraîné ne doit pas priver le parc de ses alertes. L'image Airflow porte un second environnement uv, `/opt/backend/.venv`, puisque la logique vit dans le backend (ADR 0006) et qu'aucune route HTTP ne l'expose. Le `UV_PROJECT_ENVIRONMENT` global hérité de l'issue #115 disparaît : il vaut pour tous les projets, donc `uv run` depuis `/opt/ml` résolvait le venv du backend. uv prend `<projet>/.venv` par défaut, se placer dans le dossier suffit. La CI vérifie maintenant que les deux environnements s'importent sans réseau. Le conteneur reçoit `DATABASE_URL` en asyncpg et une `APP_SECRET_KEY` distincte de celle de l'API, alimentée par `AIRFLOW_APP_SECRET_KEY` : la détection ne signe aucun jeton, et Airflow permet d'exécuter du code depuis son interface.
99 lines
3.4 KiB
YAML
99 lines
3.4 KiB
YAML
name: Airflow
|
|
|
|
# Piège : la version de Python vient de etl/airflow/.python-version. C'est 3.12 et non 3.14
|
|
# (contrairement à backend.yml et ml.yml) : apache-airflow 2.10 ne supporte pas 3.14. Le 3.14 de
|
|
# ml/ ne vit que dans l'image 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.
|
|
|
|
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"
|
|
|
|
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
|
|
defaults:
|
|
run:
|
|
working-directory: etl/airflow
|
|
|
|
steps:
|
|
- name: Récupère le dépôt
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Installe uv
|
|
uses: astral-sh/setup-uv@v5
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: etl/airflow/uv.lock
|
|
|
|
- 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: Vérifie le formatage
|
|
run: uv run ruff format --check .
|
|
|
|
- name: Analyse statique
|
|
run: uv run ruff check --output-format=github .
|
|
|
|
# Aucun test ne lance de tâche ni de scheduler : DagBag charge les fichiers de dags/ et
|
|
# vérifie import, planification, plafonds d'exécution et commande de chaque tâche.
|
|
- name: Tests d'intégrité des DAGs
|
|
run: uv run pytest
|
|
|
|
image:
|
|
name: Construction de l'image
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Récupère le dépôt
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Construit l'image (contexte à la racine, elle COPY ml/ et apps/backend/)
|
|
run: docker build -f etl/airflow/Dockerfile -t enervision-airflow:ci .
|
|
|
|
# Vérifie ce qui ne casse qu'à l'exécution, pas à la construction : libgomp1 absent
|
|
# (`OSError: libgomp.so.1` au premier import) ou environnement ml/ non figé.
|
|
- name: Vérifie que le pipeline ML s'importe sans réseau
|
|
run: >
|
|
docker run --rm --network none enervision-airflow:ci
|
|
bash -c "cd /opt/ml && env -u VIRTUAL_ENV uv run --no-sync python -m enervision_ml.train --help"
|
|
|
|
# `--help` sort par argparse avant `get_settings()` : ni base ni secret requis, et
|
|
# l'import du module prouve que l'environnement /opt/backend est complet.
|
|
- name: Vérifie que la détection d'alertes s'importe sans réseau
|
|
run: >
|
|
docker run --rm --network none enervision-airflow:ci
|
|
bash -c "cd /opt/backend && env -u VIRTUAL_ENV uv run --no-sync python -m app.detection.internal_alerts --help"
|