Cette fiche complète la théorie

La fiche CI/CD et le retex CI/CD du site couvrent comment on a un workflow GitHub Actions qui déploie ce site. Cette fiche-ci raconte une extension de ce workflow : faire faire au CI autre chose que vérifier les liens, en l'occurrence modifier le HTML avant la publication.

1. Le besoin

Sur la page À propos, je voulais afficher « Dernière mise à jour le … » avec la date du dernier push, pour qu'un visiteur sache si le site est actif ou abandonné depuis 6 mois.

Deux contraintes :

  • Le site est statique (HTML/CSS/JS pur). Pas de backend qui pourrait calculer la date à la volée.
  • La date doit être automatique. Hors de question d'éditer la valeur à la main à chaque commit.

2. Première version — fetch côté client

Réflexe naturel : appeler l'API GitHub depuis le navigateur du visiteur.

html (v1)
Dernière mise à jour le <span id="last-update">—</span>

<script>
  fetch("https://api.github.com/repos/os974/devianotes/commits?per_page=1")
    .then(r => r.json())
    .then(data => {
      const d = new Date(data[0].commit.committer.date);
      document.getElementById("last-update").textContent =
        d.toLocaleDateString("fr-FR", { day: "2-digit", month: "long", year: "numeric" });
    });
</script>

Ça marche du premier coup. Mais en y regardant de plus près, trois petites gênes accumulées :

  • Un appel réseau à chaque visite vers api.github.com. Anonyme, donc soumis à un quota (60 req/h par IP). Pour un site à 10 visites/jour aucun souci, mais c'est de la dette latente.
  • CSP à élargir : il faut ajouter connect-src 'self' https://api.github.com au Content-Security-Policy. On ouvre une porte juste pour afficher une date.
  • Désynchronisation possible : si l'API renvoie un commit plus récent que le HTML déployé (ex. tu pushes deux fois et regardes pendant le second build), la page affiche une date qui ne correspond pas à ce qui est servi. Subtil, mais incohérent.

3. Deuxième version — injection au build

Le déclic : la date est connue au moment du déploiement. Pas besoin de la calculer chez le visiteur, autant la figer dans le HTML avant publication.

La page contient un placeholder, le workflow le remplace au moment du déploiement.

Côté HTML — un placeholder

html (about.html, v2)
Dernière mise à jour le <span id="last-update">__LAST_UPDATE__</span>

__LAST_UPDATE__ est une chaîne improbable choisie pour ne risquer aucun faux positif lors du remplacement. Le double underscore signale visuellement que c'est un marker, pas du contenu.

Côté CI — un step qui remplace

yaml (.github/workflows/ci.yml)
- name: Injecter la date de dernier push dans about.html
  run: |
    months=(janvier février mars avril mai juin juillet août septembre octobre novembre décembre)
    day=$(git log -1 --format=%cd --date=format:%-d)
    month_idx=$(git log -1 --format=%cd --date=format:%-m)
    year=$(git log -1 --format=%cd --date=format:%Y)
    date_fr="${day} ${months[$((month_idx-1))]} ${year}"
    sed -i "s|__LAST_UPDATE__|${date_fr}|g" about.html
    # garde-fou : si le placeholder n'a pas été remplacé, on échoue
    if grep -q "__LAST_UPDATE__" about.html; then
      echo "::error::Placeholder __LAST_UPDATE__ encore présent"
      exit 1
    fi

Ce step est inséré entre actions/checkout@v4 et actions/upload-pages-artifact@v3. Il modifie la copie de about.html dans la VM du runner — la modif n'est jamais commitée, elle vit le temps du job, exactement comme un build classique produit un dist/ jetable.

4. Les leçons

Leçon 1 — « Au build » bat « au runtime » dès qu'une donnée est figée à la publication

La date d'un déploiement ne change pas après coup. La calculer chez le visiteur, c'est faire payer à 100 % des visites un calcul qui aurait pu être fait une seule fois par le CI.

Règle générale : si une donnée est connue au moment où tu construis le site, fige-la dans le HTML servi. Tu n'utilises du JS côté client que pour ce qui dépend du visiteur (interactions, état local, données vraiment temps réel).

Leçon 2 — Une CSP plus stricte est gratuite

En passant de v1 à v2, j'ai pu retirer connect-src https://api.github.com du Content-Security-Policy. Le navigateur ne fait plus aucune requête sortante depuis cette page. Plus la CSP est étroite, plus une éventuelle XSS injectée a de mal à exfiltrer des données.

Penser réseau chaque fois qu'on écrit du JS : « cette feature a-t-elle vraiment besoin de causer à un serveur ? ». Souvent non, et on s'épargne une autorisation CSP.

Leçon 3 — Sur un runner Ubuntu, ne suppose pas que la locale française est installée

Pour formater « 7 mai 2026 », j'ai d'abord pensé à LC_TIME=fr_FR.UTF-8 date "+%-d %B %Y". Mais ubuntu-latest n'a que en_US et C.UTF-8 installées par défaut. Soit on lance locale-gen fr_FR.UTF-8 (~5 secondes ajoutées au job), soit on tape la liste des mois dans un tableau bash.

Pour 12 strings figées qui ne changeront jamais, le tableau gagne : zéro dépendance, zéro temps installation, lisible en deux lignes. L'approche « install locale » a sa place dès qu'on en a besoin ailleurs — pas pour un seul format de date.

Leçon 4 — Un sed silencieux est un piège ; mets toujours un garde-fou

sed -i "s|...|...|g" ne renvoie pas d'erreur si le motif est absent : il ne remplace simplement rien. Si demain je renomme __LAST_UPDATE__ dans le HTML sans toucher au workflow, le CI passera vert et la page sera publiée avec le motif d'origine encore visible. Bug silencieux.

D'où le grep -q "__LAST_UPDATE__" && exit 1 après le sed : si le placeholder est encore là après remplacement, c'est que le contrat HTML/CI est cassé, on échoue fort avec un message clair.

Pattern réutilisable : après chaque transformation automatique, vérifier que le résultat correspond à ce qu'on attendait. C'est l'équivalent CI d'un test unitaire — sauf qu'on teste la sortie de l'outil, pas du code applicatif.

Leçon 5 — Le trade-off accepté : la version locale est moche

Si tu ouvres about.html en local par double-clic (sans passer par GitHub Pages), tu vois littéralement « Dernière mise à jour le __LAST_UPDATE__ ». Pas joli.

On pourrait ajouter un fallback JS qui détecte le placeholder et affiche « (version locale) » à la place. Mais ça rajoute du code pour cacher un problème qui ne se produit que dans un cas marginal (dev local, page À propos). À ce stade, accepter le placeholder visible est le bon choix : le coût (un mot moche pendant qu'on développe) est inférieur au bénéfice du code plus simple.

Règle générale : ne complexifie pas le code pour des cas que personne ne voit en condition réelle. Documente le trade-off (commentaire HTML à côté du placeholder) et passe à la suite.

5. Quand utiliser ce pattern

L'injection au build est utile dès que tu as besoin d'afficher dans une page statique une donnée qui :

  • est connue au moment du déploiement (date, numéro de version, hash du commit, environnement, nom du build) ;
  • ne change pas entre deux déploiements ;
  • n'a pas besoin d'être différente d'un visiteur à l'autre.

Cas typiques : badge de version (v1.4.2 dans un footer), SHA du commit pour debug (build: a1b2c3d), date de génération, banner d'environnement (« staging »). Dès que la donnée dépend du visiteur ou du temps présent (heure courante, données live), on revient à du JS côté client ou un appel API.

Liens vers les fiches concernées