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

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:

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": [].

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.

Esc