Skip empty pipelines when include:rules filter out all includes
What does this MR do and why?
Fixes a spurious failed pipeline (red X on the MR) that appears when include:rules: filter out all includes, leaving a config with zero visible jobs.
This is an alternative approach to !246380 (closed). Both fix the same issue (#591780); this one keys the decision on include filtering rather than deferring the error to workflow:rules.
Root cause
When every include: of a file is dropped by its include:rules: and no inline jobs exist, the config resolves to 0 visible jobs. Config::Process reports "jobs config should contain at least one visible job" as a config_error — a persistable failure reason — so a failed pipeline is saved and becomes the MR's head_pipeline (red X). But the emptiness is a filtering outcome, not a configuration mistake: the includes are conditionally applied by design and simply didn't match this pipeline.
Fix
External::Context: Add a shared mutablefilter_stateholder, copied by reference through#mutateso state recorded while processing a nested include is visible on the top-level context (the same mechanismexpandsetuses for cross-nesting visibility). It isprotected; callers usemark_includes_filtered_by_rules!/includes_filtered_by_rules?.External::Mapper::Filter: When a file's includes are all dropped by theirinclude:rules:(kept.empty? && locations.any?), callcontext.mark_includes_filtered_by_rules!.Config/YamlProcessor::Result: Exposeincludes_filtered_by_rules?, andonly_no_visible_jobs_error?so we reclassify only when emptiness is the sole error.Config::Process: When the only error is "no visible jobs" and includes were filtered by rules, report the non-persistablefiltered_by_rulesreason instead ofconfig_error. No failed pipeline is persisted.
On matching the "no visible jobs" error
only_no_visible_jobs_error? must identify one specific validation error, but CI config errors reach Result as flattened, runtime-composed strings (no typed identity survives). To avoid brittle coupling, the error message fragment lives in a single constant Jobs::NO_VISIBLE_JOBS_MESSAGE, used both when the error is added and when it is matched (via end_with?), so a reword updates both sides. An entry-level spec guards that the composed message still ends with that fragment, so any change to the composition fails loudly instead of silently reverting the fix.
Scope of the reclassification
The reclassification is deliberately narrow — it fires only when:
- the sole config error is "no visible jobs", and
- a file had
include:entries that were entirely removed byinclude:rules:.
If some includes survive (even if the surviving ones contribute only hidden jobs), or the config genuinely defines no jobs and no includes, the original config_error is preserved. This avoids masking real "you defined no jobs" mistakes, and also leaves unrelated errors (e.g. the nested empty-include syntax error in #607152 (closed)) untouched.
Behaviour matrix
| Scenario | Before | After |
|---|---|---|
| All includes of a file filtered by rules → 0 jobs | persisted config_error (red X) |
not persisted (filtered_by_rules) |
| Some includes survive, but still 0 visible jobs | persisted config_error |
unchanged |
| Config genuinely defines no jobs and no includes | persisted config_error |
unchanged |
| Syntax / other config errors | persisted config_error |
unchanged |
| Valid config | normal | unchanged |
Nested includes are covered: if a nested file's includes are all filtered out, the shared filter_state surfaces the flag at the top level.
How to reproduce
-
Project
.gitlab-ci.yml:workflow: rules: - if: $CI_PIPELINE_SOURCE == 'push' && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH when: never - if: $CI_PIPELINE_SOURCE == 'merge_request_event' include: - component: $CI_SERVER_FQDN/group/component/template@main rules: - if: $CI_PIPELINE_SOURCE == 'merge_request_event' -
Push to a non-default branch
-
Before: push pipeline created with 0 jobs,
config_error, persisted as failed (red X) -
After: no pipeline persisted
Reproduced on GitLab.com: https://gitlab.com/oyakovenko-group/broken-pipeline-591780/-/pipelines/2685242385
References
- Closes #591780
- Alternative to !246380 (closed)
- Adjacent (not fixed here): #607152 (closed)
MR acceptance checklist
- Unit tests:
Filter(all/partial/none/empty),Contextmutation propagation,Config::Process,YamlProcessor::Result, and an entry-spec drift guard for the error fragment - Integration tests: top-level filtered-to-empty, nested filtered-to-empty, surviving-include-with-no-visible-jobs, genuinely-empty
- All existing specs pass
- RuboCop clean