Chapitres

DOC-04 / Référence technique · Chapitre 09

Déploiement & infrastructure de Synedre OS

Ce chapitre décrit le pipeline de mise en ligne de Synedre OS et de ses tenants, en détaillant l'asymétrie entre `./deploy` (preprod, piloté par l'IA) et `./ship` (production, gaté par la flotte `auto_ship_allowed`), ainsi que les mécanismes associés comme le drift auto-apply, les smokes post-déploiement et les exceptions founder sans preprod.

Déploiement & infrastructure

Cette section décrit le pipeline de mise en ligne de Synedre OS et de ses tenants : l'asymétrie entre les deux points d'entrée principaux, le dispatcher piloté par configuration, le pattern de build hors-VPS suivi d'un transfert d'archive, le cas particulier du self-deploy du vaisseau-mère, la source d'inventaire unique, la doctrine des secrets et la règle commit-avant-deploy.

1. Asymétrie ./deploy vs ./ship

Deux points d'entrée coexistent à la racine du dépôt. Ils ne ciblent pas le même environnement et n'ont pas le même propriétaire.

./deploy ./ship
Cible Pré-production (ou runtime live du vaisseau-mère / PROD pour les sites fondateurs sans pré-prod) Production
Propriétaire IA / worker / cascade, systématique Gaté par la flotte : IA/tick si le tenant autorise le ship automatique, humain sinon
Git Auto-commit du dirty, reste sur la branche preprod Bascule sur main → pull → merge preprod (stratégie theirs) → push, puis retour preprod garanti par un trap EXIT
Garde-fou dirty Auto-commit silencieux ; refus dur si l'on est déjà sur main Bloquant : refus si l'arbre de travail est sale, sauf option --allow-dirty
Vérification de dérive de schéma Une seule cible pré-prod câblée ; tout autre tenant → skip explicite. Bypass possible via --skip-drift Deux cibles (hub central + DB prod tenant si connue). Aucun équivalent --skip-drift : la dérive du hub central est strictement bloquante
Application automatique de la dérive Le moteur de dérive génère et applique en une transaction les DDL idempotentes manquantes (CREATE TABLE IF NOT EXISTS, ALTER TABLE ADD COLUMN IF NOT EXISTS). Actif par défaut ; désactivable ponctuellement. Appliqué avant tout build ou rechargement. Non applicable (la dérive est appliquée manuellement avant le ship)
Smoke post-déploiement Smoke fonctionnel, suivi d'un smoke visuel non bloquant. Désactivable indépendamment. Vérification de l'environnement PM2 (anti-faux-positif) puis smoke fonctionnel incluant des vérifications de contenu JSON
Migrations DB — Affiche les fichiers .sql en attente selon le périmètre ; non bloquant, non auto-appliqué — aucun runner branché
Clôture — Propose la fermeture des chantiers de type test après le ship

Doctrine :

  • ./ship <tenant> cible la production, gaté par la table d'inventaire de la flotte (champ autorisant ou non le ship automatique). La nuit, un ship non surveillé exige en outre une preuve QA verte.
  • ./deploy <tenant> est toujours déclenché par l'IA, systématiquement et sans demander — y compris sur le vaisseau-mère où ./deploy reconstruit le runtime live (pas de pré-prod dédiée).
        ┌─────────────┐   ./deploy <tenant>    ┌──────────────┐
  IA →  │  branche     │ ────────────────────→  │  pré-prod /   │
        │  preprod     │   (auto, N fois)        │  runtime live │
        └─────────────┘                          └──────────────┘
                │
                │  Revue pré-prod validée
                ▼
        ┌─────────────┐   ./ship <tenant>      ┌──────────────┐
 ship → │ merge preprod│ ────────────────────→  │  PRODUCTION   │
        │   → main     │   (gaté flotte)         │              │
        └─────────────┘                          └──────────────┘

Exception — site public personnel du fondateur : ./deploy est refusé pour ce tenant — le site vit directement sur le VPS CodeMyShop et ne dispose pas de pré-prod. Toute modification passe uniquement par ./ship.

Exception — tenants fondateurs sans pré-prod (synedre, un tenant hébergeur, codemyshop) : ./deploy sur ces tenants pointe la production directement. La frontière habituelle deploy = pré-prod ne tient pas pour eux — l'IA déclenche ./deploy mais celui-ci modifie la production. C'est voulu : aucune pré-prod dédiée n'existe pour les sites fondateurs. ./ship reste disponible (fusion preprod → main + cérémonie complète) et est lui aussi gaté par la flotte.

1bis. Détail du pipeline ./ship (étapes numérotées)

./ship <tenant> enchaîne, dans l'ordre :

  1. Parsing des options : --allow-dirty (bypass du garde-fou dirty), --no-close (skip du post-ship).
  2. Garde-fou dirty bloquant : vérification de la propreté de l'arbre Git ; sortie en erreur si sale, sauf --allow-dirty.
  3. Trap EXIT de nettoyage : retour sur preprod garanti même en cas d'erreur ; un échec de checkout est signalé bruyamment (pour éviter de rester bloqué sur main).
  4. Étape 0 — Nettoyage des artefacts root : suppression des répertoires de sortie appartenant à root, laissés par d'anciens déploiements, qui bloqueraient sinon l'écriture.
  5. Étape 0bis — Vérification de dérive du hub central : bloquante, sans bypass possible. Étape 0ter : dérive de la DB prod du tenant si la cible est connue.
  6. Étape 1 — Fusion preprod → main : checkout main, pull, merge avec stratégie theirs, push vers l'origine.
  7. Étape 2 — Migrations DB : voir §1ter ci-dessous.
  8. Étape 3 — Audit des agents : non bloquant.
  9. Étape 4 — Déploiement prod : appel au dispatcher générique avec le tenant et l'option --all.
  10. Étape 5 — Smoke prod : vérification de l'environnement PM2 avant le smoke fonctionnel. Le smoke inclut des vérifications HTTP et de contenu JSON.
  11. Étape 6 — Post-ship : proposition de fermeture des chantiers de type test (ignorable via --no-close).

Asymétrie --skip-drift : ./deploy accepte cette option pour bypasser la vérification de dérive en pré-prod (hotfix CI urgent). ./ship n'a aucun équivalent — la dérive du hub central y est strictement bloquante : on ne ship jamais en production sur un schéma divergent.

1ter. Migrations DB lors du ship

Le ship affiche les fichiers .sql en attente mais ne les applique jamais automatiquement : le runner automatique a été désactivé et aucun runner PostgreSQL n'est branché. Le bloc est purement informatif et non bloquant — il imprime les commandes à exécuter manuellement.

Un mapping tenant → périmètre de migrations évite de mélanger les migrations du vaisseau-mère et celles des tenants :

Tenant (argument) Périmètre → dossier migrations/<périmètre>/
Vaisseau-mère / hub central mothership
Un tenant e-commerce A périmètre dédié A
Un tenant e-commerce B périmètre dédié B
Un tenant hébergeur périmètre dédié
Site personnel fondateur périmètre dédié
CodeMyShop périmètre dédié
all Cas spécial : agrège tous les périmètres
Autre Fallback : périmètre = nom du tenant

Cas spécial all : agrège l'ensemble des fichiers .sql de tous les sous-dossiers de migrations, en excluant les sous-dossiers _applied/ et applied/ (migrations déjà passées). Les autres périmètres listent simplement les fichiers .sql du dossier correspondant.

2. Routage de ./deploy et ./ship par tenant

Le dispatcher résout chaque tenant vers un script dédié ou vers le dispatcher générique de déploiement.

Tenant (argument) ./deploy → ./ship →
Vaisseau-mère (synedre-os, défaut) Vérification de verrou de santé puis script de déploiement dédié au vaisseau-mère (--prod --all), ou variante build-hôte si HOST_BUILD=1 Idem script dédié vaisseau-mère (--prod --all)
Site personnel fondateur Refusé — pas de pré-prod Dispatcher générique (--all)
Site Synedre public Dispatcher générique → PROD direct Dispatcher générique ⚠️ — vérification PM2 potentiellement bloquante si le tenant n'est pas déclaré dans le script de vérification (bug latent)
CodeMyShop Dispatcher générique → PROD direct (plus de pré-prod dédiée) ; smoke activé Dispatcher générique
CodeMyShop demo Dispatcher générique → staging démo —
Un tenant e-commerce A Dispatcher générique → --target=preprod ; smoke désactivé (pré-prod derrière auth basique) Dispatcher générique sans --target → prod
Un tenant e-commerce B Dispatcher générique ; smoke activé ⚠️ Tombe dans le cas par défaut → script Ansible legacy probablement en erreur (pas de VPS prod connu). Les alias directs de ce tenant déclenchent un message « Pas de prod ».
Un tenant hébergeur Dispatcher générique → PROD ; smoke activé Dispatcher générique
all 5 jobs parallèles en arrière-plan : vaisseau-mère, CodeMyShop (PROD), un tenant e-commerce A (pré-prod), un tenant e-commerce B, un tenant hébergeur. Smoke post-jobs sur l'ensemble. ⚠️ Le smoke all teste la démo CodeMyShop, pas la vitrine prod. 3 jobs parallèles : vaisseau-mère + site fondateur + CodeMyShop. Smoke sur ces trois cibles.
--list / -l Affiche le contenu de la table d'inventaire de la flotte (voir §6) —
Autre (tenant inconnu) Fallback : script Ansible → pré-prod Fallback : script Ansible → prod

Convergence vers le dispatcher unique : depuis la standardisation du pattern de déploiement et la purge des scripts legacy, tous les tenants distants sauf le vaisseau-mère passent par le dispatcher générique, aussi bien pour ./deploy que pour ./ship. Le vaisseau-mère conserve son script dédié car le self-deploy est architecturalement distinct (build Docker local, sans transfert réseau).

Incohérence connue — ./deploy all : le déploiement cible la vitrine CodeMyShop en production, mais le smoke suivant teste uniquement la démo (staging). La vitrine codemyshop.com n'est pas vérifiée dans ce chemin — point à corriger dans la boucle de smoke du mode all.

Le dispatcher piloté par YAML

Le pipeline de déploiement standard — utilisé pour tous les tenants distants à l'exception du vaisseau-mère lui-même — est piloté par un script bash central qui reçoit en argument le nom du tenant, une cible optionnelle et des drapeaux de comportement. Sa spécification complète est maintenue dans un document de schéma YAML dédié, régulièrement enrichi d'exemples canoniques couvrant les variantes Docker, PM2 et multi-cibles.

Résolution du fichier de configuration

À l'invocation, le dispatcher résout le fichier YAML à utiliser selon une logique simple :

  • Si le tenant désigne le vaisseau-mère, le dispatcher tente de charger un fichier de configuration dédié — ce branchement est documenté mais inerte en pratique : aucun fichier correspondant n'existe sur le disque. Le déploiement du vaisseau-mère est en réalité pris en charge en amont par un pipeline autonome distinct (auto-déploiement), jamais par ce dispatcher.
  • Pour tout autre tenant, le fichier chargé est le deploy.yaml (ou deploy.<cible>.yaml si une cible est précisée) situé dans le répertoire de configuration du tenant concerné.

Note : Le nom court synedre (sans suffixe) désigne un tenant client ordinaire et emprunte le chemin tenant classique — il ne déclenche pas le branchement vaisseau-mère.

Étapes du pipeline

  1. Analyse des arguments : le dispatcher interprète la cible optionnelle (avec le séparateur = obligatoire), le drapeau d'exécution complète (obligatoire) et le drapeau de nettoyage de cache (optionnel).
  2. Validation et résolution du YAML : un validateur Python charge le fichier YAML, vérifie la cohérence de toutes les sections et émet des variables d'environnement shell que le dispatcher évalue. Toute erreur de validation provoque l'arrêt immédiat.
  3. Analyse des drapeaux standards : extraction des options CLEAN_CACHE et du drapeau d'exécution complète.
  4. Affichage du bandeau de démarrage et initialisation du chronomètre de déploiement.
  5. Vérification de dérive de schéma (drift check) : si la section drift est présente dans le YAML, le dispatcher compare l'état réel de la base de données au schéma attendu.
  6. Application automatique de la dérive (drift auto-apply) : les DDL idempotentes manquantes (CREATE TABLE IF NOT EXISTS, ALTER TABLE ADD COLUMN IF NOT EXISTS) sont générées et appliquées en une seule transaction, avant tout build ou rechargement. Ce comportement est actif par défaut et peut être désactivé ponctuellement via une variable d'environnement dédiée.
  7. Lancement des hooks en arrière-plan : notamment l'audit des agents actifs.
  8. Génération des routes i18n (pré-build) : si la section seed_i18n est présente, le dispatcher interroge les tables de catégories et de métadonnées localisées du tenant (via SSH/Docker) pour produire le fichier JSON des segments de routes localisées que le moteur de build consomme. Ce pas est non bloquant : si la base de données est inaccessible, le build se replie sur les valeurs codées en dur.
  9. Build Nuxt : construction du front-end.
  10. Seed i18n (si applicable) : alimentation des données de traduction post-build.
  11. Source maps (si le hook correspondant est activé) : téléversement des cartes de sources.
  12. Empaquetage : création d'une archive tar dans le répertoire temporaire local.
  13. Push Git en arrière-plan (si le hook est activé).
  14. Téléversement de l'archive vers le VPS client via copie sécurisée.
  15. Rechargement distant, selon la variante déclarée dans le YAML :
    • pm2 : rechargement graceful ou redémarrage forcé.
    • docker : redémarrage du service via Docker Compose.
  16. Installation des automates récurrents (si la section cron est présente) : les tâches planifiées du tenant sont posées dans le crontab de l'utilisateur SSH sur le VPS client — jamais sur le vaisseau-mère. Chaque bloc est marqué de bornes idempotentes nommées : à chaque déploiement, l'ancien bloc est retiré et réécrit. Une liste vide déclenche le nettoyage complet du bloc. Doctrine : les automates récurrents s'exécutent au plus près de leur base de données, sur la machine qui héberge le service.
  17. Attente des hooks en arrière-plan (push Git, audit agents).
  18. Boucle de health check : interrogation de l'URL de santé jusqu'à obtention d'une réponse valide ou expiration du délai maximum.
  19. Affichage du bandeau de fin avec durée totale.

Structure du fichier deploy.yaml

Chaque tenant dispose d'un fichier YAML décrivant l'intégralité de son pipeline. Le validateur Python vérifie la cohérence de toutes les sections avant toute exécution. Les sections principales sont :

Section Obligatoire Description
name Oui Identifiant kebab-case du déploiement (regex ^[a-z0-9][a-z0-9_-]*$).
build Oui Chemin client local (local_client, requis) et environnement Node (node_env, défaut production).
ssh Oui Hôte distant (requis), utilisateur SSH (défaut ubuntu), clé optionnelle.
remote Oui Variante de déploiement (pm2 ou docker) et paramètres associés.
health Oui URL de santé (requise) et délai maximum d'attente (défaut 45 secondes).
drift Non Configuration de la vérification et de l'application automatique de dérive de schéma.
seed_i18n Non Paramètres de génération et d'alimentation des routes i18n.
hooks Non Hooks optionnels : audit des agents, téléversement des source maps, push Git.
cron Non Liste d'automates récurrents à installer sur le VPS client (key, schedule, command, comment).

Garde-fous de cohérence par variante

Le validateur applique des contraintes mutuellement exclusives selon la variante choisie :

  • Variante pm2 : le nom de l'application PM2 est requis ; les champs spécifiques à Docker (conteneur, sous-répertoire) sont interdits.
  • Variante docker : le nom du conteneur est requis ; les champs spécifiques à PM2 sont interdits.

Validation de la section cron

Chaque entrée d'automate récurrent est validée individuellement : la clé doit être en kebab-case, l'expression temporelle doit comporter exactement cinq champs crontab, et la commande ne peut être vide. La section est émise même si elle est vide — un tableau vide signale à l'installeur de nettoyer intégralement le bloc crontab du tenant sur le VPS distant.

Exemple de configuration

Voici la structure d'un fichier deploy.yaml représentatif pour un tenant utilisant la variante PM2 :

name: <nom-du-tenant>
build:
  local_client: codemyshop/tenants/<nom-du-tenant>
  node_env: production
ssh:
  host: <adresse-du-vps-client>
remote:
  variant: pm2
  dir: /var/www/codemyshop/app/codemyshop/tenants/<nom-du-tenant>
  pm2_app: <nom-app-pm2>
  pm2_remote_user: <utilisateur-pm2>
health:
  url: https://<domaine-du-tenant>
hooks:
  git_push: <nom-du-tenant>

Pattern : build sur le vaisseau-mère, transfert et rechargement à distance

L'invariant central pour les déploiements vers un VPS distant est le suivant : le VPS ne compile jamais. Le bundle de l'application est compilé sur le vaisseau-mère, compressé en archive, transféré, extrait puis rechargé sur la cible. Le journal de déploiement affiche explicitement le mode BUILD HOST dès le démarrage.

  ┌──────────────────────── VAISSEAU-MÈRE (host) ─────────────────────┐
  │ 1. npm install --prefer-offline (delta racine monorepo)           │
  │ 2. npx vitest run product-urls.test.ts   (invariants URL)         │
  │ 3. npx nuxi build <client-local>          → .output/              │
  │ 4. tar -I pigz -cf /tmp/<nom>-build.tar.gz -C <client> .output   │
  └────────────────────────────────┬──────────────────────────────────┘
                                    │ transfert sécurisé (SCP)
                                    ▼
  ┌────────────────────────── VPS CLIENT ─────────────────────────────┐
  │ 5. mv .output .output_old        (rollback prêt)                  │
  │ 6. tar -I <décompresseur> -xf /tmp/<archive>                     │
  │ 7. pm2 reload <app> --update-env   |   docker compose restart <c> │
  │ 8. si online → rm .output_old   sinon → affiche commande de       │
  │    restauration + exit 1                                           │
  └────────────────────────────────┬──────────────────────────────────┘
                                    │
                                    ▼  poll HTTP 200 (délai max configurable)
                              health check

Détail des étapes

  • Compilation Nuxt : purge du répertoire de sortie (et du cache si l'option --clean est activée), installation des dépendances en mode hors-ligne sans audit, puis smoke test bloquant sur les invariants de forme d'URL produit. Si le build incrémental échoue, une seconde tentative avec cache purgé est lancée automatiquement.
  • Compression : l'archive est produite avec pigz si disponible, avec repli automatique sur gzip. La cible de compression est le seul répertoire .output du client concerné.
  • Transfert : l'archive est copiée vers le répertoire temporaire du VPS via SCP, puis supprimée localement après envoi.
  • Rotation et extraction sur le VPS : le répertoire de sortie existant est renommé en .output_old avant extraction, ce qui permet un retour arrière immédiat. Une correction de compatibilité CommonJS est appliquée à la volée sur l'arbre extrait (suppression d'un sous-module de driver de base de données qui provoque des conflits de tree-shaking).
  • Rechargement gracieux (PM2) : la commande de rechargement PM2 conserve les connexions actives et injecte les variables d'environnement à jour. Un symlink optionnel vers un répertoire de fichiers statiques peut être créé pour éviter des erreurs de résolution de fichiers côté back-office. Un rechargement hard (suppression + redémarrage + sauvegarde PM2) est utilisé lors d'un premier déploiement ou lorsque la configuration le requiert explicitement.
  • Rechargement via Docker Compose : après extraction, le container applicatif est redémarré ; la présence et l'état running du container sont vérifiés avant de poursuivre.
  • Health check : une boucle interroge l'URL cible jusqu'à obtenir un HTTP 200, ou déclenche un échec au terme du délai maximum (45 secondes par défaut).
  • Encadrement visuel : des bandeaux de début et de fin affichent le mode de déploiement et un récapitulatif chronométré par phase.
  • Installation de tâches planifiées : un installeur dédié configure les tâches cron par client sur le VPS distant (cf. étape de configuration planifiée §3).

Rollback

.output_old est conservé pendant toute la durée du rechargement et purgé uniquement si le process revient en ligne. En cas d'échec (process ou container non détecté comme actif), le script affiche uniquement la commande de restauration suggérée et sort avec un code d'erreur 1 — la restauration est manuelle et délibérée, le script ne l'exécute jamais automatiquement. La variable d'environnement SHIP_KEEP_ROLLBACK=1 force la conservation de .output_old même en cas de succès.

Smoke post-déploiement : trois couches de validation

Le script de smoke post-déploiement comprend deux sous-niveaux de vérification, auxquels s'ajoute depuis mi-2026 un troisième niveau visuel.

  1. Vérifications HTTP : une requête curl est émise vers chaque URL configurée. Un code 5xx est bloquant ; un code 4xx génère un avertissement non bloquant. La boucle générale couvre un ensemble de tenants configurés ; certains tenants sont vérifiés individuellement via la commande ./ship. Les URLs couvertes incluent notamment la racine, les pages de gouvernance publique (/constitution, /charter, /conduct), la liste des agents (/agents) et les endpoints API associés (/api/agents, /api/automates).
  2. Vérifications de contenu JSON : au-delà du code HTTP, la forme du JSON retourné par l'API de navigation est validée. Cette couche a été introduite à la suite d'un incident où une application rendait HTTP 200 pendant deux jours avec un payload JSON contenant un flag d'erreur — la navigation était vide et le pied de page affichait des clés brutes sans traduction. Sans ce niveau, les ruptures SSR data-driven passaient invisibles derrière un statut HTTP nominal.
  3. Smoke visuel (non bloquant) : un agent de vérification visuelle prend une capture d'écran via Playwright et rend un verdict multimodal. Si aucune configuration de vérification visuelle n'existe pour le client concerné, l'étape est ignorée sans latence. Ce niveau est actif par défaut et peut être désactivé via la variable d'environnement SY_HUOYAN_SMOKE=0.
    python3 -m <module-smoke-visuel> <identifiant-tenant>

Anti-faux-positif : vérification des variables d'environnement du process distant

Un health-check HTTP 200 ne suffit pas à valider un déploiement. Un incident documenté illustre le risque : un process PM2 redéployé sans aucune variable de connexion à la base de données rendait HTTP 200 pendant deux jours, car la page produisait un squelette HTML valide — mais toutes les routes SSR data-driven (navigation, pied de page, avis, traductions) retournaient 500 en silence.

Pour prévenir ce scénario, la commande ./ship exécute un script de vérification d'environnement avant le smoke HTTP. Ce script se connecte en SSH au VPS et inspecte les variables critiques déclarées dans l'environnement du process PM2 distant. Les codes de retour sont :

  • 0 — toutes les variables critiques sont présentes ;
  • 1 — des variables sont manquantes (bloquant) ;
  • 2 — connexion SSH impossible ou process introuvable (bloquant).

Les cibles non gérées par PM2 (vaisseau-mère, environnements de smoke isolés) sont ignorées avec un retour 0.

Garde-fou pré-build : vérification de cohérence du lockfile

Avant tout déploiement du site principal, un script de contrôle détecte deux dérives bloquantes dans le lockfile avant tout npm install ou compilation :

  1. Présence d'une version de l'outil de minification CSS antérieure au seuil requis — les plugins PostCSS transitifs manquants provoquent un build cassé en silence.
  2. Version du framework Nuxt inférieure au seuil de stabilité requis — risque de dérive avec le bundler.

La présence d'overrides de version dans le manifeste racine génère un avertissement non bloquant. Toute erreur détectée produit un message correctif explicite et annule le déploiement avec exit 1. Le correctif suggéré est la suppression du lockfile et une réinstallation propre des dépendances.

Self-déploiement du vaisseau-mère

Frontière réseau : aucune préproduction client sur le vaisseau-mère

Le vaisseau-mère est la machine sensible qui héberge l'orchestration, le Hub, les secrets et les services internes. Il ne doit héberger ni publier aucun site de client ou de prospect, y compris derrière une authentification Basic Auth ou une directive X-Robots-Tag: noindex. Ces mesures ne suppriment pas le point d'entrée réseau ni le risque de rebond depuis un CMS ou son container vers le réseau Docker interne.

Les préproductions clients temporairement exposées vivent exclusivement sur un serveur tampon dédié, avant migration vers le VPS dédié du client. Un script de garde applique une liste fermée sur la configuration Nginx : seul le Hub et son upstream applicatif sont autorisés. Cette garde est bloquante au niveau du pre-commit et peut également auditer l'état runtime avec l'option --runtime.

Mécanisme de self-déploiement

Le site principal du vaisseau-mère ne passe pas par le pattern build-host / upload décrit plus haut. C'est un self-déploiement local : le vaisseau-mère compile à l'intérieur de son propre container applicatif. La commande ./deploy déclenche un rebuild en production live du cockpit de pilotage, sans cérémonie git supplémentaire.

Spécificités par rapport aux tenants distants :

  • Transfert local par copie Docker : au lieu d'un transfert SCP, une archive source minimale (code applicatif et manifestes de dépendances uniquement) est copiée dans le container via docker cp. Les répertoires de cache et de sortie de build sont explicitement exclus de l'archive. Côté container, le cache de build du client est mis de côté avant extraction puis restauré — c'est le levier principal de performance : le build est incrémental, pas un cold build.
  • Optimisation du cycle npm : un sentinel basé sur l'empreinte du lockfile est conservé dans le container. Si le lockfile n'a pas changé depuis le dernier déploiement, l'installation des dépendances est ignorée.
  • Build dans le container : la compilation Nuxt s'exécute à l'intérieur du container en cours d'exécution. L'ancienne version reste disponible en ligne pendant toute la durée du build. Aucun gestionnaire de process de type PM2 n'est utilisé dans ce pipeline : le container lance directement le serveur Node issu du build.
  • Rotation atomique : après le build, le container est arrêté proprement, le répertoire de sortie est copié sur l'hôte, puis le container est recréé via docker compose up --force-recreate, ce qui force la relecture du fichier d'environnement (correction d'un incident documenté lié à des variables non rechargées lors d'un simple restart).
  • Health check par UUID de build : le contrôle de santé ne se limite pas à un HTTP 200. L'UUID du build attendu (lu dans le manifeste de sortie côté hôte) est comparé à celui retourné par l'endpoint de santé du container. Cela garantit que le nouveau build est effectivement actif, et non l'ancien servi par un hot-reload partiel.
  • Mode source indépendante : le site du vaisseau-mère n'étend plus le socle partagé CodeMyShop. L'archive transférée est réduite au strict nécessaire (gain de taille d'environ 19 MB à environ 3 MB).
  • Variante accélérée : la variable HOST_BUILD=1 active un chemin alternatif qui effectue le build sur l'hôte puis effectue un swap atomique dans le container, avec un gain de temps d'environ 35 %.
  • Cérémonie complète via ./ship : la commande ./ship ajoute la séquence complète (merge de branche, push, dérive du Hub, audit des agents, smoke de production, clôture de chantier) mais appelle in fine le même script de self-déploiement.

Inventaire de la topologie — source unique de vérité

L'ensemble de la topologie du parc (liste des VPS clients, accès SSH, containers de base de données) est centralisé dans une table de référence unique hébergée dans la base de données du vaisseau-mère. Toute autre source — fichiers d'architecture, notes, documentation statique — peut être obsolète ; seule cette table fait foi.

Consulter l'inventaire

La commande ./deploy --list interroge cette table et affiche la topologie courante sous forme de tableau aligné. L'ordre de tri place l'infrastructure centrale en tête, suivie des environnements critiques, puis des tenants par ordre alphabétique.

Les colonnes significatives de la table sont les suivantes :

Colonne Rôle
Identifiant tenant Clé logique unique du tenant dans le parc
Environnement production, staging, infra, legacy ou audit
Adresse et domaine Point d'accès SSH et URL publique du VPS
Accès SSH Utilisateur et chemin de clé SSH (stockés en table, jamais en dur dans le code)
Base de données associée Container, nom de base, utilisateur et nom de la variable d'environnement portant le mot de passe — jamais la valeur elle-même
Runtime web Container applicatif, présence de PrestaShop, présence de Nuxt
Criticité business Indicateur de criticité, MRR associé, offre souscrite
Ship automatique autorisé Booléen indiquant si un déploiement en production peut être déclenché sans confirmation manuelle ⚠️
Codename de déploiement Identifiant utilisé pour construire la commande exacte de déploiement
Codename client Clé étrangère vers le référentiel client fédéré (spine 2026)

Cas particuliers de l'inventaire actif

Le parc actif compte une douzaine d'entrées. Quelques cas méritent une attention particulière pour éviter toute mauvaise catégorisation :

  • Deux tenants portant purpose = 'production' correspondent à des projets annexes actuellement en sommeil — des productions orphelines, non des environnements legacy ou d'audit. Ils ne doivent pas être agrégés sous une étiquette générique.
  • Un tenant apparaît en double entrée : une ligne pour sa version Nuxt active en production, une autre pour sa version legacy sans Nuxt. Ces deux lignes doivent être traitées séparément.
  • Un tenant précédemment en statut audit a été décommissionné (passé active = 0) : son dossier et ses fichiers de configuration legacy ont été retirés ; l'historique client reste conservé dans le référentiel client fédéré.

Doctrine d'usage avant tout déploiement

Avant toute exécution de ./deploy ou ./ship, la procédure impose de consulter la fiche du tenant dans le cadran de gestion afin de copier la commande exacte associée à son codename de déploiement. Cette règle prévient les erreurs de ciblage entre environnements.


Doctrine des secrets — cinq niveaux de portée

Chaque secret possède un seul fichier canonique. La présence d'une même clé dans deux fichiers distincts est considérée comme un bug de configuration, pas comme une redondance acceptable.

Les cinq niveaux

# Fichier de référence Périmètre Versionné git
1 Fichier d'environnement propre à chaque tenant Variables d'un seul tenant (lues par son container applicatif et son descripteur de déploiement) Non
2 Fichier d'environnement du vaisseau-mère (core) Cockpit et container applicatif central Non
3 Fichier d'environnement hôte du vaisseau-mère Scripts hôte : tâches planifiées, automates Python, accès SSH aux VPS clients Non
4 Fichier d'environnement partagé (hors dépôt) Clés transverses : clés API des fournisseurs IA, clé de chiffrement API, configuration SMTP maître Non (hors dépôt)
5 Fichiers .env.example Templates publics open-source — uniquement des noms de variables, jamais de valeurs Oui

Ordre de chargement et règle de priorité

Au démarrage du container applicatif central, les fichiers sont chargés dans l'ordre suivant : fichier partagé (niveau 4) → fichier core (niveau 2) → fichier hôte (niveau 3). En cas de clé présente à la fois au niveau 2 et au niveau 3, le niveau 3 (hôte) prend la priorité. Conséquence : une même clé ne doit jamais être définie aux deux niveaux simultanément.

Note : La documentation interne propose une vue résumée en trois couches (infrastructure / core / tenants). La doctrine complète à cinq niveaux détaille en plus le fichier partagé cross-projet et les templates d'exemple. En cas de divergence, la doctrine à cinq niveaux prévaut.

Règle anti-fuite absolue

Aucun secret en clair ne doit figurer dans un fichier versionné. Lorsqu'un fichier git-tracké doit faire référence à un secret, il cite uniquement le nom de la variable d'environnement et le fichier .env* qui la porte — jamais la valeur. Par exemple, le descripteur de déploiement d'un tenant référence le nom de la variable contenant le mot de passe de base de données ; la table d'inventaire topologique stocke le nom de la variable d'environnement portant le mot de passe, pas le mot de passe lui-même.


Règle de commit préalable au déploiement

Un incident passé a démontré qu'une modification locale non commitée peut être silencieusement écrasée lors d'un déploiement : le mécanisme de synchronisation effectue un git checkout sur le répertoire cible, ce qui ramène tout fichier non versionné à son état en dépôt. Dans le cas documenté, un correctif d'URL appliqué en production mais jamais commité a été perdu, provoquant quatre heures de panne.

Comportement selon la commande

  • ./deploy (preprod / tenants) : si l'arbre de travail contient des modifications non commitées, un commit automatique silencieux est créé ("chore(auto): pre-deploy snapshot <horodatage ISO>") et poussé en arrière-plan — sauf si la branche courante est main, auquel cas le déploiement est refusé en dur.
  • ./ship (production) : tout arbre de travail sale est bloquant — la commande s'arrête avec un message demandant de commiter d'abord. Un paramètre --allow-dirty existe pour les hotfixes intentionnels uniquement.

Garde-fous complémentaires

  • Un script de vérification de propreté Git est exécuté avant chaque déploiement. Il retourne trois états : arbre propre, arbre sale, ou répertoire hors dépôt. Son périmètre est délimité par une option --target : une modification dans les fichiers du vaisseau-mère ne bloque pas le déploiement d'un tenant, et vice-versa.
  • Un hook de fin de session bloque la clôture si l'arbre de travail n'est pas propre. La règle de fonctionnement est que l'agent commite ; l'opérateur humain ne tape jamais manuellement de commande Git d'ajout ou de commit.

Annexe — fichiers de référence du système de déploiement

Composant Rôle
Point d'entrée de déploiement preprod Commande racine ./deploy — routage vers le bon tenant ou la preprod du vaisseau-mère
Point d'entrée de déploiement production Commande racine ./ship — gestion du consentement de flotte (auto_ship_allowed) et cérémonie complète de mise en production
Dispatcher piloté par descripteur Lit le descripteur de déploiement YAML de chaque tenant, valide son schéma, et orchestre les étapes : build, SSH, dérive, i18n, santé, hooks, cron
Bibliothèque d'helpers de déploiement Fonctions partagées : build, packaging, upload, rechargement, vérification de santé, bandeaux console
Parseur du descripteur YAML Analyse et validation du fichier de déploiement d'un tenant ; exporte les variables de déploiement pour les scripts shell
Auto-déploiement du vaisseau-mère (standard) Procédure de self-deploy du container applicatif central : build interne au container, arrêt, copie, redémarrage
Auto-déploiement du vaisseau-mère (accéléré) Variante avec build sur l'hôte et permutation atomique — réduit le temps d'indisponibilité
Vérificateur de propreté Git Garde-fou pré-déploiement : détecte les modifications non commitées, scopé par cible
Vérificateur de cohérence du lock Garde-fou pré-déploiement vaisseau-mère : détecte les dérives de versions dans le fichier de verrouillage des dépendances
Sonde de fumée HTTP Vérifie les réponses HTTP et le contenu/JSON post-déploiement sur l'ensemble du parc de tenants actifs ; couvre également le vaisseau-mère et CodeMyShop
Vérificateur d'environnement PM2 Contrôle la cohérence des variables d'environnement injectées dans les processus PM2 avant la sonde de fumée en production
Schéma du descripteur de déploiement Spécification complète du format YAML avec exemples canoniques
Runbook d'exploitation VPS Procédures SSH, PM2, Docker, renouvellement de certificats
Doctrine des secrets Référence des cinq niveaux de secrets et règles anti-fuite
Table d'inventaire topologique Source de vérité unique de la topologie du parc (base de données du vaisseau-mère)

⚠️ Attention : Le fichier de documentation sur le workflow général est partiellement obsolète. Il décrit encore un environnement de preprod démantelé et un script de ship remplacé. En cas de conflit entre la documentation et le code, le code fait foi : les commandes ./deploy, ./ship et le script de self-deploy du vaisseau-mère sont les sources de vérité du pipeline actuel.