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 |
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.
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": [].
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": "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)" }
]
}
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 and Project config only appear when they have
something to say. 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.