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 needUse
React to jobs while the pipeline runsglci run --json
A result file a CI system rendersglci 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:

FieldTypeMeaning
schemaintSchema version. Currently 1.
typestringEvent type, from the table below.
timeRFC 3339When the event occurred.
pipeline_idintThe 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:

FieldTypeMeaning
child_pipeline_idintThe child pipeline the event came from.
parent_job_namestringThe trigger job that spawned that child.

Event types#

typeWhenExtra fields
pipeline_startOnce, after the pipeline is preparedstages, jobs (name, stage, when, allow_failure, needs), detached
stage_startA stage beginsstage
job_statusA job changes statusjob_name, stage, status, attempt, max_retries, allow_failure, duration_ms, exit_code, failure_reason
job_logA chunk of job trace — only with --json-logsjob_name, log
job_variablesOnly with --show-variablesjob_name, variables, rule_trace
child_pipeline_startA trigger job spawns a childparent_job_name, child_pipeline_id, job_count, cross_project
child_pipeline_doneA child pipeline finishesparent_job_name, child_pipeline_id, failed, cross_project, error
pipeline_doneLast event of every runoutcome, 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:

outcomeMeaningExit code
passedEvery job that could fail the pipeline succeeded0
failedAt least one job failed without allow_failure1
canceledThe run was cancelled before it finished1
detachedglci stopped watching; jobs continue in the daemon0

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:

FormatDescription
junit (default)JUnit XML — every CI system renders it with no extra tooling
jsonA 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:

FieldMeaning
schemaSchema version, shared with the event stream
pipeline_idThe pipeline
outcomepassed, failed or canceled
started_at, finished_atRFC 3339 timestamps
duration_msTotal wall-clock time of the run
stagesStage names, in order
jobsThe 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 statusJUnit
passed, reusedpass
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.

Esc