The Override That Outlived Its Job
I folded eight lint jobs into one and deleted their includes. The YAML was valid. Every downstream pipeline went completely empty — and the reason was a job I'd forgotten still had references. Part 5 of "The Ops Log," sequel to part 2.
TL;DR
TL;DR: A job name is referenced in more places than its definition: includes, but also overrides, needs, and extends. A shared pipeline carried a fleet-wide allow_failure override on one of the folded jobs. Removing the include orphaned it into a job with no script → every consumer pipeline resolved to zero jobs.
What happened
This is the sequel to part 2. The consolidation shipped: eight golden-image linters became one lint:php:all, the composed pipeline swapped its eight includes for one, the YAML validated, and a real-consumer run went green. Done.
Then the migration MRs — one per consumer that had customised a folded job — started producing empty pipelines. Not a lint failure, not a broken include. Zero jobs. GitLab's error:
pipeline creation
The resulting pipeline would have been empty.
Review the rules configuration for the relevant jobs.
# and, buried in a pipeline-create call:
jobs lint:composer:normalize config should implement
the script:, run:, or trigger: keyword
The error named lint:composer:normalize — a job I had just deleted the include for. I'd checked the consumer files; the override was gone from those. But the composed pipeline itself carried a fleet-wide non-blocking policy on it, sitting far away from the include list:
composed pipeline — 100 lines below the includes
# make normalization non-blocking for every consumer
lint:composer:normalize:
allow_failure: true
With the include removed, this stopped being an override of an included job and became a definition of a brand-new job — one with no script. GitLab rejects that, and a single invalid job takes the whole pipeline down to nothing. One dangling two-line block, and 43 consumers would have produced empty pipelines.
The fix had two halves: delete the orphan, and re-express its intent through the new job — the consolidated linter already had a per-check soft-fail input, so the fleet-wide "normalize never blocks" became one line on the include:
the fix
- component: …/lint-tools/lint-php-all@1.11.0
inputs:
soft-checks: normalize # normalize warns, never blocks — fleet-wide
# …and the orphaned lint:composer:normalize block deleted
What stings is that nothing in the authoring loop caught it. The YAML linter was happy — it's syntactically a valid job map. The consolidation diff looked complete — the includes were all gone. The gap was only visible when a real pipeline tried to resolve the job graph, which is exactly what the differential consumer run does and the linter doesn't.
The lesson
Before you delete a job, grep for its name across the whole surface — includes, overrides,
needs,extends— not just where it's defined. A valid-looking YAML file can still resolve to an invalid pipeline, so validate on a real consumer, not the linter.
Frequently asked questions
How could this have been found before rollout?+
Only through a differential run against a real consumer, as described in part 2 — a plain YAML lint or a green test run on the original consumer alone wouldn't have surfaced the 43 affected consumers with the orphaned override.
Why doesn't GitLab report directly that the job no longer has a definition?+
Because the YAML itself stays syntactically valid — an override block looks identical to a job definition. Only at pipeline creation, when GitLab resolves the full job graph, does it become visible that no script exists anymore.
Why isn't it enough to just search consumer files for overrides?+
Because overrides can also live in shared/composed pipelines, far from the include list — that's exactly where the fleet-wide allow_failure override sat in this case.
Conclusion
A job name lives in more files than its own definition. Deleting an include can silently turn a distant override into an invalid job definition — and a single invalid job is enough to take an entire pipeline down to zero. Grep the whole surface before deleting, and validate against real consumers, not the linter.
I check your GitLab CI consolidations for orphaned overrides before they hit 43 consumers at once.
Full-surface search for job references (includes, overrides, needs, extends) before any consolidation, plus differential validation against real consumer pipelines.
Platform operations, not consulting on paper: I build and maintain your CI/CD pipelines on an ongoing basis.
About the author
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.
