GitLab CI

One job in the .post stage, gated on when: on_failure. It reads the pipeline it belongs to and writes its report as artifacts.

The minimal job

Deterministic report, artifacts on every failure, no model required:

.gitlab-ci.yml
# runs only on failure; always exits 0.
ci-doctor:
  stage: .post
  image: registry.internal.example.com/ci-doctor:latest
  rules:
    - when: on_failure
  allow_failure: true
  variables:
    CI_DOCTOR_GITLAB_TOKEN: "$CI_DOCTOR_TOKEN"      # a read-scoped API token
  script:
    - ci-doctor analyze "$CI_PIPELINE_ID"
  artifacts:
    when: always
    expire_in: 1 week
    paths:
      - report.md
      - report.json

CI_PIPELINE_ID and CI_PROJECT_ID are predefined by GitLab, so the only thing you supply is the token. It needs read_api — and apionly if you turn on merge-request notes below.

Add an LLM explanation

Point at any OpenAI-compatible endpoint (swap in place of the deterministic-only variables):

  variables:
    CI_DOCTOR_GITLAB_TOKEN: "$CI_DOCTOR_TOKEN"
    CI_DOCTOR_LLM__MODEL: "qwen2.5-coder:32b"
    CI_DOCTOR_LLM__API_BASE: "http://vllm.internal:8000/v1"
    # CI_DOCTOR_LLM__API_KEY... uses api_key_env if your gateway needs one

Optional at every step: with no model the job still emits the full deterministic report. The other backends are on Configuration .

Post a note on the merge request

Idempotent — it updates its own previous comment instead of spamming. Gated on confidence ≥ medium.

  variables:
    CI_DOCTOR_GITLAB_TOKEN: "$CI_DOCTOR_TOKEN"
    CI_DOCTOR_OUTPUT__MR_NOTE: "true"   # post/update one note when confidence >= medium

Needs CI_MERGE_REQUEST_IID, which GitLab sets on merge-request pipelines, and a token with api scope rather thanread_api.

Self-hosted GitLab

Override the API host and hand it your internal CA. Proxy variables are honoured as-is:

  variables:
    CI_DOCTOR_GITLAB_TOKEN: "$CI_DOCTOR_TOKEN"
    CI_DOCTOR_GITLAB__BASE_URL: "https://gitlab.internal.example.com"
    CI_DOCTOR_GITLAB__CA_BUNDLE: "/etc/ssl/certs/internal-ca.pem"

HTTPS_PROXY, NO_PROXY and REQUESTS_CA_BUNDLE behave the way you would expect. For runners with no egress at all, seeLocal & air-gapped .

It always exits 0 and is read-only, so adding this job can never fail your pipeline or mask the real error. allow_failure: true is belt-and-braces.