<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hugo on Mook’s Lab Chronicles</title><link>https://blog.homeblack.fr/tags/hugo/</link><description>Recent content in Hugo on Mook’s Lab Chronicles</description><generator>Hugo -- gohugo.io</generator><language>fr-FR</language><lastBuildDate>Sat, 15 Aug 2026 23:30:00 +0200</lastBuildDate><atom:link href="https://blog.homeblack.fr/tags/hugo/index.xml" rel="self" type="application/rss+xml"/><item><title>Le pipeline qui publie ce blog sans toucher à la prod</title><link>https://blog.homeblack.fr/p/deploiement-automatique-hugo-gitlab-cicd-warpgate/</link><pubDate>Sat, 25 Apr 2026 23:00:00 +0200</pubDate><guid>https://blog.homeblack.fr/p/deploiement-automatique-hugo-gitlab-cicd-warpgate/</guid><description>&lt;img src="https://blog.homeblack.fr/p/deploiement-automatique-hugo-gitlab-cicd-warpgate/cover.png" alt="Featured image of post Le pipeline qui publie ce blog sans toucher à la prod" /&gt;&lt;p&gt;Au début, publier ce blog tenait dans une commande &lt;code&gt;rsync&lt;/code&gt; lancée depuis mon Mac. Ce n’était pas compliqué. C’était même suffisamment simple pour que je repousse l’opération, puis que j’oublie quel article était réellement en ligne.&lt;/p&gt;
&lt;p&gt;Le dépôt avançait, le serveur attendait et la production finissait parfois avec un temps de retard sur ce que j’avais sous les yeux.&lt;/p&gt;
&lt;p&gt;J’ai donc écrit un premier pipeline GitLab CI/CD avec deux étapes : construire le site, puis copier &lt;code&gt;public/&lt;/code&gt; sur &lt;code&gt;dmz-hugo-01&lt;/code&gt;. Quarante lignes de YAML, une clé SSH et, pensais-je, une petite soirée tranquille.&lt;/p&gt;
&lt;p&gt;Le pipeline a fonctionné. Après plusieurs petites leçons.&lt;/p&gt;
&lt;h2 id="warpgate-avait-un-deux-points-de-trop-pour-rsync"&gt;Warpgate avait un deux-points de trop pour rsync
&lt;/h2&gt;&lt;p&gt;Le serveur Hugo n’est pas directement accessible depuis le runner. Le déploiement passe par &lt;a class="link" href="https://github.com/warp-tech/warpgate" target="_blank" rel="noopener"
&gt;Warpgate&lt;/a&gt;, qui encode la cible SSH dans le nom d’utilisateur :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;compte:serveur-cible
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;C’est parfaitement compréhensible pour OpenSSH. &lt;code&gt;rsync&lt;/code&gt;, lui, voit le premier &lt;code&gt;:&lt;/code&gt; et pense avoir trouvé la séparation entre un hôte et un chemin distant.&lt;/p&gt;
&lt;p&gt;Ma première destination ressemblait à ceci :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;compte:dmz-hugo-01@warpgate:/var/www/blog.homeblack.fr/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Le résultat était surtout une erreur difficile à relier au véritable problème. La solution a été de sortir le nom d’utilisateur de la destination et de le passer explicitement à SSH avec &lt;code&gt;-l&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -rlptDz --delete-delay &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -e &lt;span class="s2"&gt;&amp;#34;ssh -i /tmp/hugo_deploy_key -p &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WARPGATE_PORT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -l &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_USER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; public/ &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WARPGATE_HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEPLOY_PATH&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;La destination redevient un banal &lt;code&gt;hôte:chemin&lt;/code&gt;, tandis que SSH reçoit toujours l’identité Warpgate complète.&lt;/p&gt;
&lt;p&gt;J’utilisais initialement mon propre compte pour cette connexion. Le pipeline possède maintenant un compte dédié, &lt;code&gt;gitlab-ci-hugo&lt;/code&gt;, limité à cette cible. C’est plus lisible dans les journaux et le déploiement ne dépend plus de mon identité personnelle.&lt;/p&gt;
&lt;h2 id="la-clé-ssh-nétait-pas-le-problème-que-je-croyais"&gt;La clé SSH n’était pas le problème que je croyais
&lt;/h2&gt;&lt;p&gt;Ensuite est arrivé le classique :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Load key &amp;#34;/tmp/hugo_deploy_key&amp;#34;: error in libcrypto
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;J’ai d’abord accusé le format de la clé. L’historique du dépôt raconte une histoire moins élégante : variable GitLab de type fichier, retours chariot, essais de copie, différences entre Alpine et Debian… J’ai corrigé plusieurs symptômes avant de stabiliser le chemin complet.&lt;/p&gt;
&lt;p&gt;Le job de déploiement utilise désormais Debian Bookworm et transporte la clé sous forme encodée en base64. Elle est décodée dans un fichier temporaire avec des permissions strictes :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;%s&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$SSH_DEPLOY_KEY&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; base64 -d &amp;gt; /tmp/hugo_deploy_key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; /tmp/hugo_deploy_key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ce n’est pas un mécanisme de chiffrement : la confidentialité dépend toujours de la variable CI stockée dans GitLab. Le base64 évite simplement qu’un copier-coller ou un retour à la ligne transforme la clé avant qu’OpenSSH puisse la lire.&lt;/p&gt;
&lt;h2 id="deux-étapes-ne-suffisaient-plus"&gt;Deux étapes ne suffisaient plus
&lt;/h2&gt;&lt;p&gt;Le premier pipeline savait construire et déployer. Il ne savait pas dire qu’un article avait une description trop longue, une couverture absente ou un lien local cassé. Il pouvait donc automatiser très efficacement la publication d’une erreur.&lt;/p&gt;
&lt;p&gt;Le pipeline actuel possède trois étapes :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;stages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;validate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;deploy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;La validation exécute les tests du dépôt et le validateur de contenu :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;validate-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;validate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;python:3.14-alpine&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;python3 -m unittest discover -s scripts -p &amp;#34;test_*.py&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;python3 scripts/validate_content.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ce contrôle vérifie notamment le front matter, les titres SEO, les descriptions, les couvertures, les liens locaux et quelques motifs sensibles. Il impose aussi le format éditorial choisi pour chaque article : carnet de maintenance, retour d’expérience ou projet.&lt;/p&gt;
&lt;p&gt;Le build utilise ensuite une version précise de Hugo, active &lt;code&gt;--panicOnWarning&lt;/code&gt; et conserve &lt;code&gt;public/&lt;/code&gt; comme artefact pendant deux heures. Le job de déploiement récupère cet artefact : il ne reconstruit pas discrètement un site différent de celui qui vient d’être validé.&lt;/p&gt;
&lt;h2 id="une-branche-construit-seule-main-déploie"&gt;Une branche construit, seule main déploie
&lt;/h2&gt;&lt;p&gt;Le changement le plus important n’est pas une commande. C’est la frontière entre vérifier et publier.&lt;/p&gt;
&lt;p&gt;Une branche ou une merge request lance la validation et le build. Cela suffit pour savoir que l’article est publiable, mais aucun fichier n’est envoyé au serveur. Le job &lt;code&gt;deploy&lt;/code&gt; possède sa propre règle : il ne s’exécute que sur la branche principale.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;interruptible&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;resource_group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;production&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;resource_group&lt;/code&gt; empêche deux déploiements de production de se marcher dessus. Le job n’est pas interruptible afin qu’un nouveau commit ne coupe pas un &lt;code&gt;rsync&lt;/code&gt; déjà engagé. À l’inverse, les validations et builds ordinaires peuvent être annulés lorsqu’un commit plus récent les rend inutiles.&lt;/p&gt;
&lt;p&gt;Les règles globales évitent également de créer deux pipelines identiques lorsqu’une branche possède déjà une merge request ouverte. Ce sont de petits garde-fous, mais ils ont fait passer la CI d’un script distant à un véritable chemin de publication.&lt;/p&gt;
&lt;h2 id="le-dernier-contrôle-se-fait-sur-le-vrai-site"&gt;Le dernier contrôle se fait sur le vrai site
&lt;/h2&gt;&lt;p&gt;Le transfert conserve les permissions utiles, retarde les suppressions avec &lt;code&gt;--delete-delay&lt;/code&gt; et active &lt;code&gt;StrictHostKeyChecking&lt;/code&gt; contre le fichier &lt;code&gt;known_hosts&lt;/code&gt; préparé par le job.&lt;/p&gt;
&lt;p&gt;Une fois &lt;code&gt;rsync&lt;/code&gt; terminé, le pipeline interroge directement l’URL de production :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --fail --silent --show-error --retry &lt;span class="m"&gt;5&lt;/span&gt; --retry-delay &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://blog.homeblack.fr/&amp;#34;&lt;/span&gt; &amp;gt; /dev/null
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Ce test ne prouve pas que chaque paragraphe est magnifique. Il confirme au moins que le chemin GitLab → artefact Hugo → Warpgate → Nginx aboutit encore à un site qui répond.&lt;/p&gt;
&lt;h2 id="ce-que-cela-a-changé-quand-jécris"&gt;Ce que cela a changé quand j’écris
&lt;/h2&gt;&lt;p&gt;Aujourd’hui, je ne « déploie » plus le blog depuis mon poste. Je pousse une branche, la CI contrôle le contenu et construit le site. Quand la modification rejoint &lt;code&gt;main&lt;/code&gt;, le même pipeline publie l’artefact validé puis vérifie la page réelle.&lt;/p&gt;
&lt;p&gt;Le système reste volontairement simple. &lt;code&gt;rsync&lt;/code&gt; écrit directement dans le répertoire servi par Nginx : je n’ai ni répertoire de releases ni bascule atomique vers une version précédente. Si un article doit être retiré, le rollback passe encore par Git et un nouveau pipeline.&lt;/p&gt;
&lt;p&gt;Pour un blog personnel, ce compromis me convient. Le point important n’était pas d’empiler les outils, mais d’enlever une commande manuelle tout en gardant une limite nette : tester partout, déployer seulement après la fusion.&lt;/p&gt;
&lt;p&gt;Je pensais automatiser un &lt;code&gt;rsync&lt;/code&gt;. J’ai surtout fini par définir ce que « prêt à publier » voulait dire.&lt;/p&gt;</description></item></channel></rss>