GitLab.Cli: From Versioned Inputs to Typed Observations
The GitLab CLI integration exposes commands and observations for repositories, pipelines, jobs and registry resources. Its observations can include server API data whose evolution is independent of the CLI version.
The integration preserves complete JSON evidence and adds contextual resource projections for known command paths. It also illustrates why normalized CLI names and C# names need separate handling.
Acquisition and generation
The Design executable collects releases from gitlab-org/cli, prepares version images and parses help for glab. Its default minimum is 1.47.0. Declared process values from GlabEnvironmentCatalog configure the runner.
Cached help can be reparsed without the image pipeline. JSON snapshots feed the shared generator, with commands in Contracts and builders/client in the implementation.
Hyphen and underscore command spellings can normalize to the same C# identifier. The shared naming resolver assigns deterministic distinct generated names while preserving the external command tokens.
A complete pipeline observation
This offline example constructs a generated CI status command and supplies a source-shaped JSON wrapper. It makes no API request.
using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.BinaryWrapper.Lib;
using FrenchExDev.Net.BinaryWrapper.Testing;
using FrenchExDev.Net.GitLab.Cli;
var binding = new BinaryBinding
{
Identifier = new BinaryIdentifier("glab"),
ExecutablePath = "unused",
DetectedVersion = SemanticVersion.Parse("1.117.0"),
};
var command = await Glab.Create(binding).Ci.StatusAsync(
builder => builder.WithOutput("json"));
var line = new OutputLine(
"""{"pipeline":{"id":42,"project_id":7,"status":"success","ref":"main","sha":"abc","web_url":"https://gitlab.example/pipeline/42"},"jobs":[]}""",
OutputSource.StdOut);
var executor = new CommandExecutor(new DictionaryBinaryResolver(binding),
new FakeProcessRunner([line]));
await foreach (var evt in executor.StreamAsync(
binding.Identifier, command, GlabOutputParsers.Default))
{
if (evt is GlabPipelineObserved pipeline)
Console.WriteLine($"Pipeline {pipeline.Id}: {pipeline.Status}");
if (evt.OutputLine is { } original)
Console.WriteLine($"{original.Source}: {original.Text}");
if (evt is GlabCommandExited exited)
Console.WriteLine($"glab process exit: {exited.ExitCode}");
}using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.BinaryWrapper.Lib;
using FrenchExDev.Net.BinaryWrapper.Testing;
using FrenchExDev.Net.GitLab.Cli;
var binding = new BinaryBinding
{
Identifier = new BinaryIdentifier("glab"),
ExecutablePath = "unused",
DetectedVersion = SemanticVersion.Parse("1.117.0"),
};
var command = await Glab.Create(binding).Ci.StatusAsync(
builder => builder.WithOutput("json"));
var line = new OutputLine(
"""{"pipeline":{"id":42,"project_id":7,"status":"success","ref":"main","sha":"abc","web_url":"https://gitlab.example/pipeline/42"},"jobs":[]}""",
OutputSource.StdOut);
var executor = new CommandExecutor(new DictionaryBinaryResolver(binding),
new FakeProcessRunner([line]));
await foreach (var evt in executor.StreamAsync(
binding.Identifier, command, GlabOutputParsers.Default))
{
if (evt is GlabPipelineObserved pipeline)
Console.WriteLine($"Pipeline {pipeline.Id}: {pipeline.Status}");
if (evt.OutputLine is { } original)
Console.WriteLine($"{original.Source}: {original.Text}");
if (evt is GlabCommandExited exited)
Console.WriteLine($"glab process exit: {exited.ExitCode}");
}A pipeline status of success is a server resource observation. The process exit is a separate result of retrieving that observation.
Contextual JSON projections
Complete values first produce GlabJsonObserved. Known paths can additionally project issues, merge requests, projects, pipelines, jobs, releases, runners, labels and registry data.
ci get exposes pipeline fields directly alongside jobs/variables. ci status wraps a pipeline object and jobs. The parser treats those shapes differently rather than applying one generic recursive search for every id field.
Unknown fields remain in JSON evidence. Required fields are validated before a resource projection is added; an unfamiliar shape remains a generic document. Status values stay strings so a server's new status vocabulary is not discarded.
API framing and format guards
The API command can emit multi-line JSON, NDJSON or adjacent paginated values without an intervening newline. The parser buffers and frames complete values while retaining their source lines.
DocumentIndex identifies an observed document, not a guessed page number. Incomplete buffered documents are released as raw lines at completion, and retained text has explicit limits.
--jq transformations, included HTTP headers and silent modes change the expected output. They are not treated as the normal JSON document format. The existing scraped api command has positional/flag limitations; the integration's explicit adapter handles supported invocations without pretending the generated surface is complete.
Diagnostics and credential protocols
Reviewed stderr diagnostics and JSON error envelopes have their own observations. GraphQL errors retain arrays, paths and extensions rather than only a flattened message.
The credential-helper protocol can emit a typed credential or helper error. A successfully encoded helper-error response can accompany process exit zero, so collectors or business policy must account for both content and terminal status.
Credentials are data in the contract, but applications should choose intentional logging/redaction policies instead of indiscriminately printing every raw JSON document.
Environment and independent versions
GlabEnvironment includes token/host/repository selection, API configuration, certificates, proxies and related settings. The catalogue distinguishes legacy names from newer prefixed variables and assigns their actual declared availability.
For example, several GLAB_* protocol/certificate settings start at 1.118.0, beyond the reviewed output profile at 1.117.0. Environment support and output support are therefore visibly separate.
Token alias precedence belongs to glab configuration. BinaryWrapper's command-over-binding merge is another boundary. The examples use no real tokens or host access.
GitLab server version and API fields are independent of glab version. A CLI profile cannot guarantee that all server resource fields stay constant.
Profiles, testing and consumption
The implemented profile covers the reviewed 1.117.0 stable patch, pinned in the source catalogue to 44790937bcbf6120698250cc41c9b4fb811c2a03. Other versions retain raw output and terminal status.
Tests cover command-context projections, JSON framing, pagination shapes, provenance, unknown fields, malformed/incomplete documents, diagnostics, format guards, fresh parsers and generated-command integration.
Consume resource observations through Contracts and distinguish pipeline/job statuses from the glab process exit. The separate GitLab CI YAML study addresses declaring pipeline configuration rather than observing a glab process.
Source snapshot
Source inspection: 2 October 2026. These implementation paths are relative to Net/FrenchExDev in the local ecosystem checkout. The v2 changes are largely uncommitted; these paths identify inspected code, without claiming that public branch links contain it.
GitLab.Cli/src/FrenchExDev.Net.GitLab.Cli.Design/Program.csGitLab.Cli/src/FrenchExDev.Net.GitLab.Cli.Contracts/FrenchExDev.Net.GitLab.Cli.Contracts.csprojGitLab.Cli/src/FrenchExDev.Net.GitLab.Cli/FrenchExDev.Net.GitLab.Cli.csprojGitLab.Cli/doc/TYPED-OUTPUT.mdGitLab.Cli/test/FrenchExDev.Net.GitLab.Cli.Tests
Previous: PodmanCompose · Implementation index · Series index · Next: Dotnet