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
- Les mots du sujet
- Le problème : un pipeline aveugle au type de changement
- Pourquoi il fallait déplacer le backend d'abord
- Ce qui déclenche quoi, désormais
- Pourquoi les huit jobs de la chaîne image partagent un seul déclencheur
- Le piège : un pipeline vide n'est pas neutre
- La documentation continue d'être publiée
- Le write-back ordonne avant d'écrire
- Ce que ces corrections ne font pas
- Ce qu'il faut retenir
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.
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 ».
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:.
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.
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ément — apt-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.