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 list

A 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 deep

It 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 hi
glci 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-c

Then 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 deep

The 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.

Merge request reports

Loading
Loading