Chapitres
Sur cette page
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ù./deployreconstruit 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 :
- Parsing des options :
--allow-dirty(bypass du garde-fou dirty),--no-close(skip du post-ship). - Garde-fou dirty bloquant : vérification de la propreté de l'arbre Git ; sortie en erreur si sale, sauf
--allow-dirty. - Trap EXIT de nettoyage : retour sur
preprodgaranti même en cas d'erreur ; un échec de checkout est signalé bruyamment (pour éviter de rester bloqué surmain). - É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.
- É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.
- Étape 1 — Fusion
preprod → main: checkoutmain, pull, merge avec stratégie theirs, push vers l'origine. - Étape 2 — Migrations DB : voir §1ter ci-dessous.
- Étape 3 — Audit des agents : non bloquant.
- Étape 4 — Déploiement prod : appel au dispatcher générique avec le tenant et l'option
--all. - É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.
- Étape 6 — Post-ship : proposition de fermeture des chantiers de type
test(ignorable via--no-close).
Asymétrie
--skip-drift:./deployaccepte cette option pour bypasser la vérification de dérive en pré-prod (hotfix CI urgent)../shipn'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
./deployque 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 vitrinecodemyshop.comn'est pas vérifiée dans ce chemin — point à corriger dans la boucle de smoke du modeall.
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(oudeploy.<cible>.yamlsi 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
- 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). - 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.
- Analyse des drapeaux standards : extraction des options
CLEAN_CACHEet du drapeau d'exécution complète. - Affichage du bandeau de démarrage et initialisation du chronomètre de déploiement.
- Vérification de dérive de schéma (drift check) : si la section
driftest présente dans le YAML, le dispatcher compare l'état réel de la base de données au schéma attendu. - 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. - Lancement des hooks en arrière-plan : notamment l'audit des agents actifs.
- Génération des routes i18n (pré-build) : si la section
seed_i18nest 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. - Build Nuxt : construction du front-end.
- Seed i18n (si applicable) : alimentation des données de traduction post-build.
- Source maps (si le hook correspondant est activé) : téléversement des cartes de sources.
- Empaquetage : création d'une archive tar dans le répertoire temporaire local.
- Push Git en arrière-plan (si le hook est activé).
- Téléversement de l'archive vers le VPS client via copie sécurisée.
- 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.
- Installation des automates récurrents (si la section
cronest 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. - Attente des hooks en arrière-plan (push Git, audit agents).
- Boucle de health check : interrogation de l'URL de santé jusqu'à obtention d'une réponse valide ou expiration du délai maximum.
- 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
--cleanest 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
pigzsi disponible, avec repli automatique surgzip. La cible de compression est le seul répertoire.outputdu 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_oldavant 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.
-
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). - 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.
-
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 :
- 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.
- 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=1active 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./shipajoute 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 estmain, 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-dirtyexiste 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,./shipet le script de self-deploy du vaisseau-mère sont les sources de vérité du pipeline actuel.