Configuration

One layered, pydantic-validated config. Unknown keys are an error, not a silent typo.

This page is the reference — every key and its default. For what the keysmean — phases, attribution, denoising, matchers, budget, redaction — start withConcepts. The shipped matcher packs are listed onMatchers.

How layers combine

schema defaults  <  defaults.yml  <  repo .ci-doctor.yml  <  CI_DOCTOR_* env  <  CLI flags

Mappings deep-merge; lists and scalars replace. The one exception is a list whose entries all carry an id — today only extraction.matchers — which merges per id, so your packs add to the shipped ones instead of wiping them. Point at specific files with -f path/to/.ci-doctor.yml, repeated as often as you like — each one overrides the last.

Inspecting the result

Config bugs are almost always a layer you forgot was applied. Ask:

ci-doctor config           # the effective config, every layer merged
ci-doctor config --diff    # only what your file/env/flags change vs the defaults
ci-doctor config --schema  # the JSON Schema, for editors and CI validation
ci-doctor config --validate            # merge every layer and report what fails
ci-doctor config -f base.yml -f ci.yml # repeatable, the rightmost file wins
# Output pages through $PAGER on a terminal; --plain prints raw (as does piping).

--diff reads like git diff: green is what your config adds, red is the default it replaced.

Editor completion

Every release ships ci-doctor.schema.json, generated from the same pydantic model that validates your file, so it can never drift. Point your editor at it:

# yaml-language-server: $schema=https://github.com/fennet82/ci-doctor/releases/latest/download/ci-doctor.schema.json
llm:
  model: qwen2.5-coder:32b

Environment variables

# Nested keys use a double underscore:
export CI_DOCTOR_LLM__MODEL="qwen2.5-coder:32b"
export CI_DOCTOR_LLM__API_BASE="http://vllm.internal:8000/v1"
export CI_DOCTOR_PROVIDER="github"

# Secret-looking single-underscore vars (e.g. CI_DOCTOR_GITLAB_TOKEN) are
# referenced by NAME (token_env) and ignored by the config loader.

LLM backends

The LLM step is bring-your-own. Pick one with llm.backend:

backendWhat it callsNeeds
openai (default)Any OpenAI-compatible endpoint (Ollama, vLLM, llama.cpp, internal gateway)model + api_base
litellmProviders the OpenAI shape cannot reach — Bedrock (SigV4), Vertex (GCP auth), Azure. Via litellm model; pip install ci-doctorr[litellm]
claude_codeThe local claude CLI, headless (claude -p)the claude binary on PATH; uses its own auth
Reach for openai first. It is the only backend in the base install (no extra deps, no runtime downloads), and the OpenAI Chat Completions shape is what Ollama, vLLM, llama.cpp, Together, Groq, DeepSeek, Mistral and most others already speak — including Gemini and Claude via their compatibility endpoints. Uselitellm only for the clouds that need their own auth. It may pulltiktoken, which fetches vocab at runtime, so it is unfit for a strict air gap. When the LLM is disabled or unavailable, ci-doctor emits the deterministic report instead of failing.

Full reference

Drop a .ci-doctor.yml at your repo root. Everything below is optional — every key has a default.

# .ci-doctor.yml — every knob, with defaults shown.
provider: gitlab                 # gitlab | github

gitlab:
  base_url: https://gitlab.com   # default; override for a self-hosted instance
  api_version: v4
  token_env: CI_DOCTOR_GITLAB_TOKEN
  token_file: null               # e.g. /run/secrets/gitlab_token (wins over token_env)
  ca_bundle: null                # path to a custom/internal CA bundle
  verify_ssl: true
  timeout_seconds: 30

github:                          # used when provider: github
  base_url: https://api.github.com     # default; override with a GHE base, e.g. https://ghe/api/v3
  token_env: CI_DOCTOR_GITHUB_TOKEN
  token_file: null
  ca_bundle: null
  verify_ssl: true
  timeout_seconds: 30

llm:
  enabled: true                  # false => deterministic-only report
  backend: openai                # openai | litellm | claude_code
  model: null                    # e.g. "qwen2.5-coder:32b" or "claude-opus-4-8"
  api_base: null                 # OpenAI-compatible / litellm base URL
  api_key_env: null              # optional; local servers often need none
  ca_bundle: null                # LLM endpoint's own CA, independent of the provider's
  max_input_tokens: 12000
  temperature: 0.1
  timeout_seconds: 120

analysis:
  include_allowed_failures: false   # allow_failure jobs are noise by default
  max_jobs_analyzed: 10
  skip_llm_for: [no_runner, missing_dependency, cancelled]  # fully-determined -> templated
  known_flaky_tests: []             # substrings that short-circuit to likely_flaky

phases:                          # section/step name -> phase (merged onto the shipped map)
  my_custom_deploy_section: script

extraction:
  tail_lines: 120
  matchers:                      # anchored windows; higher priority survives budget pressure
                                 # Merged by id onto the shipped language packs: a new
                                 # id adds a pack, a shipped id retunes one (and warns).
    - id: my_service_errors      # new id -> added alongside the shipped packs
      pattern: '^(ERROR|FATAL)\b'
      before: 5
      after: 20
      priority: 50
    - id: pytest                 # shipped id -> only these fields change, the rest is inherited
      priority: 90

denoise:
  collapse_carriage_returns: true   # collapse \r progress bars to their final state
  dedupe_repeats: true              # repeated lines -> "<line>  (×47)"
  noise_patterns:
    - '^\s*$'

output:
  terminal: true
  markdown_path: report.md
  json_path: report.json
  mr_note: false                 # post/update a note on the MR/PR (needs a live pipeline)

redaction:
  enabled: true
  extra_patterns: []             # additional regexes to scrub
base_url defaults to the public host (gitlab.com /api.github.com). Override it for a self-hosted GitLab or GitHub Enterprise instance. Custom CA bundles and proxies are honoured either way.