fix(config): Fix flatten nested stages: arrays as GitLab does
What this MR does and why?
GitLab accepts a nested array for stages: and flattens it — Entry::Stages validates nested_array_of_strings: { max_level: 10 } and #value returns @config.flatten. glci filtered the list with a plain string type assertion, so nested entries vanished and every job assigned to one became unschedulable.
It was not silent, and that made it worse. glci lint rejected the config with an error naming a stage glci itself had just discarded:
CI configuration is invalid:
job "job-a" references stage "a" which is not defined in stages listA shared flattenStageList now flattens the list the way Entry::Stages#value does and returns GitLab's verdict on it. Both readers of the key use it — injectEdgeStages on the offline path and parseStages on both paths.
The second one matters more than symmetry: GitLab's lint API returns merged_yaml with stages: still nested (['.pre', ['a','b'], 'c', '.post']), so parseStages is the only reader on the --use-api path and flattening it is what fixes that path.
Validation
The same GitLab validator that permits nesting also bounds it, and rejects everything else. Implementing the bound without its message would just move the silent drop, so glci now reports what GitLab reports, verbatim:
CI configuration is invalid:
stages config should be an array of strings or a nested array of strings up to 10 levels deepIt lands in Pipeline.ConfigErrors alongside the existing hooks: diagnostics, so it fails glci lint without aborting glci run — the same contract every other config diagnostic has today.
Every row below was confirmed against POST /ci/lint with dry_run on gitlab.com, not inferred from the Ruby:
stages: |
GitLab | glci before | glci after |
|---|---|---|---|
[[a, b], c] |
valid → .pre a b c .post |
.pre c .post |
matches |
[[[a]], b] |
valid | .pre b .post |
matches |
[[], c] |
valid | .pre c .post |
matches |
| nested 9 more levels | valid | nested entry dropped | matches |
| nested 10 more levels | error | nested entry dropped, silently | matches |
[a, 1], [a, true] |
error | .pre a .post, silently |
matches |
[a, {k: v}], [a, <nil>] |
error | .pre a .post, silently |
matches |
stages: a |
error | passed through, silently | matches |
stages: (null) |
valid | valid | valid |
The bound counts the top-level list as the first level, so total array nesting is at most 10. NestedArrayHelpers#validate_nested_array_recursively tests the string predicate before the depth guard, which is why a string is legal at any level but an array is not legal at level zero.
Not in this MR
#136 (closed) — .pre/.post are not stripped and re-anchored when a config lists them out of position — is a separate open issue in the same function. injectEdgeStages keeps its presence check untouched.
Steps to reproduce
stages: [[a, b], c]
job-a:
stage: a
script: echo hi
job-c:
stage: c
script: echo higlci merged # before: stages: [.pre, c, .post] after: [.pre, a, b, c, .post]
glci lint # before: job "job-a" references stage "a" which is not defined in stages list
# after: CI configuration is valid: 2 jobs, 5 stages
glci jobs # before: job-c only after: job-a and job-cThen the rejected shapes:
printf 'stages: [a, 1]\njob:\n stage: a\n script: echo\n' > .gitlab-ci.yml
glci lint # before: silently valid, stage "1" dropped
# after: stages config should be an array of strings or a nested
# array of strings up to 10 levels deepThe comprehensive e2e fixture now declares its first stage pair nested (- [setup, build]), so the whole suite exercises the flatten. The resolved list is unchanged, so all three golden fixtures are untouched.
Relevant issues and other links
- Closes #137 (closed)
- Related: #136 (closed) — edge stages not re-anchored, same function, not fixed here
- Related: !154 (merged) — added
glci merged, which is where the dropped stages are visible lib/gitlab/ci/config/entry/stages.rb—MAX_NESTING_LEVEL,#value→@config.flattenlib/gitlab/config/entry/validators/nested_array_helpers.rb— the depth/predicate ordering