Skip to content

Le pipeline qui reconstruisait tout (périmètre de build et filtrage)

Corriger une phrase de documentation déclenchait 17 jobs, poussait une image dans le registre et faisait avancer l'environnement de dev. Cette page explique pourquoi, ce que les corrections du 2026-08-16 changent, et le piège qu'elles ont failli créer.

En trois phrases

Le pipeline ne savait pas distinguer un changement de code d'un changement de documentation : il reconstruisait tout, tout le temps. Une MR de documentation coûtait 17 jobs et environ 8 minutes de runner, dont 2 jobs justifiés.

La cause profonde n'était pas le pipeline mais la structure du dépôt : le backend vivait à la racine, donc son contexte de build était le dépôt entier. Le corriger demandait d'abord de déplacer le backend sous backend/.

Une fois chaque workload dans son dossier, la règle de filtrage tient en trois lignes au lieu d'une liste de chemins à ne jamais oublier. C'est l'idée centrale de cette page : une structure remplace une liste.

Les mots du sujet

À lire une fois, tout le reste s'appuie dessus.

Terme Ce que c'est
job Une tâche du pipeline : lancer les tests, construire une image, publier la doc. Un pipeline est une collection de jobs.
workload Une application déployée séparément. Ce projet en a deux : l'API FastAPI (le backend) et la SPA React (le frontend).
contexte de build L'ensemble des fichiers envoyés au constructeur d'image. Tout ce qui est dans le contexte peut finir dans l'image.
.dockerignore La liste des fichiers à exclure du contexte. Une liste : elle grandit avec le dépôt et ne dit rien quand elle est incomplète.
kaniko L'outil qui construit les images dans la CI, sans démon Docker. C'est lui qui reçoit le --context.
rules: / changes: Les mots-clés GitLab qui décident si un job se lance. changes: déclenche selon les fichiers modifiés.
needs: Déclare qu'un job dépend d'un autre. Il permet de démarrer plus tôt, mais crée un lien dur entre les jobs.
write-back Le job qui, après un build, réécrit dans Git le tag d'image de l'overlay de dev. C'est ce qui fait avancer dev tout seul.
overlay Le dossier de configuration propre à un environnement (k8s/overlays/fastapi/prod/), où est écrit le tag que cet env doit servir.
pipeline de MR Le pipeline qui valide une merge request avant le merge. À distinguer du pipeline qui tourne après, sur develop.

Le problème : un pipeline aveugle au type de changement

Mesuré le 2026-08-16 sur !304, une MR de documentation pure — 5 fichiers, tous sous docs/ :

Ce qui tournait Durée Justifié ?
semgrep-sast 128 s non
build-candidate 46 s non
run-tests 38 s non
trivy-fs-scan, trivy-image-scan, scans front ~86 s non
build_docs 23 s oui
pages 15 s oui
les 9 autres (promote, write-back, lint, fmt…) ~115 s non

2 jobs sur 17. Et le write-back faisait avancer dev d'une image pour un changement de documentation.

AVANT docs/page.md 17 jobs, ~8 min image poussée dev avance une phrase corrigée dont 2 justifiés dans le registre ECR pour rien APRÈS docs/page.md 1 job, 12 s aucune image, dev ne bouge pas le même changement pipeline-scope la doc est publiée au merge, comme avant
Le même changement, avant et après. Ce n'est pas une optimisation de confort : chaque build inutile poussait une image de plus dans la fenêtre de rétention du registre, ce qui rapproche l'incident INC-062.

Pourquoi il fallait déplacer le backend d'abord

La correction évidente serait d'écrire « ne construis que si le code applicatif a changé ». Mais où vit le code applicatif ?

Avant le 2026-08-16, le backend occupait la racine du dépôt : app/, alembic/, tests/, requirements*.txt, Dockerfile, tous mélangés avec terraform/, k8s/, docs/ et ansible/. Le frontend, lui, vivait déjà dans frontend/ avec son propre Dockerfile.

La règle aurait donc dû énumérer : app/**, alembic/**, requirements*, Dockerfile, tests/**. Une liste à ne pas oublier.

Une liste oublie, et ne le dit pas

Le .dockerignore était exactement cette liste-là, côté exclusions. Elle avait déjà laissé passer .claude/ dans l'image de production, sans que rien ne le signale : le pipeline restait vert, l'image grossissait d'un dossier que personne n'avait vu passer.

Le défaut n'est pas qu'on ait mal écrit la liste. C'est qu'une liste énumère ce qu'on ne veut pas, donc elle doit être tenue à jour à chaque ajout à la racine.

Le déplacement du backend sous backend/ (#161, ADR 030) renverse la logique. Un contexte de build restreint énumère ce qu'on veut. Ajouter un dossier à la racine du dépôt n'a alors plus aucun effet sur l'image, sans qu'aucune liste n'ait à être maintenue.

La propriété recherchée n'est pas « la liste est complète aujourd'hui » mais « la question ne se pose plus ».

AVANT · CONTEXTE = RACINE app/ alembic/ tests/ Dockerfile terraform/ k8s/ docs/ ansible/ scripts/ .claude/ mkdocs.yml … tout entre dans l'image, sauf ce que le .dockerignore pense à exclure APRÈS · CONTEXTE = LE WORKLOAD backend/ frontend/ app/ alembic/ tests/ Dockerfile src/ public/ Dockerfile le reste du dépôt ne peut plus entrer, par construction LA RÈGLE DE FILTRAGE, EN CONSÉQUENCE app/** alembic/** requirements* Dockerfile tests/** ← à maintenir backend/** frontend/** ← rien à maintenir
La structure porte la règle. À gauche, cinq entrées qu'il faut penser à mettre à jour. À droite, deux entrées qui restent vraies quoi qu'on ajoute au dépôt.

Ce qui déclenche quoi, désormais

Job(s) Ne tourne que si ces chemins changent
build-*, trivy-*-image-scan, promote-*, tag-inuse-images, update-image-tag backend/, frontend/, .gitlab-ci.yml
run-tests backend/, .gitlab-ci.yml (et toujours sur main)
tfsec-scan, terraform-fmt terraform/, .gitlab-ci.yml
kube-linter-scan k8s/, .gitlab-ci.yml
trivy-fs-scan backend/, frontend/, .trivyignore, .vex/, .gitlab-ci.yml
sast, secret_detection, dependency_scanning aucun filtre possible — voir plus bas
build_docs, pages aucun filtre — ils tournent à chaque push sur develop

.gitlab-ci.yml figure partout, et c'est volontaire : modifier la chaîne sans la rejouer ne la teste pas.

Pourquoi les huit jobs de la chaîne image partagent un seul déclencheur

C'est le point le moins intuitif de cette refonte.

On voudrait faire plus fin : si seul le backend change, ne reconstruire que le backend. C'est un piège, et il vient des needs:.

CHAÎNÉS PAR needs: build-candidate trivy-image-scan promote-image build-frontend trivy-front-scan promote-frontend tag-inuse-images update-image-tag ces deux jobs dépendent des DEUX promote
Le lien dur est à droite. update-image-tag et tag-inuse-images déclarent needs: sur les deux promote. Filtrer les deux chaînes séparément casse ce lien.

Un needs: vers un job absent du pipeline n'est pas un job sauté. C'est une erreur de création : le pipeline n'existe pas du tout.

GitLab propose bien un contournement, needs: optional: true, qui laisse le job tourner même si sa dépendance manque. Ce serait pire. Le write-back écrirait dans Git le tag deployed-<sha> d'une image qui n'a jamais été promue, et ArgoCD exigerait du cluster une image inexistante — c'est-à-dire INC-062 reproduit volontairement.

Les huit jobs partagent donc le même déclencheur. Le coût est réel et assumé : un changement qui ne touche que le frontend reconstruit aussi le backend.

Le piège : un pipeline vide n'est pas neutre

Une fois tout filtré, une MR qui ne touche que docs/ n'a plus aucun job à lancer. On pourrait croire que GitLab s'en accommode. Non.

Zéro job produit un pipeline failed, sans erreur YAML

Ce projet en avait déjà la preuve sous les yeux : le schedule de teardown de 21 h échouait chaque soir depuis le 2026-08-06 pour cette raison exacte — aucune règle ne correspondait, donc zéro job, donc failed, et aucun message d'erreur pour le dire (pipeline 2762923575).

Or le projet exige un pipeline vert pour merger (only_allow_merge_if_pipeline_succeeds = true). Le réglage allow_merge_on_skipped_pipeline ne rattrape que le statut skipped, pas failed.

Toute MR de documentation serait devenue impossible à merger.

D'où le job pipeline-scope : aucune condition de chemin, il tourne toujours, il coûte 12 secondes. Son rôle premier est d'exister, pour qu'un pipeline ne soit jamais vide.

Son rôle second est d'être lisible. Il affiche les fichiers détectés, seule trace disponible quand la chaîne image ne tourne pas :

Perimetre du pipeline (#157). Base de comparaison : 11bf9767
--- fichiers modifies ---
docs/local-development.md
-------------------------
La chaine image ne tourne que si backend/, frontend/ ou .gitlab-ci.yml a change.

Sans lui, un pipeline à un seul job ne dirait pas si GitLab a vu le changement ou l'a raté. C'est précisément le contrôle qui manquait.

La documentation continue d'être publiée

C'est la question qui vient naturellement : si le pipeline ne fait plus rien sur une MR de doc, qui met à jour le site MkDocs ?

Il faut distinguer deux pipelines, à deux moments.

MOMENT 1 · LA MR EST OUVERTE pipeline de MR 1 job : pipeline-scope valider avant de merger aucune publication, avant comme après MOMENT 2 · APRÈS LE MERGE SUR DEVELOP pipeline develop build_docs + pages doc.devopsyouss.com à jour non filtré gen_dashboard, gen_status, mkdocs dashboard des incidents et radiateur compris
La publication a toujours eu lieu au merge, jamais sur la MR. Le filtrage n'y a rien changé : build_docs et pages vivent dans .gitlab-ci-docs.yml et ne portent aucune règle de chemin.

Il n'y a donc pas de prévisualisation MkDocs par MR, et il n'y en a jamais eu. C'est un choix antérieur à cette refonte.

Le write-back ordonne avant d'écrire

Le contexte, avant le détail

Le write-back est le job update-image-tag : après le build et la promotion, il écrit le nouveau tag d'image dans les kustomizations de l'overlay dev et pousse un commit sur develop. ArgoCD lit ce commit et déploie.

Jusqu'au 2026-09-07, il commitait sur un HEAD détaché et poussait sans jamais se resynchroniser. N'importe quel commit atterri sur develop entre le checkout du pipeline et ce push le cassait en non-fast-forward. Un merge de documentation suffisait.

La règle qui en découlait, « aucun commit sur develop tant qu'un write-back est en vol », n'était tenue par rien : elle reposait sur le fait qu'on y pense.

Les deux corrections qui semblent évidentes, et pourquoi elles sont fausses

Un fetch + rebase. C'est la première idée, et c'est la pire. Deux pipelines qui se croisent :

  10:00  pipeline A démarre sur le commit A  ->  image deployed-A
  10:02  pipeline B démarre sur le commit B  ->  image deployed-B
  10:05  write-back de B pousse deployed-B
  10:07  write-back de A s'exécute, en retard
Comportement Résultat sur develop Visible ?
L'échec d'avant reste deployed-B, la bonne oui, job rouge
Avec rebase repasse à deployed-A, la mauvaise non, tout est vert

L'échec était bruyant, mais il n'a jamais déployé la mauvaise image.

« Si mon commit n'est plus la pointe, s'abstenir ». Plus subtil, et faux aussi. Ce critère ne discrimine pas : la pointe bouge aussi quand un commit sans rapport atterrit. S'abstenir dans ce cas saute le déploiement d'une image qui vient d'être construite, scannée et promue, en sortant vert. On remplacerait un rouge visible par un vert faux.

Ce que fait scripts/gitops-write-back.sh

Il ordonne. Le tag vaut deployed-<SHA complet>, donc il désigne un commit précis.

  le commit de ce pipeline est-il un ancêtre de celui déjà taggé ?
        │
        ├── oui  ->  un pipeline plus récent est passé
        │            abstention, sortie en SUCCÈS
        │
        └── non  ->  ce pipeline est le plus récent
                     repartir de la pointe de develop,
                     RÉAPPLIQUER l'édition, pousser
                     (jusqu'à trois tentatives)

La dernière ligne est ce qui remplace le rebase : on ne rejoue pas un commit préparé sur un état périmé, on recalcule l'édition sur l'état courant. Rien ne peut reculer, puisque l'ordre a été établi avant d'écrire.

⚠️ Le clone d'un job GitLab est shallow. Sans approfondir l'historique, le commit désigné par le tag en place peut être absent du graphe local, et l'ancêtre devient incalculable. Le script approfondit, et s'il n'y arrive pas il échoue bruyamment plutôt que de deviner : quand l'ordre est indécidable, un vert faux coûte plus cher qu'un rouge.

Ce que cela change au quotidien

La règle « aucun commit sur develop pendant un write-back » disparaît. Un merge de documentation pendant qu'une chaîne image tourne est désormais absorbé.


Ce que ces corrections ne font pas

Elles ne stabilisent pas le digest de l'image. Mesuré le 2026-08-11 : deux builds sans le moindre changement produisent deux empreintes différentes. Sur 10 couches, les 4 de l'image de base sont stables et les 6 couches construites dérivent spontanémentapt-get --only-upgrade, reconstruction du venv, suppression des caches pip.

C'est pour cette raison que nettoyer le contexte de build ne suffisait pas : deux commits ne différant que par un manifeste produisaient quand même une image différente. Seul le fait de ne pas construire du tout règle le problème.

Elles n'atteignent pas la cible de 40 secondes annoncée dans #157. semgrep-sast (125 s) et secret_detection sont des jobs config-only fournis par des modèles GitLab : leurs règles ne peuvent pas être surchargées par rules:. Un merge documentaire passe de ~8 minutes à environ 3 minutes, sans image ni write-back. La dernière minute demande de traiter les modèles, ce qui reste ouvert.

Ce qu'il faut retenir

Une structure vaut mieux qu'une liste. Une liste énumère ce qu'on ne veut pas : elle grandit avec le dépôt et se tait quand elle est incomplète. Une structure énumère ce qu'on veut, et reste vraie sans entretien.

L'ordre des corrections n'était pas négociable. Écrire le filtrage avant de déplacer le backend aurait encodé dans la CI la liste de chemins qu'on venait de supprimer ailleurs.

Retirer des jobs crée un risque nouveau : celui du vert qui ne prouve rien. Une règle changes: trop large produit un pipeline vert qui n'a rien construit — le mode de défaillance le plus dangereux de ce projet. Le réflexe reste le même : lister les jobs, jamais lire le statut du pipeline.

Un pipeline vide est un échec, pas une absence. La même mécanique cassait le teardown du soir depuis dix jours sans que personne ne la relie à un problème de règles.