Skip to content

fix(linting): stop the lint checks failing open on Compose output - #89

Merged
owine merged 1 commit into
mainfrom
fix/harden-compose-output-parsing
Sep 19, 2026
Merged

owine merged 1 commit into
mainfrom
fix/harden-compose-output-parsing

Conversation

@owine

@owine owine commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Why now

The self-hosted runners already run Compose v5.5.1 (verified on piwine). Every Compose-touching job in deploy.yml is self-hosted — :67, :258, :647, :742, :760 — while lint runs hosted on ubuntu-24.04 with 2.38.2. So the two sides already validate with different engines; moving the hosted runners to 26.04 closes that gap rather than opening one.

Before doing that, the output parsers had to be sound. Two were not.

1. validate-image-platforms.sh failed open

done < <(docker compose ... config --images 2>/dev/null | sort -u)

if [[ ${#IMAGES[@]} -eq 0 ]]; then
  echo "ℹ️  No images resolved — nothing to check."
  exit 0     # ← PASS
fi

2>/dev/null discarded the diagnostics and the exit status. Any failure to resolve the compose file produced an empty list, which the zero-images branch reported as a pass — a check that verified nothing, reporting success. This was the one place a Compose change could break things silently rather than loudly.

Measured on an unparseable fixture:

exit output
before 0 (silent pass) No images resolved — nothing to check.
after 1 ✗ 'docker compose config --images' failed + the parse error

Zero images is still legitimately a pass for a build-only or service-less file, so the empty case is discriminated with config --services instead of being failed outright.

2. validate-stack.sh — set -e swallowed the entire report

Under set -euo pipefail (line 14) a bare wait on a failing child aborts before DOCKER_EXIT=$?, and before every formatted block below it. A stack that failed validation printed its raw tee'd output and then died — no "Issues found", no fix hint, no overall status.

CI verdicts were always correct (the job still exited non-zero), which is why this went unnoticed — only the diagnostics were lost. It also meant the warning-filter block was dead code on every path that uses it, since it only runs when Compose fails.

After, on the same fixture:

🐳 DOCKER COMPOSE VALIDATION (docker compose config)
❌ FAILED - Docker Compose validation detected issues in ./badcfg/compose.yaml:
🔍 Issues found:
    invalid containerPort: notaport
🛠️  Fix locally with:
    docker compose -f badcfg/compose.yaml config
💥 OVERALL STATUS: VALIDATION FAILED

3. Warning filters didn't match Compose 5.x

2.x:  WARNING: The "TZ" variable is not set. Defaulting to a blank string.
5.x:  time="..." level=warning msg="The \"TZ\" variable is not set. ..."

The filters matched WARNING case-sensitively, so under 5.x every missing-variable warning would leak into the displayed error block. Replaced the three chained greps with one case-insensitive match on the bare token warning — a substring of both spellings, so there's no alternation to keep in sync as formats drift again. The || cp fallback behaviour is unchanged.

Still narrow, not a blanket suppressor — WARNING: Found orphan containers is correctly retained, since it matches none of the three phrases.

Not changed, and why

A missing stack initially looked like a third fail-open: lint-summary.sh printed ERROR: Stack file not found and still reported ALL STACKS PASSED. It isn't one. lint-summary.sh takes --lint-result as an input from the upstream per-stack matrix job; it reports, it doesn't decide. Passing it --lint-result success for a nonexistent stack feeds it a false premise. validate-stack.sh, which actually decides, fails correctly on a missing stack. Left alone.

Validation

Against Compose v5.5.1 locally and on piwine:

  • shellcheck clean on all three scripts.
  • validate-stack.sh: broken stack → 1, healthy stack (dozzle) → 0.
  • validate-image-platforms.sh: real digest-pinned stack → PASSED (2 verified, 0 skipped); unparseable fixture → 1.
  • Filter drops both warning spellings, keeps real errors and unrelated warnings.
  • test-classify-rollback-scope.sh and test-detect-stack-changes.sh pass. (test-workflow.sh exits 1 printing usage when given no args, on main too — not a regression.)

Next

With these sound, the ubuntu-24.04 hold from #87 can be lifted — its stated exit condition, "once the self-hosted runners have been upgraded", turned out to have been satisfied before it was written. That's a separate PR.

Summary by Sourcery

Harden Compose linting so parser failures are reported clearly and validation checks cannot pass without verifying their inputs.

Bug Fixes:

  • Make image-platform validation fail when Compose cannot resolve the file or returns no images for declared services, instead of silently passing.
  • Preserve formatted validation diagnostics and status guidance when Docker Compose validation fails.
  • Support filtering of environment-variable warnings across Compose output formats while retaining unrelated warnings and errors.

Prep for moving the hosted runners to ubuntu-26.04. The self-hosted
runners already run Compose v5.5.1, so lint (hosted, 2.38.2) and deploy
(self-hosted, 5.5.1) validate with different engines today; the migration
closes that gap. These are the parsers that had to be sound first - and
two of them were not.

validate-image-platforms.sh: `config --images` was piped into the read
loop with `2>/dev/null`, discarding both the diagnostics and the exit
status. Any failure to resolve the compose file produced an empty image
list, which the zero-images branch then reported as a PASS - a check that
verified nothing, reporting success. Verified before/after on an
unparseable fixture: exit 0 (silent pass) -> exit 1 with the parse error
surfaced. Zero images is still a legitimate pass when the file declares
no services, so the empty case is discriminated with `config --services`
rather than failed outright.

validate-stack.sh: under `set -e`, a bare `wait` on a failing child
aborted the script before `DOCKER_EXIT=$?` and before the whole formatted
report. A stack that failed validation printed raw output and died - no
"Issues found", no fix hint, no status line. CI verdicts were always
correct (the job still exited non-zero), which is why it went unnoticed;
only the diagnostics were lost. This also made the filter block below it
dead code on every path that uses it.

validate-stack.sh + lint-summary.sh: the env-var warning filters matched
`WARNING` case-sensitively. Compose 5.x emits `level=warning msg="..."`
instead, so every missing-variable warning would leak into the displayed
error block. Matching the bare token `warning` case-insensitively covers
both spellings.

Verified against Compose v5.5.1: shellcheck clean; broken stack -> 1,
healthy stack -> 0, image platform check passes 2/2 against a real
digest-pinned stack; filter drops both warning formats while keeping real
errors and unrelated warnings (`WARNING: Found orphan containers` is
retained, so it is not a blanket suppressor).

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @owine, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 21 hours and 12 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

The lint scripts now fail closed on unparseable or structurally unexpected Compose output, preserve the complete formatted failure report under set -e, and filter environment-variable warnings across Compose 2.x and 5.x formats without suppressing unrelated warnings.

Sequence diagram for preserved Compose validation failure reporting

sequenceDiagram
    participant Script as validate-stack.sh
    participant YAML as YAML validator
    participant Compose as docker compose
    participant Output as Formatted report

    Script->>YAML: validate stack YAML
    Script->>Compose: config
    Compose-->>Output: tee raw output
    Script->>YAML: wait YAML_PID
    YAML-->>Script: exit status
    Script->>Compose: wait DOCKER_PID
    Compose-->>Script: exit status
    Script->>Script: grep -viE warning filters
    Script->>Output: print issues, fix hint, overall status
Loading

Flow diagram for fail-closed Compose image validation

flowchart TD
    A["Run docker compose config --images"] --> B{Command succeeds?}
    B -- No --> C["Print Compose diagnostics"]
    C --> D["Exit 1"]
    B -- Yes --> E["Collect unique image references"]
    E --> F{Images resolved?}
    F -- Yes --> G["Verify image platforms"]
    G --> H["Report validation result"]
    F -- No --> I["Run config --services"]
    I --> J{Services declared?}
    J -- Yes --> K["Reject unexpected empty output"]
    K --> D
    J -- No --> L["Pass: no services to check"]
Loading

File-Level Changes

Change Details Files
Make image-platform validation fail closed when Compose output cannot be resolved.
  • Capture image-list stdout and stderr separately and check the Compose command exit status.
  • Report Compose diagnostics and fail on resolution errors.
  • Distinguish valid zero-image files from files declaring services with no resolved images.
scripts/linting/validate-image-platforms.sh
Preserve formatted validation diagnostics when either child process fails.
  • Capture nonzero wait statuses without triggering set -e before report generation.
  • Allow the existing failure summary, fix hint, and overall status to run for failed YAML or Compose validation.
scripts/linting/validate-stack.sh
Update Compose warning filtering for both legacy and current output formats.
  • Use a case-insensitive regex matching the warning token and known environment-variable warning phrases.
  • Retain the fallback that displays raw output when filtering removes everything.
scripts/linting/lint-summary.sh
scripts/linting/validate-stack.sh

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@owine
owine merged commit 3d95f44 into main Sep 19, 2026
3 checks passed
@owine
owine deleted the fix/harden-compose-output-parsing branch September 19, 2026 18:36
owine added a commit that referenced this pull request Sep 19, 2026
The hold's exit condition was already met when it was written. It said to
remove the rule "once the SELF-HOSTED runners have been upgraded to a
Compose 5.x image" - they run v5.5.1, verified on piwine, and have for
some time.

So the premise was inverted. The hold was written to stop lint and deploy
validating with different Compose engines, but that split is the CURRENT
state: every Compose-touching job in deploy.yml is self-hosted on 5.5.1
(:67, :258, :647, :742, :760), while lint runs hosted on ubuntu-24.04
with 2.38.2. Keeping the hold perpetuates the mismatch; moving the hosted
runners to 26.04 closes it.

A detail that shows how invisible this was: the 2 literal `runs-on:`
values Renovate could already see are deploy.yml's notify job and
workflow-lint.yml - neither runs Compose at all. The 3 labels that do
feed `docker compose config` were the ones it could not see, until #88.

Compose 5.x compatibility was checked directly rather than assumed:
`config --images` still emits one resolved ref per line on stdout;
`ps -a --format json` is still NDJSON, so deploy.yml's `jq -s` health
gate parses correctly; verdicts remain exit-code driven. The parsers that
were genuinely fragile are fixed in #89.

With #88 in place Renovate sees all 5 labels, so this produces one PR
that moves them together rather than the partial migration #87 was
guarding against.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant