Aller au contenu

Livraison continue

La chaîne de livraison sépare trois responsabilités : GitHub Actions valide et publie, GHCR conserve les artefacts immuables, Dokploy exécute les applications et protège leurs volumes. Staging est continu ; la production reste une promotion explicitement approuvée.

feature/* → PR develop → CI → image sha-… → staging continu
↓
release/x.y.z → PR main → CI → staging du candidat
↓
approbation production → sauvegarde
↓
version → backend → web → smoke → changelog → notification

Chaque CI réussie sur develop ou main publie une image identifiée par le SHA complet :

ghcr.io/chris589/harmony-backend:sha-<40 caractères>
ghcr.io/chris589/harmony-web:sha-<40 caractères>

Ne déployez jamais latest en production. Le SHA est également embarqué dans l’image et exposé par X-Harmony-Revision sur /api/hello et /healthz. Le workflow attend la fin du déploiement Dokploy, puis refuse la livraison si l’URL publique ne renvoie pas la révision attendue.

Staging définit SEO_INDEXING_ENABLED=false et laisse SEO_INDEXING_ALLOWED_HOSTS vide. Le workflow staging vérifie le header X-Robots-Tag: noindex. La promotion vérifie la politique déclarée dans la variable GitHub SEO_INDEXING_ENABLED de l’environnement production : false exige noindex, true refuse noindex. En l’absence de cette variable, le contrôle attend false, comme le réglage applicatif par défaut. Une valeur différente de true ou false ou une réponse HTTP autre que 200 fait échouer le contrôle. Le contrôle ne modifie ni les variables Dokploy ni l’indexation du site. Gardez cette variable GitHub alignée avec SEO_INDEXING_ENABLED dans Dokploy. Pour activer l’indexation, configurez les deux à true et autorisez l’hôte public dans SEO_INDEXING_ALLOWED_HOSTS. Cloudflare Access protège le domaine de recette ; seul /healthz possède une règle Bypass ciblée pour permettre le contrôle de révision sans ouvrir le reste du site. Les previews restent en noindex grâce à la liste exacte SEO_INDEXING_ALLOWED_HOSTS, limitée à www.leclosdeseucalyptus.com dans la configuration publique cible. La bascule du domaine exige aussi d’aligner les URL de déploiement et de health check sur ce nouvel hôte. Voir Domaines et indexation.

Créez quatre applications Docker distinctes : backend staging, web staging, backend production et web production. Configurez leurs domaines, réseaux et variables d’exécution dans Dokploy. Les deux backends doivent utiliser des volumes nommés différents montés sur /app/pb_data ; aucune preview ni application staging ne doit monter le volume de production.

Pour les images privées GHCR, créez un jeton durable limité à read:packages et associez-le au registre Dokploy. N’utilisez pas le GITHUB_TOKEN d’un run comme identifiant persistant : il expire à la fin du job.

Avant d’automatiser les sauvegardes de volume, utilisez Dokploy 0.29.13 ou plus récent. Les versions antérieures contiennent une vulnérabilité critique dans cette fonction. Configurez ensuite une destination S3, un backup du volume PocketBase par environnement et l’option d’arrêt du conteneur pendant la copie pour obtenir un snapshot SQLite cohérent.

Créez un environnement staging dans chacun des deux dépôts.

Variables communes :

Nom Contenu
DOKPLOY_URL origine HTTPS de Dokploy, sans / final
DOKPLOY_APPLICATION_ID application staging du dépôt
DEPLOY_URL URL publique principale
DEPLOY_HEALTH_URL web : /healthz ; backend : /api/hello

Le backend reçoit aussi DOKPLOY_VOLUME_BACKUP_ID. Cette variable est obligatoire pour activer le déploiement staging : chaque livraison sauvegarde ainsi le volume avant que les migrations de l’image ne démarrent.

Secrets communs :

Nom Contenu
DOKPLOY_API_TOKEN jeton API Dokploy dédié à la CI
DOKPLOY_REGISTRY_USERNAME utilisateur autorisé à lire les packages GHCR
DOKPLOY_REGISTRY_TOKEN jeton GHCR limité à la lecture

Tant que DOKPLOY_APPLICATION_ID n’est pas défini, l’image est publiée mais le job de déploiement est ignoré. Pour le backend, le même garde-fou s’applique à DOKPLOY_VOLUME_BACKUP_ID. Cela permet d’intégrer la chaîne sans diriger accidentellement une première exécution vers la mauvaise application ou lancer une migration sans sauvegarde.

L’orchestrateur de production se trouve dans harmony-web, workflow Promote images to production. Configurez un environnement production avec approbateur obligatoire, auto-approbation désactivée et accès limité à main. Si le forfait GitHub ne permet pas les approbateurs sur un dépôt privé, conservez la confirmation PRODUCTION du workflow et faites valider la release hors bande avant son lancement.

Variables :

Nom Contenu
DOKPLOY_URL origine HTTPS de Dokploy
DOKPLOY_BACKEND_APPLICATION_ID application backend production
DOKPLOY_WEB_APPLICATION_ID application web production
DOKPLOY_BACKEND_VOLUME_BACKUP_ID backup du volume PocketBase production
BACKEND_HEALTH_URL URL absolue terminant par /api/hello
WEB_HEALTH_URL URL absolue terminant par /healthz
WEB_URL URL publique utilisée pour le smoke test
SEO_INDEXING_ENABLED true ou false, identique au réglage Dokploy ; défaut false
BACKEND_RELEASE_NOTIFICATION_URL URL absolue /api/internal/releases

Ajoutez les trois mêmes secrets que pour staging, puis :

Secret Usage
HARMONY_RELEASE_GITHUB_TOKEN crée les tags/releases et pousse les images versionnées des deux repos
HARMONY_RELEASE_NOTIFICATION_TOKEN authentifie l’appel interne qui avertit les utilisateurs du back-office

GitHub Packages impose actuellement un PAT classic pour GHCR : limitez-le aux scopes repo, read:packages et write:packages (ajoutez workflow si une révision de release modifie les workflows), donnez-lui une durée courte et faites-le tourner régulièrement. Utilisez exactement la même valeur pour HARMONY_RELEASE_NOTIFICATION_TOKEN dans l’environnement du backend production sur Dokploy et dans le secret GitHub production. Les identifiants staging et production doivent être différents, même lorsque les applications sont sur le même serveur Dokploy.

Une release produit coordonne les deux dépôts, même si un seul contient des changements fonctionnels. Les commits et titres de PR suivent Conventional Commits 1.0.0. Les PR de feature et de hotfix sont squash-mergées avec leur titre comme message final. Les PR de release utilisent un merge commit dont le titre reprend aussi celui de la PR, afin de conserver dans main tous les commits fonctionnels de la version. Utilisez notamment feat(scope): …, fix(scope): … et chore(release): prepare Le Clos des Eucalyptus x.y.z; marquez une incompatibilité avec ! et un footer BREAKING CHANGE:.

Pour la version x.y.z :

  1. vérifiez que les versions de develop souhaitées sont vertes et validées en staging ;
  2. créez release/x.y.z depuis develop dans harmony-backend et harmony-web ;
  3. n’acceptez sur ces branches que les corrections de stabilisation et la documentation de release ;
  4. ouvrez une PR release/x.y.z vers main intitulée chore(release): prepare Le Clos des Eucalyptus x.y.z dans chaque dépôt et attendez toute la CI ;
  5. fusionnez les deux PR avec Create a merge commit, sans squash ; le titre validé devient celui du merge commit et les commits fonctionnels restent visibles dans main ;
  6. attendez le redéploiement staging des deux commits de main et relevez leurs tags sha-… ;
  7. si la branche de release contient des corrections absentes de develop, fusionnez-la aussi vers develop avant de supprimer la branche.

Une correction urgente part de main sur hotfix/<description>, revient par PR vers main, passe par staging, puis est fusionnée dans develop. Les commits directs sur main et develop sont interdits.

Après une CI verte sur develop ou main, deploy-staging.yml :

  1. reconstruit le commit exact dans une image et publie sha-… dans GHCR ;
  2. sauvegarde le volume backend si le backup est configuré ;
  3. affecte l’image immuable à l’application Dokploy ;
  4. suit le déploiement jusqu’à son statut final ;
  5. contrôle la révision publique et un smoke test représentatif.

Pour un changement cross-stack, fusionnez et observez d’abord le backend rétrocompatible, puis le web. N’introduisez pas une rupture qui exige une bascule parfaitement simultanée.

Depuis la branche main dans Actions sur harmony-web, lancez Promote images to production, renseignez la version SemVer sans préfixe (0.2.0) et les deux tags sha-… produits après la fusion des PR de release. Saisissez PRODUCTION, contrôlez les deux révisions, puis approuvez l’environnement lorsque cette protection est disponible.

Le workflow exécute dans cet ordre :

  1. validation stricte de la version, du lancement depuis main et des tags immuables ;
  2. pour une nouvelle version, vérification que chaque SHA est exactement la tête de main de son dépôt ; pour un redéploiement, vérification que les tags v… existants ciblent les mêmes SHA ;
  3. ajout d’une couche de métadonnées aux images validées et publication des tags v… ;
  4. sauvegarde PocketBase et attente du statut done ;
  5. backend, contrôle de X-Harmony-Revision et X-Harmony-Version ;
  6. web, même contrôle sur /healthz ;
  7. smoke tests backend et site public ;
  8. génération des notes GitHub catégorisées pour les deux dépôts et publication du changelog combiné ;
  9. événement system.release_published, dédupliqué par version et diffusé en temps réel au back-office.

Le code applicatif n’est pas recompilé pendant cette phase : Dockerfile.release ajoute uniquement HARMONY_RELEASE_VERSION et PUBLIC_HARMONY_RELEASE_VERSION au-dessus des images déjà exercées en staging. Le SHA reste donc la preuve de contenu, tandis que la version est lisible par les humains.

Les catégories du changelog proviennent de .github/release.yml, tandis que chaque entrée reprend le titre Conventional Commit de la PR. Étiquetez les PR avec feature ou enhancement, bug ou fix, security, documentation, dependencies ou maintenance. Le label skip-changelog exclut une PR. Relancer le workflow avec la même version met à jour les releases existantes et ne crée pas une seconde notification.

Les migrations sont compilées dans le binaire backend et s’exécutent au démarrage. Une release doit suivre expand → migrate → contract :

  1. ajouter des collections ou champs optionnels en conservant l’ancien contrat ;
  2. déployer le backend et effectuer le backfill idempotent ;
  3. déployer le web qui consomme le nouveau contrat ;
  4. retirer l’ancien schéma dans une release ultérieure seulement.

PocketBase utilise SQLite : maintenez une seule instance en écriture par volume et ne faites pas de rolling deployment avec deux versions écrivant simultanément dans la même base. make seed est strictement réservé aux données locales ou jetables.

Le même workflow peut redéployer une ancienne version en fournissant sa version et la paire exacte de tags sha-… déjà associée. Le garde-fou interdit de réaffecter une version existante à d’autres commits. Pour le web, cette opération est normalement sans état. Pour le backend :

  • sans migration, redéployez l’ancienne image ;
  • après une migration additive vérifiée compatible, l’ancienne image peut être envisagée ;
  • après une migration destructive ou de nouvelles écritures, préférez une correction en avant ;
  • en cas de corruption, restaurez le snapshot dans un volume isolé, validez-le, puis basculez.

Une sauvegarde réussie n’autorise pas un rollback aveugle. Notez le SHA, l’instant du snapshot et la dernière migration dans le compte rendu de release.

  • image publiée mais aucun déploiement : vérifier les variables de l’environnement GitHub ;
  • 401/403 Dokploy : renouveler le jeton API sans l’afficher dans les logs ;
  • timeout avant done : consulter le déploiement et les logs Dokploy portant le titre du run GitHub ;
  • révision inattendue : vérifier le routage du domaine et l’image réellement affectée à l’application ;
  • backup bloqué : interrompre la promotion et résoudre le stockage S3 avant de relancer ;
  • backend sain mais web en erreur : vérifier API_BASE_URL depuis le conteneur web.

Le script scripts/dokploy-deploy.sh, utilisé pour le staging et la promotion de production du backend puis du web, journalise la méthode, l’endpoint et le numéro de tentative de chaque appel Dokploy. En cas d’échec, il affiche le statut HTTP et le code de sortie de curl, sans exposer les corps de réponse ni les identifiants.

Seules les lectures de statut (GET deployment.all) sont réessayées après une erreur temporaire HTTP 408, 429 ou 5xx, ou une erreur de connexion. Elles sont limitées à trois tentatives, espacées de cinq secondes, avec un délai de connexion de dix secondes et une durée maximale de trente secondes par requête. Les erreurs permanentes, dont les refus d’authentification, arrêtent immédiatement le script.

Les POST de configuration et de déploiement ne sont jamais réessayés : une réponse en erreur peut suivre une opération déjà effectuée. Dans ce cas, consulter l’historique Dokploy et la révision servie avant de relancer le workflow. Les contrôles de résultat du déploiement, de révision et de version restent requis. Les tests de ce comportement utilisent uniquement des commandes simulées, via make release-script-test, sans contacter Dokploy.