Structured Output
glci’s human output is designed to be read by a person and is free to change. When a script, a CI job, or an editor integration needs to know what happened, use the structured surfaces on this page instead.
| What you need | Use |
|---|---|
| React to jobs while the pipeline runs | glci run --json |
| A result file a CI system renders | glci run --report <file> |
| The state of past runs, the daemon, or the environment | --json on history, ps, doctor |
The event stream: glci run --json#
--json writes one JSON object per line to stdout, as the run progresses, and
suppresses the decorative terminal output. Because it is line-delimited rather than one
document at the end, a consumer can react while the pipeline is still going, and a
cancelled run still yields usable output.
glci run --json | while read -r line; do
echo "$line" | jq -r 'select(.type == "job_status" and .status == "failed") | .job_name'
done
Exit codes are unchanged: 0 when the pipeline passes, 1 when it fails or is
cancelled, 2 for a glci error.
Every event#
All events carry these fields:
| Field | Type | Meaning |
|---|---|---|
schema | int | Schema version. Currently 1. |
type | string | Event type, from the table below. |
time | RFC 3339 | When the event occurred. |
pipeline_id | int | The pipeline the event belongs to. |
A child pipeline’s own job_status and job_log events are forwarded onto the
parent’s stream. Those carry two extra fields, so you can attribute them without
correlating against an earlier event — and note their pipeline_id is the
child’s, not the parent’s:
| Field | Type | Meaning |
|---|---|---|
child_pipeline_id | int | The child pipeline the event came from. |
parent_job_name | string | The trigger job that spawned that child. |
Event types#
type | When | Extra fields |
|---|---|---|
pipeline_start | Once, after the pipeline is prepared | stages, jobs (name, stage, when, allow_failure, needs), detached |
stage_start | A stage begins | stage |
job_status | A job changes status | job_name, stage, status, attempt, max_retries, allow_failure, duration_ms, exit_code, failure_reason |
job_log | A chunk of job trace — only with --json-logs | job_name, log |
job_variables | Only with --show-variables | job_name, variables, rule_trace |
child_pipeline_start | A trigger job spawns a child | parent_job_name, child_pipeline_id, job_count, cross_project |
child_pipeline_done | A child pipeline finishes | parent_job_name, child_pipeline_id, failed, cross_project, error |
pipeline_done | Last event of every run | outcome, failed, canceled, error, results |
needs is the resolved list the scheduler used, not the list the config wrote: a dangling optional: true entry has already been dropped.
status on a job_status event is one of running, passed, failed,
skipped, reused, retrying. (results[] and the report can additionally
carry manual, canceled, or an empty status, for jobs that never reported one.)
duration_ms is set on passed and failed. exit_code and failure_reason
are set on failures when known; failure_reason uses GitLab’s own vocabulary
(script_failure, job_execution_timeout, runner_system_failure, …) plus
canceled for a job whose run glci stopped.
allow_failure on a terminal job_status event is the effective value: what the
pipeline’s verdict actually used. A job that failed with an exit code listed under
allow_failure:exit_codes reports true even though it declared no plain boolean, and
a job that timed out reports true if it declared allow_failure: true — tolerance is
a property of the job, not of the kind of failure. Two failures report false whatever
the job declared: one belonging to a cancelled run (failure_reason: canceled), and a
glci-side error that stopped the job from running at all. See
Tolerated failures.
pipeline_done#
Every stream ends with exactly one pipeline_done, including a detached run and a run
you interrupt with Ctrl+C — so a consumer can read until that event without
special-casing anything.
The exception is glci itself failing — a bad flag, an unparseable CI file, an
unreachable daemon, a broken stdout. The error is plain text on stderr, glci
exits 2, and the stream is either empty or ends without a pipeline_done. A
consumer should treat “exited non-zero with no pipeline_done” as a glci error
rather than a pipeline result.
A pipeline rejected by workflow: rules:
lands in that same bucket, and so do glci jobs --json / glci show --json,
which print nothing rather than an empty array. No pipeline was created, so
there is no outcome to report — the message on stderr starts with the pipeline did not run:.
outcome is one of:
outcome | Meaning | Exit code |
|---|---|---|
passed | Every job that could fail the pipeline succeeded | 0 |
failed | At least one job failed without allow_failure | 1 |
canceled | The run was cancelled before it finished | 1 |
detached | glci stopped watching; jobs continue in the daemon | 0 |
results lists only the parent pipeline’s own jobs, each with name, stage, status,
allow_failure, duration_ms, retried and failure_reason. A child pipeline’s
overall verdict arrives as its trigger job’s status and as child_pipeline_done;
its individual jobs stream as they run but are not listed in results.
With --watch#
--watch re-runs the pipeline on every file change, and each run emits its own
complete stream — pipeline_start through pipeline_done, concatenated. That is
valid NDJSON throughout. --report, if given, is overwritten by each run.
Job logs#
Logs are not in the stream by default: they are the bulk of a run’s output, and
glci log already serves them. Pass --json-logs when you want the stream to be
self-sufficient — each job_log event carries a raw chunk of trace, not split on line
boundaries, so concatenate the log fields of a job to reassemble its output.
Without --json-logs, glci also tells the daemon not to send trace events at
all, so a log-heavy pipeline costs nothing to stream. --json-logs on its own,
without --json, does nothing and says so.
Compatibility#
The event schema is a supported surface, not an internal detail. Fields are added, never
renamed or removed; schema is bumped only when an addition changes the meaning of a
field that already existed. Ignore event types and fields you do not recognise — new
ones will appear.
The daemon’s own socket protocol is not this surface. It has no compatibility promise.
The result report: glci run --report#
--report <file> writes the pipeline’s outcome to a file when the run ends — job names,
statuses, durations and failure reasons. It works with or without --json.
glci run --report reports/junit.xml
Parent directories are created as needed, and the file is written atomically, so a CI job that collects the report cannot pick up a half-written document.
--report-format selects the format:
| Format | Description |
|---|---|
junit (default) | JUnit XML — every CI system renders it with no extra tooling |
json | A single JSON document, schema-versioned like the event stream |
The json document carries a little more than pipeline_done does — it is the
run’s record rather than an event:
| Field | Meaning |
|---|---|
schema | Schema version, shared with the event stream |
pipeline_id | The pipeline |
outcome | passed, failed or canceled |
started_at, finished_at | RFC 3339 timestamps |
duration_ms | Total wall-clock time of the run |
stages | Stage names, in order |
jobs | The same array pipeline_done puts in results |
An invalid --report-format, or a --report path glci cannot write, is reported
before the pipeline starts. If the write itself fails at the end of an
otherwise-passing run, glci exits 2; if the pipeline had already failed, the
failure is a warning on stderr and the exit code stays 1 — a broken report
never hides a broken pipeline. The file is created mode 0600.
Use it to make glci assert on pipelines from inside a real CI job:
test-pipeline:
script:
- glci run --json --report reports/junit.xml
artifacts:
when: always
reports:
junit: reports/junit.xml
How statuses map onto JUnit#
One <testsuite> per stage, one <testcase> per job, in pipeline order.
| Job status | JUnit |
|---|---|
passed, reused | pass |
failed | <failure>, message = the failure reason and exit code |
failed with allow_failure | <skipped>, message = “job failed but allow_failure is set”, followed by the reason |
canceled | <error> |
skipped, manual, never ran | <skipped> |
A job that failed under allow_failure is reported as skipped rather than failed on
purpose: it does not fail the pipeline, so recording it as a failure would paint a CI job
red that glci deliberately exited 0 on. The report always agrees with the exit code.
--report is rejected with --detached, because a detached run has no outcome by the
time glci exits.
--json on read-only commands#
show, jobs and variables have always had --json. history, history show, ps
and doctor accept it too. In each case the JSON document is the only thing on stdout.
glci history --json#
An array of pipeline records: id, status, git_ref, git_sha, started_at,
finished_at, job_count, stages, file, inputs, plus simulate,
parent_pipeline_id and parent_job_name when they apply (child pipelines always
carry the latter two). No pipelines is [], not a message.
glci history show --json#
The same pipeline record, plus a jobs array with each job’s name, stage,
status, exit_code, image, order_index, needs, allow_failure, when,
retry_attempt, retry_max, started_at, finished_at, and — where they apply —
child_pipeline_id, pages and pages_publish. A pipeline that died during
preparation reports "jobs": [].
Here too needs is the resolved list, with any dangling optional: true entry already dropped.
glci ps --json#
{
"daemon_running": true,
"pipelines": [
{
"pipeline_id": 4,
"status": "running",
"project": "my-project",
"work_dir": "/home/me/my-project",
"jobs": { "build": "passed", "test": "running" },
"running": 1,
"pending": 0
}
],
"engine_available": true,
"containers": [
{ "name": "glci-mock-1", "status": "Up 2 minutes", "image": "registry:2", "ports": ["0.0.0.0:5000->5000/tcp"] }
]
}
daemon_running and engine_available are reported rather than implied, because an
empty list means something different when the thing that owns it is not there. With
--containers, the pipelines and daemon_running keys are absent — as opposed to
pipelines: [], which means “the daemon has none”.
glci doctor --json#
{
"ok": true,
"checks": [
{ "name": "Docker", "status": "pass", "detail": "docker 27.4.0" },
{
"name": "glci images",
"status": "pass",
"detail": "configured by [images]",
"hints": ["runner proxy.corp.internal/mirror/gitlab/gitlab-runner:latest (from [images] registry)"],
"images": [
{ "kind": "glci", "ref": "proxy.corp.internal/mirror/gitlab-org/ci-cd/runner-tools/glci:latest", "source": "[images] registry" },
{ "kind": "runner", "ref": "proxy.corp.internal/mirror/gitlab/gitlab-runner:latest", "source": "[images] registry" },
{ "kind": "helper", "ref": "", "source": "chosen by gitlab-runner" },
{ "kind": "utility", "ref": "proxy.corp.internal/mirror/library/alpine:latest", "source": "[images] registry" },
{ "kind": "binfmt", "ref": "proxy.corp.internal/mirror/tonistiigi/binfmt:latest", "source": "[images] registry" }
],
"upstreams": ["proxy.corp.internal"]
},
{ "name": "Daemon", "status": "warn", "detail": "not running (starts automatically on first use)" },
{ "name": "GitLab token", "status": "pass", "detail": "...1e6s" },
{ "name": "CI config", "status": "pass", "detail": "31 jobs, 10 stages" },
{ "name": "Git repository", "status": "pass", "detail": "my-project (67ddd98)" }
]
}
hints carries the → continuation lines a row printed, if any. The glci images row additionally carries images and upstreams as data, so a script
does not have to re-split the padded hint text; ref is empty when glci sets no
reference and gitlab-runner chooses. Both keys are omitted on every other row.
See The images glci pulls for itself.
name is the same label the text report prints, so it varies with the
environment — the engine row is Docker or Podman, and rows like Daemon endpoint, Engine config, Project config and Global config only appear when
they have something to say (Global config when it is the global
~/.glci/config.toml that failed to parse). A row’s hints are the → ... continuation lines from the
text report. Token values are masked in both outputs.
status is pass, fail or warn. ok mirrors the exit code: only a fail clears
it, because a warn is a condition glci recovers from on its own. The text report is
suppressed entirely, so glci doctor --json | jq -e .ok is a clean gate.