Der Override, der seinen Job überlebt hat
Ich habe acht Lint-Jobs zu einem zusammengelegt und ihre Includes gelöscht. Das YAML war gültig. Jede nachgelagerte Pipeline wurde komplett leer — und der Grund war ein Job, von dem ich vergessen hatte, dass noch Referenzen auf ihn existierten. Teil 5 von „The Ops Log“, Fortsetzung von Teil 2.
TL;DR
TL;DR: Ein Job-Name wird an mehr Stellen referenziert als nur seiner Definition: Includes, aber auch Overrides, needs und extends. Eine geteilte Pipeline trug einen flottenweiten allow_failure-Override auf einem der zusammengelegten Jobs. Das Entfernen des Includes machte ihn zu einem Job ohne script → jede Consumer-Pipeline löste sich zu null Jobs auf.
Was passiert ist
Das hier ist die Fortsetzung von Teil 2. Die Konsolidierung war ausgeliefert: Acht goldene-Image-Linter wurden zu einem lint:php:all, die zusammengesetzte Pipeline tauschte ihre acht Includes gegen eines, das YAML validierte, und ein Lauf bei einem echten Consumer war grün. Fertig.
Dann begannen die Migrations-MRs — einer pro Consumer, der einen zusammengelegten Job angepasst hatte — leere Pipelines zu produzieren. Kein Lint-Fehlschlag, kein kaputter Include. Null Jobs. GitLabs Fehler:
Pipeline-Erstellung
The resulting pipeline would have been empty.
Review the rules configuration for the relevant jobs.
# und, vergraben in einem Pipeline-Create-Aufruf:
jobs lint:composer:normalize config should implement
the script:, run:, or trigger: keyword
Der Fehler nannte lint:composer:normalize — einen Job, dessen Include ich gerade eben gelöscht hatte. Ich hatte die Consumer-Dateien geprüft; der Override war dort weg. Aber die zusammengesetzte Pipeline selbst trug, weit entfernt von der Include-Liste, eine flottenweite Non-Blocking-Policy auf genau diesem Job:
zusammengesetzte Pipeline — 100 Zeilen unter den Includes
# Normalisierung für jeden Consumer non-blocking machen
lint:composer:normalize:
allow_failure: true
Mit entferntem Include hörte das auf, ein Override eines eingebundenen Jobs zu sein, und wurde zur Definition eines brandneuen Jobs — einem ohne script. GitLab lehnt das ab, und ein einziger ungültiger Job zieht die ganze Pipeline auf null Jobs herunter. Ein einziger herumliegender Zwei-Zeilen-Block, und 43 Consumer hätten leere Pipelines produziert.
Der Fix hatte zwei Hälften: den Waisen löschen, und seine Absicht über den neuen Job neu ausdrücken — der konsolidierte Linter hatte bereits einen Per-Check-Soft-Fail-Input, also wurde das flottenweite „Normalize blockiert nie“ zu einer Zeile am Include:
der Fix
- component: …/lint-tools/lint-php-all@1.11.0
inputs:
soft-checks: normalize # normalize warnt, blockiert nie — flottenweit
# …und der verwaiste lint:composer:normalize-Block gelöscht
Was schmerzt: Nichts im Autoring-Loop hat es abgefangen. Der YAML-Linter war zufrieden — es ist syntaktisch eine gültige Job-Map. Der Konsolidierungs-Diff sah vollständig aus — die Includes waren alle weg. Die Lücke war nur sichtbar, als eine echte Pipeline versuchte, den Job-Graph aufzulösen — genau das, was der differenzielle Consumer-Lauf tut und der Linter nicht.
Die Lehre
Bevor Sie einen Job löschen, nach seinem Namen über die gesamte Fläche greppen — Includes, Overrides,
needs,extends— nicht nur, wo er definiert ist. Eine gültig aussehende YAML-Datei kann sich trotzdem zu einer ungültigen Pipeline auflösen, also gegen einen echten Consumer validieren, nicht gegen den Linter.
Häufige Fragen
Wie hätte man das vor dem Rollout finden können?+
Nur durch einen differenziellen Lauf gegen einen echten Consumer, wie in Teil 2 beschrieben — ein reiner YAML-Lint oder ein grüner Testlauf beim ursprünglichen Consumer allein hätten die 43 betroffenen Consumer mit dem verwaisten Override nicht aufgedeckt.
Warum meldet GitLab nicht direkt, dass der Job keine Definition mehr hat?+
Weil das YAML selbst syntaktisch gültig bleibt — ein Override-Block sieht identisch zu einer Job-Definition aus. Erst bei der Pipeline-Erstellung, wenn GitLab den vollständigen Job-Graph auflöst, wird sichtbar, dass kein script mehr existiert.
Warum reicht es nicht, nur die Consumer-Dateien nach Overrides zu durchsuchen?+
Weil Overrides auch in gemeinsam genutzten/zusammengesetzten Pipelines liegen können, weit entfernt von der Include-Liste — genau dort saß der flottenweite allow_failure-Override in diesem Fall.
Fazit
Ein Job-Name lebt in mehr Dateien als seiner eigenen Definition. Das Löschen eines Includes kann einen weit entfernten Override lautlos in eine ungültige Job-Definition verwandeln — und ein einziger ungültiger Job reicht, um eine ganze Pipeline auf null herunterzuziehen. Grep über die gesamte Fläche vor dem Löschen, und validieren Sie gegen echte Consumer, nicht gegen den Linter.
Ich prüfe Ihre GitLab-CI-Konsolidierungen auf verwaiste Overrides, bevor sie 43 Consumer gleichzeitig treffen.
Vollständige Flächen-Suche nach Job-Referenzen (Includes, Overrides, needs, extends) vor jeder Konsolidierung, plus differenzielle Validierung gegen reale Consumer-Pipelines.
Plattform-Betrieb statt Beratung auf Papier: Ich baue und pflege Ihre CI/CD-Pipelines laufend.
Über den Autor

Kai Ole Hartwig
Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.
