Cette fiche complète la théorie

La fiche CI/CD explique ce qu'est un pipeline d'intégration continue, ses concepts, ses outils. Cette fiche-ci raconte une application concrète : la CI qui tourne sur ce site, comment elle est née, et pourquoi elle a la forme qu'elle a. Lis la théorie d'abord si les mots workflow, job, action ne te parlent pas encore.

1. Le contexte

DevIA Notes est un site statique : du HTML/CSS/JS pur, aucun build (pas de Hugo, pas de Jekyll, pas de bundler). Il est hébergé sur GitHub Pages, déployé automatiquement à chaque push sur main. Côté contenu, ~25 fiches qui se citent les unes les autres via la section « écosystème ».

Le risque principal :

  • Un lien interne cassé (typo dans href="mflow.html", fiche renommée pas mise à jour partout).
  • Un lien externe qui pourrit avec le temps (doc officielle déplacée, repo GitHub renommé).

Avant la CI, rien ne détectait ça : le déploiement partait en silence, et c'était à moi de cliquer dans le site pour vérifier. Pas viable quand on ajoute des fiches au rythme de plusieurs par jour.

2. Ce qu'on voulait

  • Vérifier les liens à chaque push.
  • Bloquer le déploiement si un lien est cassé.
  • Déployer automatiquement sinon.
  • Garder simple : pas de over-engineering pour un projet perso.

Outils choisis : GitHub Actions pour orchestrer, lychee (un crawler de liens écrit en Rust) pour la vérification, actions/deploy-pages pour la publication. Trois actions du marché, zéro code maison.

3. Première version (naïve)

Voici en gros ce que j'ai écrit au départ — un workflow minimal, deux jobs en série :

yaml (v1, simplifié)
jobs:
  link-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: lycheeverse/lychee-action@v2
        with:
          args: --no-progress --max-retries 2 --exclude-mail './**/*.html'
          fail: true

  deploy:
    needs: link-check
    # ... 3 étapes Pages standard

Sur le papier ça paraissait OK. En pratique, ça a planté 5 fois d'affilée avant de stabiliser. Chaque échec a apporté une leçon — les voici dans l'ordre.

4. Les leçons (par ordre d'apparition)

Leçon 1 — @v2 n'est pas une version, c'est un alias mouvant

Symptôme : error: unexpected argument '--exclude-mail' found.

Cause : entre lychee 0.22 et 0.23, le flag --exclude-mail a été supprimé (les mailto: sont désormais exclus par défaut). L'action lycheeverse/lychee-action@v2 télécharge la dernière version compatible v2 du binaire — donc la 0.23 est arrivée sans prévenir et a cassé mon flag.

Décision : retirer le flag (c'est devenu le défaut). Pour un projet perso, on accepte cette dérive et on patche quand ça casse. En prod, on épinglerait au SHA du commit (@a1b2c3d) pour figer le comportement exactement.

Leçon 2 — Les localhost dans les fiches sont des faux positifs structurels

Symptôme : http://localhost:8000/ — Connection refused sur docker.html et fastapi.html.

Cause : les fiches contiennent des <a href="http://localhost:8000/docs"> à but pédagogique. Lychee tente de les contacter ; rien ne tourne sur le runner GitHub.

Décision : ajouter le flag --exclude-loopback (couvre localhost, 127.0.0.1, ::1, tous ports). Une règle générique vaut mieux qu'une liste d'URLs : voir Leçon 5.

Leçon 3 — Les CDN modernes bloquent les runners GitHub

Symptômes (à 4 reprises) :

  • https://try.redis.io — Connection failed
  • https://helm.sh — Connection reset by peer
  • https://demo.uptime.kuma.pet — Timeout
  • https://streamlit.io — Connection failed

Cause : Cloudflare, Fastly et consorts filtrent les plages d'IPs cloud (les runners GitHub partagent leurs IPs avec des milliers d'autres acteurs, dont du trafic abusif). Les liens sont valides — ils marchent dans n'importe quel navigateur.

Constat : ce n'est pas mon problème, je ne peux rien y faire, et ça va se reproduire à chaque nouveau lien externe vers une grosse marque tech.

Leçon 4 — Distinguer ce qu'on contrôle (le pattern fail-loud / fail-soft)

Le déclic : tant qu'un seul job vérifie tout, il échoue pour des raisons légitimes et pour des raisons hors de mon contrôle, sans distinction. Le déploiement est bloqué pour des liens qui ne sont pas cassés. C'est inacceptable.

Solution : couper en deux jobs.

  • Liens internes (lychee --offline) — sous mon contrôle. Une erreur ici est un VRAI bug. → bloque le déploiement.
  • Liens externes ( continue-on-error: true) — dépend du monde. → ne bloque rien, juste informatif.

C'est le pattern fail-loud / fail-soft : on hurle fort sur ce qu'on contrôle, on chuchote sur ce qu'on ne contrôle pas. Universel, pas spécifique à lychee.

Leçon 5 — Une règle générique vaut mieux qu'une liste d'exceptions

Pour les localhost, deux approches étaient possibles :

  • Lister chaque URL dans .lycheeignore : ^http://localhost:8000/$, ^http://localhost:8000/docs$… À chaque nouvelle fiche citant localhost, ajouter une ligne. Pénible et non-exhaustif.
  • Exprimer une règle : --exclude-loopback. Un flag, toutes les variantes (n'importe quel port, n'importe quel chemin), couverture automatique des cas futurs.

À chaque fois que tu peux caractériser une famille de faux positifs par une règle, fais-le. La liste reste utile pour les cas vraiment particuliers (par ex. une URL publique précise qui flanche depuis le CI).

Leçon 6 — La règle anti « cry-wolf »

Le job externe est best-effort : il peut être rouge sans bloquer le déploiement. Tentation : ne plus s'en occuper du tout.

Mauvaise idée — sans entretien, il sera rouge tout le temps, et donc personne ne lira jamais le rapport. Du coup, le jour où un vrai lien meurt (doc officielle déplacée, repo renommé), ça passe inaperçu. C'est le berger qui crie au loup : trop d'alertes → on ignore les vraies.

D'où .lycheeignore : on y met les URLs durablement flaky pour que le job externe soit vert la plupart du temps. Il devient un signal utile : rouge = quelque chose à regarder.

Leçon 7 — Le CI s'itère

5 commits successifs juste pour stabiliser le link-check. Sur le moment ça frustre ; avec du recul, c'est normal. Un pipeline n'est pas une config qu'on écrit une fois et qu'on oublie : il vit avec le projet. Chaque nouvelle dépendance, chaque nouvelle plateforme apporte ses faux positifs. L'objectif n'est pas d'écrire le bon workflow du premier coup, c'est de le faire évoluer vite quand il déconne.

5. Le résultat final

Le workflow stabilisé tient en trois jobs :

schéma final
┌──────────────────────────┐
│ link-check-internal      │ STRICT (offline, zéro réseau)
│ → liens fiche→fiche, CSS │
└────────────┬─────────────┘
             │ bloque si rouge
             ▼
┌──────────────────────────┐
│ deploy                   │ Publie sur GitHub Pages
│ → uniquement push/main   │
└──────────────────────────┘

┌──────────────────────────┐
│ link-check-external      │ INFORMATIF (best-effort)
│ → URLs externes          │ continue-on-error: true
└──────────────────────────┘ N'INFLUE PAS sur le déploiement

Deux fichiers sont entièrement commentés à fin pédagogique dans le repo (lis-les en parallèle de cette fiche, c'est l'intérêt) :

  • .github/workflows/ci.yml — le workflow lui-même. Chaque section a son commentaire expliquant le pourquoi plutôt que le quoi.
  • .lycheeignore — la liste des URLs ignorées, avec l'explication du format regex et la règle de décision (« quand ajouter une URL ici, quand corriger la fiche à la place »).

Tu peux les ouvrir sur GitHub : ci.yml et .lycheeignore.

Liens vers les fiches concernées

  • CI/CD — la théorie générale (concepts, outils, vocabulaire). À lire avant ce retex si tu débutes.
  • GitHub — l'hébergement et les Actions.
  • Mettre un projet sur GitHub — l'étape précédente : avant la CI, il faut un repo et un déploiement Pages qui marche.