Un projet Compose par environnement sur la même machine : ports du proxy et origine
publique en variables dans docker-compose.prod.yml, réglage mémoire des deux bases
TimescaleDB et du webserver Airflow. Le workflow deploy.yml déploie dev en recette et
main en production depuis un runner installé sur la VM, jamais sur pull_request.
scripts/provision-host.sh prépare les deux dossiers, secrets et certificats compris,
sans rien démarrer.
L'image frontend quitte dhi.io/nginx, registre authentifié dont personne n'a l'accès,
pour nginx:1.28-alpine : elle n'avait jamais été construite.
ADR 0009, vues infra et CI/CD, README et .env.example mis à jour.
Refs #21, #22
ML-START.md affirmait que l'orchestration Airflow n'existait pas : `ml_score`
tourne en `@hourly` depuis l'issue #115, seuls le mode `--csv` et un lancement
local restent manuels.
50-cicd.md : Dependabot compte six entrées sur cinq écosystèmes et non cinq
entrées, le filtre d'`airflow.yml` couvre aussi `apps/backend/` depuis le DAG
`alertes`, et les issues #21 (job de déploiement) et #22 (secrets) sont
distinguées au lieu d'être citées l'une pour l'autre. Le `continue-on-error` du
second passage Bandit est nommé pour ce qu'il est : le job reste vert même avec
un constat LOW.
Bandit est épinglé à 1.9.4 dans les deux jobs `sast` : sans épingle, une
nouvelle version passe la CI au rouge sans qu'une ligne du dépôt ait changé, et
le rejeu à l'identique documenté n'existe pas. Le `cache-dependency-glob` part :
`uvx` n'installe pas le projet, le verrou n'alimentait aucune clé de cache.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`execution_timeout` plafonne une tentative, pas la tâche. Avec deux reprises, quinze
minutes par tentative autorisaient quarante-neuf minutes par tâche et quatre-vingt-dix-huit
pour l'enchaînement, quand le commentaire annonçait une somme tenant sous le pas horaire.
Le plafond passe à cinq minutes, ce qui borne le pire cas à trente-huit minutes, et le test
d'intégrité calcule désormais ce pire cas plutôt que la somme des plafonds : reprises et
délais d'attente compris, c'est la durée qu'un `max_active_runs=1` fait payer à l'exécution
suivante.
La CI vérifie aussi `app.cli generate-recommendations --help` sans réseau. C'est la seconde
commande du DAG, et son import tire FastAPI, les repositories et les services, donc une part
de l'environnement `/opt/backend` que la détection seule ne touche pas.
`10-infra.md` nomme enfin ce que le décalage de quinze minutes ne garantit pas : le plafond
de `ml_score` valant trente minutes, un scoring qui déborde prive la règle `anomaly` de la
prédiction de l'heure, qu'elle ne retrouvera au passage suivant que si sa fenêtre la couvre
encore.
Conflit sur docs/architecture/00-vue-ensemble.md, résolu au profit de l'état réel
de dev après la #117 et la #115.
La ligne ML reprend l'orchestration Airflow de dev, que la branche avait retirée
alors que la ligne ETL de la même table la décrit ; seul le renvoi vers
ML-START.md garde la correction de chemin apportée ici.
Trois manques listés comme assumés ne le sont plus : la terminaison TLS pose HSTS
et CSP, le proxy limite le débit, et environment.ts de production est passé en URL
relative. La section plus haut les décrit déjà comme livrés.
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.
Rapatrie la #118 (DAGs Airflow). Quatre conflits, tous additifs sauf un :
- `.env.example` et `.gitignore` : les blocs Airflow et proxy cohabitent.
- `Makefile` : `AIRFLOW` rejoint les variables de dossier, les cibles Airflow
et TLS cohabitent dans `.PHONY`.
- `00-vue-ensemble.md` : la ligne ML de `dev` est retenue, la ligne Infra de
cette branche aussi, chacune portant sa propre mise à jour.
L'interface Airflow rejoint la base et Mailpit sur `127.0.0.1` dans l'overlay :
elle n'a pas d'authentification à publier derrière le proxy.
Le pipeline auditait les dépendances (pip-audit, npm audit, Dependabot) mais
jamais le code lui-même : aucun SAST, aucun DAST. C'était le seul rouge de BC03
qui se fermait en une étape de workflow.
Le job bloque à partir de MEDIUM/MEDIUM, et une seconde passe sans seuil publie
les constats LOW sans bloquer : sans elle, un LOW disparaîtrait du journal sans
trace. Le périmètre est le code livré (`app`, `enervision_ml`) et non les
tests, qui emploient légitimement des secrets factices et des `assert`.
Relevé au 21/09 : zéro constat tous niveaux confondus sur 5 904 lignes.
Couvre #39. Ferme C18 de la grille d'auto-évaluation.
Le SPA appelle /api/v1 en relatif et rien ne routait cet appel vers l'API
une fois en conteneur. Le cookie de rafraîchissement prend le préfixe
__Secure- dès que APP_ENV sort de local, donc sans HTTPS il n'était jamais
posé et l'authentification ne survivait pas à un rechargement de page.
Un service proxy, image officielle nginx dont la configuration est montée en
volume, devient le seul composant publié : 80 redirige vers 443 et sert le
défi ACME, 443 termine le TLS, sert le SPA sur / et l'API sur /api/ sous la
même origine, pose HSTS et CSP que l'application refuse délibérément de
poser, et ajoute une limitation de débit au frontal. Backend et frontend ne
sont plus publiés, la base et l'interface Mailpit sont ramenées sur la
boucle locale.
nginx lit toujours les deux mêmes fichiers de certificat : seule leur
fabrication varie, script openssl pour la démonstration, deploy-hook certbot
le jour où un domaine public existera. Le chemin ACME est livré et
documenté, pas exercé : sur une IP privée le défi HTTP-01 ne peut pas
aboutir.
Conflit sur .github/workflows/backend.yml : dev y a ajouté le job
`security-audit` (PR #100) pendant que cette branche y ajoutait le job
`integration`. Les deux jobs sont conservés côte à côte.
Résolution du conflit sur .github/workflows/frontend.yml :
- conserve les étapes upload/download de l'artifact lcov ajoutées sur dev,
sans lesquelles le rapport ne quitte pas le runner du job test et
n'atteint jamais le scanner Sonar ;
- conserve le job security-audit et le retrait du bloc deploy commenté ;
- retient la commande de test de la branche.
Le séparateur `--` manquait côté dev : npm consommait les trois flags
comme sa propre configuration et ng test tournait sans argument. De plus
`--code-coverage` n'existe pas dans le builder @angular/build:unit-test,
où l'option s'appelle `coverage`.
L'audit backend était la dernière étape du job de vérification : un lint
ou un test en échec suffisait à le sauter, et `pip-audit` sans argument
auditait l'environnement courant, donc aussi les 28 paquets injectés par
son propre `--with`. Il audite maintenant l'export du verrou, dans un job
dédié, en symétrie avec le frontend.
Côté frontend, `npm audit` lit le verrou et n'a besoin ni de `npm ci` ni
du job `build`. Le workflow déclare enfin ses permissions, comme
backend.yml et ml.yml.
`pyproject.toml` écarte le marqueur `integration` par défaut, et aucun workflow ne montait de
base : 97 tests, dont les neuf fichiers de dépôts et le schéma de données, n'avaient jamais
été joués ailleurs que sur un poste. La condition avait été déléguée à #20, fermée le 17/09
sans l'avoir livrée.
Le job monte l'image de `docker-compose.yml` et non une image `postgres` nue : la première
migration refuse de s'appliquer sans l'extension TimescaleDB, et un écart d'image rendrait ce
job vert sur une base qui n'est pas la nôtre. `db/init/110-test-database.sql` n'étant pas
monté ici, l'extension est créée en une étape avant `alembic upgrade head`.
Le job `verification` est inchangé : il reste jouable sans Docker, avec son seuil de
couverture de 85 %.