How compose-lint works¶
compose-lint grades the configuration Compose would actually run, then turns the result into an exit code. If it was meant to read part of the stack and could not, the run fails. A pass always means everything was graded.
A check run¶
flowchart TB
subgraph project["Inside the repository (the checkout)"]
direction LR
compose["compose.yml"]
overlay["compose.override.yml<br/>or COMPOSE_FILE"]
env[".env"]
refs["include: and<br/>extends: targets"]
envfile["env_file:<br/>targets"]
policyfile[".compose-lint.yml"]
end
parse["<b>Merge and parse</b><br/>overlays merged in Compose's order<br/>${VAR:-default} resolved from .env<br/>YAML loaded with line numbers"]
rules["<b>Rules</b><br/>every rule runs on every service<br/>each finding: rule, severity, file:line, fix"]
policy["<b>Policy</b><br/>disabled rules reported as suppressed<br/>severity overrides, excluded services"]
report["<b>Report</b><br/>text, JSON or SARIF on stdout"]
code{"exit code"}
gap(["exit 2: coverage gap<br/>a referenced file is outside<br/>the repository or unreadable"])
usage(["exit 2: usage error<br/>invalid Compose"])
compose & overlay & env & refs & envfile -->|read| parse
project -.-> gap
parse -.-> usage
parse -->|one merged document| rules
rules -->|findings| policy
policyfile -->|rule policy| policy
policy --> report --> code
code -->|"0: nothing at or above the threshold"| pass(["pass"])
code -->|"1: findings at or above the threshold"| fail(["fail"])
classDef stop stroke:#c2410c,stroke-width:2px
class gap,usage stop
- Inputs. The file you name, or the one found in the working directory, plus
everything
docker compose upwould read with it: the sibling override, the sibling.env,include:and cross-fileextends:targets, andenv_file:targets. Each must resolve inside the project. No registry, Docker daemon or image is ever contacted. What a run reads has the full list and the flags that switch each source off. - Merge and parse. Overlays are merged in the order Compose uses, and
${VAR:-default}resolves to what Compose would deploy. Line numbers are kept so every finding points at the line to change. - Rules. Every rule runs against every service. A rule's severity is derived from a fixed model, not picked per rule; see Severity levels.
- Policy.
.compose-lint.ymlcan disable a rule, override its severity, or exclude a service. A disabled rule's findings are still reported, marked suppressed with your reason. There are no inline suppression comments. - Report and exit. Findings go to stdout as text, JSON or SARIF. The run
exits
1if any finding is at or above--fail-on(defaulthigh),0otherwise.
The dashed paths are what makes the result safe to gate on. A part of the stack
compose-lint could not read is a coverage gap, and a coverage gap exits 2, the
same as a usage error. It is never reported as a pass. The exit codes are part of
the compatibility contract.
Where fix branches off¶
flowchart LR
findings["Unsuppressed findings"] --> q{"One correct value, in one place,<br/>with no anchors or ${VAR} there?"}
q -->|no| manual["Reported for manual review"]
q -->|yes| edit["Edit, then re-parse and<br/>re-lint the merged result"]
edit -->|would not round-trip| refused["Refused, diff shown"]
edit -->|clean| out["Dry run prints the diff<br/>apply mode writes it in place"]
compose-lint fix edits only findings with exactly one right answer, and checks
each edit by linting the merged result before anything is written. A dry run is
the default. --apply writes with an atomic swap, and every edit that changes
how the stack behaves is labelled. Fixing findings is the full
contract.