feat(cli): add glci merged to print the fully-resolved CI configuration

What

Adds glci merged — prints the CI configuration as glci understands it, after include:, extends:, !reference, default: inheritance and spec: inputs: interpolation. YAML by default, --json for scripting. Alias: merged-yaml.

A real pipeline is rarely one file, and until now nothing showed the merged result: lint says whether it parses, jobs lists names, variables explains precedence. glci already computes the merged tree internally, so this mostly exposes what exists rather than building new resolution logic.

Output is before rules: evaluation, so it stays context-independent. Keys are sorted, so it is byte-stable and diffs cleanly. Config goes to stdout and warnings to stderr, so a redirect yields a clean file while a skipped include is still reported.

glci merged                               # merged YAML
glci merged --json | jq '.["build-job"]'  # machine-readable
glci merged --input env=staging           # with pipeline inputs

How

Steps 1–8 of ParseOffline move into resolveConfigTree, exported as ResolveMergedConfig and reached from the CLI via config.MergedConfig. ParseOffline keeps steps 9–10 (typed job parsing, matrix expansion) on top of it, so the two paths cannot drift. cmd/glci's parsePipeline is likewise split into pipelineParseOptions, now the single construction site for every command that resolves the CI config.

Fixed along the way

Surfaced by review of the refactor:

  • ParseOffline's empty-config short circuit dropped Inputs/Workflow for a config that resolves to nothing (a spec:-only component header).
  • ResolveInputs ran twice per parse; the second result was discarded.
  • The include HTTP client was rebuilt per parse, so --watch reopened TLS connections on every save.
  • A typo'd -p/--pipeline was silently ignored by config-only commands, because collectIncludeVars swallows preset errors. Now it fails the way it already does for run/show.
  • CLI errors reached the TTY unsanitized, so a cloned repo could repaint the terminal through an error message quoting its file paths (new daemon.SanitizeForTerminal).

GitLab fidelity

The docs spell out how the output differs from GitLab's merged_yaml, verified against lib/gitlab/ci/config.rb, so a diff against the Pipeline Editor's "Full configuration" tab is not misleading: default: and global variables: are applied into each job (GitLab does that after producing merged_yaml), extends: keys are removed (GitLab keeps them), keys are sorted (GitLab preserves source order), and parallel: matrix: expansion plus .glciconfig.toml [jobs."<name>"] overrides are absent.

Tests

36 new tests: includes + extends: + !reference + spec: inputs: together, determinism, agreement with ParsePipeline, repo-root include resolution, --inputs-file, missing required input, no ReferenceTag leaking into JSON, httptest-backed remote: include (success and failure), --watch document separators, preset validation, plus real-binary e2e coverage. All hermetic — no network.

Docs

CLI reference (including a "How this differs from GitLab's merged YAML" table), debugging guide, troubleshooting, quick start, pipeline inputs, pipeline presets, running pipelines, ARCHITECTURE.md, SKILL.md, README.md.

Verification

go build ./..., go vet ./..., make test, make test-e2e-parse, make test-e2e-golden, and TestCLICommand_Help all pass locally.

Follow-ups

Two pre-existing GitLab-fidelity bugs that glci merged makes visible are filed separately rather than widened into this MR: .pre/.post are not re-anchored in stages:, and nested stage arrays are dropped instead of flattened. A third issue covers CLI-side masking for variable values echoed in include-resolution warnings.

Closes #133 (closed)

Merge request reports

Loading
Loading