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 inputsHow
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 droppedInputs/Workflowfor a config that resolves to nothing (aspec:-only component header).ResolveInputsran twice per parse; the second result was discarded.- The include HTTP client was rebuilt per parse, so
--watchreopened TLS connections on every save. - A typo'd
-p/--pipelinewas silently ignored by config-only commands, becausecollectIncludeVarsswallows preset errors. Now it fails the way it already does forrun/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)