Aller au contenu

Checklist de merge — feat/energy → main

Extension énergie (ADR-013). Un push sur main déclenche le déploiement en production, et le conteneur api applique les migrations au démarrage (prisma migrate deploy dans apps/api/entrypoint.sh). La CI ne lance aucune base de données : les deux migrations de la branche n'ont jamais tourné que sur la base de dev.

Le merge reste un arbitrage humain. Cette liste se suit dans l'ordre ; une étape en échec arrête tout.

Ce que la branche apporte à la base : deux migrations purement additives, 20260927121740_add_energy_tariffs (5 tables, une valeur d'enum LINDAS_ELCOM) et 20260927124253_add_tariff_metering_rate (une colonne nullable). Aucune table hydrologique n'est modifiée.

Constaté le 2026-09-27 : Postgres 16 en dev comme en prod (postgres:16-alpine) ; apps/api/entrypoint.sh, apps/api/Dockerfile, docker-compose.prod.yml, apps/api/prisma/seed.ts et apps/api/src/plugins/ sont identiques à main.


1. Rejouer les migrations sur une copie de la base de production

Tout se passe en local, sur un conteneur jetable. La base de production n'est que lue.

  1. Exporter la base de production (lecture seule) :
ssh root@95.216.196.69 \
  'docker exec <pg-container> pg_dump -U alpimonitor -d alpimonitor -Fc' > /tmp/alpimonitor-prod.dump

Le fichier contient des données de production : il ne sort pas de la machine et il est supprimé à l'étape 1.8.

  1. Restaurer dans un conteneur jetable, sur un port libre (5434 ; ni 5432, pris par un autre projet, ni 5433, la base de dev) :
docker run -d --rm --name am-prod-replay \
  -e POSTGRES_USER=alpimonitor -e POSTGRES_PASSWORD=replay -e POSTGRES_DB=alpimonitor \
  -p 127.0.0.1:5434:5432 postgres:16-alpine
# attendre que pg_isready réponde, puis :
docker exec -i am-prod-replay pg_restore -U alpimonitor -d alpimonitor --no-owner < /tmp/alpimonitor-prod.dump
export REPLAY_URL=postgresql://alpimonitor:replay@127.0.0.1:5434/alpimonitor
  1. Relever l'état avant migration :
DATABASE_URL=$REPLAY_URL pnpm --filter @alpimonitor/api exec prisma migrate status
# attendu : 2 migrations en attente, add_energy_tariffs et add_tariff_metering_rate
docker exec am-prod-replay psql -U alpimonitor -d alpimonitor -tAc \
  'SELECT (SELECT COUNT(*) FROM "Station"), (SELECT COUNT(*) FROM "Measurement"), (SELECT COUNT(*) FROM "IngestionRun")'
  1. Démarrer l'image de la branche sur la copie, exactement comme en production : c'est l'entrypoint qui applique les migrations, puis le seed.
git switch feat/energy
docker build -f apps/api/Dockerfile -t alpimonitor-api:feat-energy .
docker run -d --rm --name am-api-replay --network host \
  -e DATABASE_URL=$REPLAY_URL -e HOST=127.0.0.1 -e PORT=3200 \
  -e SEED_ON_BOOT=true -e INGESTION_ENABLED=true \
  alpimonitor-api:feat-energy
docker logs -f am-api-replay   # attendu : migrations appliquées, seed, « Starting API », aucune erreur
  1. Vérifier le résultat :
DATABASE_URL=$REPLAY_URL pnpm --filter @alpimonitor/api exec prisma migrate status
# attendu : « Database schema is up to date! »
curl -s http://127.0.0.1:3200/api/v1/health            # attendu : status ok, database ok
curl -s http://127.0.0.1:3200/api/v1/energy/positioning # attendu : 404 ENERGY_DATA_NOT_IMPORTED
docker exec am-prod-replay psql -U alpimonitor -d alpimonitor -tAc \
  'SELECT (SELECT COUNT(*) FROM "Station"), (SELECT COUNT(*) FROM "Measurement"), (SELECT COUNT(*) FROM "ElectricityTariff"), (SELECT COUNT(*) FROM "Municipality")'

Attendu : autant de stations qu'avant ; au moins autant de mesures (la première passe d'ingestion hydro peut en ajouter) ; tables énergie vides.

  1. Rejouer l'import sur la copie : c'est la répétition générale de l'étape 4, qui mesure durée et mémoire dans les conditions de l'image.
docker exec am-api-replay node dist/energy/import-elcom-cli.js; echo "exit=$?"
docker stats --no-stream am-api-replay

Attendu : exit=0, une ligne elcom-import: success (≈ 50 000 tarifs, 91 lignes ignorées en 2027) et un IngestionRun LINDAS_ELCOM en SUCCESS.

  1. Mémoire : point de vigilance. Mesuré en dev le 2026-09-27, l'import culmine à ≈ 605 Mo de mémoire résidente pendant ≈ 54 s. Cette mesure date d'un passage sans création de ligne, donc un premier import peut monter plus haut. Avant l'étape 4, vérifier la mémoire libre du VPS, partagé avec exploreiot, et la limite éventuelle du conteneur api dans Coolify. Si la marge est insuffisante, réduire d'abord la taille des pages SPARQL et découper le createMany en lots : c'est un changement de code, à faire sur feat/energy avant le merge.

  2. Nettoyer :

docker stop am-api-replay am-prod-replay
rm /tmp/alpimonitor-prod.dump

2. Vérifier que l'import ne se déclenche ni au boot ni par un cron

  1. Aucun appel à l'import hors de son module :
grep -rn "runElcomImport\|import-elcom-cli\|elcom-import" apps/api/src apps/api/prisma apps/api/entrypoint.sh \
  | grep -v "^apps/api/src/energy/"
# attendu : aucun résultat
  1. Chemin de démarrage inchangé par rapport à main :
git diff main --stat -- apps/api/entrypoint.sh apps/api/Dockerfile docker-compose.prod.yml \
  apps/api/prisma/seed.ts apps/api/src/plugins/
# attendu : aucune différence
  1. Constat à l'exécution : sur la copie de l'étape 1, avant l'étape 1.6 et après au moins 11 minutes de fonctionnement (une passe complète du cron hydro de 10 minutes), aucun import énergie ne doit avoir eu lieu :
docker exec am-prod-replay psql -U alpimonitor -d alpimonitor -tAc \
  "SELECT COUNT(*) FROM \"IngestionRun\" WHERE source = 'LINDAS_ELCOM'"
# attendu : 0

3. Merge (arbitrage humain)

  • CI verte sur la PR feat/energy → main (format, lint, typecheck, tests, build). En CI, 11 tests API d'intégration sont ignorés faute de base : c'est attendu.
  • Merge commit, comme les extensions précédentes ; tag proposé : v2.0.0-energy.
  • Le push sur main déclenche le déploiement Coolify. Les migrations sont appliquées au premier démarrage du conteneur api.

4. Après déploiement : lancer l'import à la main

  1. Déploiement sain, import pas encore lancé :
curl -s https://api.alpimonitor.fr/api/v1/health                 # status ok, database ok
curl -s https://api.alpimonitor.fr/api/v1/energy/positioning     # 404 ENERGY_DATA_NOT_IMPORTED

La page https://alpimonitor.fr/energie affiche alors « Les tarifs ElCom n'ont pas encore été importés sur ce serveur. »

  1. Lancer l'import, une fois, dans le conteneur api (le nom exact est visible dans Coolify ou via docker ps sur le VPS) :
ssh root@95.216.196.69
docker exec <api-container> node dist/energy/import-elcom-cli.js; echo "exit=$?"

Attendu : exit=0 en une à deux minutes, une ligne elcom-import: success, ≈ 50 000 tarifs créés et 91 lignes ignorées. En cas de exit=1, la cause est dans la ligne elcom-import: failure et dans le dernier IngestionRun LINDAS_ELCOM. Les contrôles de lignes ignorées, de présence de SEDRE SA et des médianes échouent avant toute écriture ; une collision de clé métier (fait republié sous un nouvel IRI) interrompt en revanche l'import pendant l'écriture, et relève d'une analyse manuelle avant toute relance.

  1. Vérifier :
curl -s https://api.alpimonitor.fr/api/v1/energy/positioning | jq '.data.period, .data.operatorsTotal'
# attendu : 2027 puis 536
curl -s https://api.alpimonitor.fr/api/v1/energy/positioning \
  | jq '[.data.categories[] | {c: .category, v: .verdict}]'
# attendu au 2026-09-27 : H1, C5, C6, C7 above ; H6, H7, H8, C1, C2 below ; les six autres nonDiscriminating

Puis ouvrir https://alpimonitor.fr/energie et https://alpimonitor.fr/energie/regroupement.

  1. Optionnel — idempotence en production : relancer la même commande ; attendu created: 0.

5. Retour arrière

  • Code : redéployer l'image précédente depuis Coolify. Les migrations étant additives, l'ancienne image fonctionne sur le nouveau schéma ; les tables énergie restent en place, inutilisées.
  • Données : l'import est idempotent. En cas d'échec partiel, il suffit de le relancer ; aucune suppression n'est nécessaire.
  • Aucune migration descendante n'est prévue : supprimer les tables énergie en production demanderait une migration dédiée, à décider le moment venu.