Featured image of post Le pipeline qui publie ce blog sans toucher à la prod
Projet

Le pipeline qui publie ce blog sans toucher à la prod

Comment mon simple rsync est devenu un pipeline GitLab qui valide chaque article, construit Hugo et ne déploie qu’après une fusion sur main.

Au début, publier ce blog tenait dans une commande rsync 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.

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.

J’ai donc écrit un premier pipeline GitLab CI/CD avec deux étapes : construire le site, puis copier public/ sur dmz-hugo-01. Quarante lignes de YAML, une clé SSH et, pensais-je, une petite soirée tranquille.

Le pipeline a fonctionné. Après plusieurs petites leçons.

Warpgate avait un deux-points de trop pour rsync

Le serveur Hugo n’est pas directement accessible depuis le runner. Le déploiement passe par Warpgate, qui encode la cible SSH dans le nom d’utilisateur :

1
compte:serveur-cible

C’est parfaitement compréhensible pour OpenSSH. rsync, lui, voit le premier : et pense avoir trouvé la séparation entre un hôte et un chemin distant.

Ma première destination ressemblait à ceci :

1
compte:dmz-hugo-01@warpgate:/var/www/blog.homeblack.fr/

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 -l :

1
2
3
rsync -rlptDz --delete-delay \
  -e "ssh -i /tmp/hugo_deploy_key -p ${WARPGATE_PORT} -l ${DEPLOY_USER}" \
  public/ "${WARPGATE_HOST}:${DEPLOY_PATH}/"

La destination redevient un banal hôte:chemin, tandis que SSH reçoit toujours l’identité Warpgate complète.

J’utilisais initialement mon propre compte pour cette connexion. Le pipeline possède maintenant un compte dédié, gitlab-ci-hugo, limité à cette cible. C’est plus lisible dans les journaux et le déploiement ne dépend plus de mon identité personnelle.

La clé SSH n’était pas le problème que je croyais

Ensuite est arrivé le classique :

1
Load key "/tmp/hugo_deploy_key": error in libcrypto

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.

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 :

1
2
printf '%s' "$SSH_DEPLOY_KEY" | base64 -d > /tmp/hugo_deploy_key
chmod 600 /tmp/hugo_deploy_key

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.

Deux étapes ne suffisaient plus

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.

Le pipeline actuel possède trois étapes :

1
2
3
4
stages:
  - validate
  - build
  - deploy

La validation exécute les tests du dépôt et le validateur de contenu :

1
2
3
4
5
6
validate-content:
  stage: validate
  image: python:3.14-alpine
  script:
    - python3 -m unittest discover -s scripts -p "test_*.py"
    - python3 scripts/validate_content.py

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.

Le build utilise ensuite une version précise de Hugo, active --panicOnWarning et conserve public/ 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é.

Une branche construit, seule main déploie

Le changement le plus important n’est pas une commande. C’est la frontière entre vérifier et publier.

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 deploy possède sa propre règle : il ne s’exécute que sur la branche principale.

1
2
3
4
5
deploy:
  interruptible: false
  resource_group: production
  rules:
    - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"

resource_group 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 rsync déjà engagé. À l’inverse, les validations et builds ordinaires peuvent être annulés lorsqu’un commit plus récent les rend inutiles.

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.

Le dernier contrôle se fait sur le vrai site

Le transfert conserve les permissions utiles, retarde les suppressions avec --delete-delay et active StrictHostKeyChecking contre le fichier known_hosts préparé par le job.

Une fois rsync terminé, le pipeline interroge directement l’URL de production :

1
2
curl --fail --silent --show-error --retry 5 --retry-delay 2 \
  "https://blog.homeblack.fr/" > /dev/null

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.

Ce que cela a changé quand j’écris

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 main, le même pipeline publie l’artefact validé puis vérifie la page réelle.

Le système reste volontairement simple. rsync é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.

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.

Je pensais automatiser un rsync. J’ai surtout fini par définir ce que « prêt à publier » voulait dire.

Généré avec Hugo
Thème Stack conçu par Jimmy