Skip to content

fix(ci): the container gate watched the Dockerfiles and not what they install - #370

Merged
ibuilder merged 1 commit into
mainfrom
fix/container-gate-manifests-2026-08-28
Aug 28, 2026
Merged

fix(ci): the container gate watched the Dockerfiles and not what they install#370
ibuilder merged 1 commit into
mainfrom
fix/container-gate-manifests-2026-08-28

Conversation

@ibuilder

Copy link
Copy Markdown
Owner

docker-scope decides whether a PR builds the container images, from a regex over the changed
files. It matched Dockerfile, .dockerignore and ci.yml — and nothing else. But a Dockerfile is
not the only build input a Dockerfile has:

services/api/Dockerfile   COPY services/api/requirements.lock …
                          RUN  pip install --require-hashes -r /tmp/req/requirements.lock
apps/web/Dockerfile       COPY package.json package-lock.json ./
                          COPY apps/web/package.json …

A dependency change replaces what the image contains without touching a line the filter watches,
so Container build (PR, no push) reports skipping — which renders green.

Every pip and npm bump has merged that way, including the five web toolchain bumps of #358#364,
the python floors of #368, and the anthropic 1.x lock recompile of #369 an hour ago. None of those
greens had built the image whose dependency set it had just replaced.

This is the hole test_container_pr_gate.py exists to close, arriving through the input the filter
was not watching rather than the one it was — a skipped job is not a passed job, and it reads
identically either way.

It is also the converter gap from that same file, one level out. That was a Dockerfile outside the
matrix; this is a build input that is not a Dockerfile at all, so it sat outside the population by
kind rather than by omission — which is why rule 3's converse, added specifically to catch "a
file nobody listed", could not reach it either.

The fix

Rule 5 derives the check rather than listing it: read each matrix Dockerfile's own COPY lines,
keep the sources whose basename is a dependency manifest, and assert the extracted filter matches
every one. --from= copies are skipped — those come out of an earlier build stage, not the build
context, so they are not PR-visible paths.

It finds four today, and the day an image starts copying a new lock file that path is covered
without editing the test:

PASS  the matrix Dockerfiles declare dependency manifests to check at all
      apps/web/package.json, package-lock.json, package.json, services/api/requirements.lock
PASS  ...and the filter matches every manifest an image is actually built from   all 4 matched

Mutation-checked by reverting the filter to its previous value — it fails and names them:

FAIL  ...and the filter matches every manifest an image is actually built from
      apps/web/package.json (builds web), package-lock.json (builds web), package.json (builds web),
      services/api/requirements.lock (builds api) -- a dependency bump there reports `skipping` and
      never builds the image it changed

The decoy assertion still passes, so the widened filter has not become one that matches everything
and quietly makes the touched=false branch dead code.

Self-verifying

This PR edits ci.yml, which the filter already matched, so the container jobs run here — you
should see Container build (PR, no push) actually build all three images rather than skip.

🤖 Generated with Claude Code

… install

`docker-scope` decides whether a PR builds the container images, from a regex over the changed
files. It matched `Dockerfile`, `.dockerignore` and `ci.yml` — and nothing else. But a Dockerfile is
not the only build input a Dockerfile has:

    services/api/Dockerfile   COPY services/api/requirements.lock …
                              RUN  pip install --require-hashes -r /tmp/req/requirements.lock
    apps/web/Dockerfile       COPY package.json package-lock.json ./
                              COPY apps/web/package.json …

So a dependency change replaces what the image contains without touching a line the filter watches,
and `Container build (PR, no push)` reports **skipping** — which renders green. Every pip and npm
bump has merged that way: the five web toolchain bumps of #358#364, the python floors of #368, and
the anthropic 1.x lock recompile of #369 an hour ago. Not one of those greens had built the image
whose dependency set it had just replaced.

This is the same hole `test_container_pr_gate.py` was written to close, arriving through the input
the filter was not watching rather than the one it was — **a skipped job is not a passed job**, and
it reads identically either way.

It is also the same shape as the converter gap that file already records, one level out. That was a
Dockerfile outside the matrix; this is a build input that is not a Dockerfile at all, so it sat
outside the population by KIND rather than by omission — which is why rule 3's converse, added
specifically to catch "a file nobody listed", could not reach it either. **A gate's population is
part of its claim.**

Rule 5 derives the check instead of listing it: read each matrix Dockerfile's own `COPY` lines, keep
the sources whose basename is a dependency manifest, and assert the extracted filter matches every
one. It finds four today — `package.json`, `package-lock.json`, `apps/web/package.json`,
`services/api/requirements.lock` — and the day an image starts copying a new lock file, that path is
covered without editing the test. `--from=` copies are skipped: those come out of an earlier stage,
not the build context, so they are not PR-visible paths.

Mutation-checked by reverting the filter to its previous value: rule 5 fails and names all four
manifests with the image each one builds, rather than failing anonymously. The decoy assertion still
passes, so the widened filter has not become one that matches everything and quietly makes the
`touched=false` branch dead code.

This commit edits `ci.yml`, which the filter already matched, so the container jobs run on its own
PR — the change verifies itself on the way in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@strix-security

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

So far, Strix has reviewed 28 pull requests, surfaced 3 security issues (1 critical/high) and blocked 2 risky merges across this workspace.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 21 minutes.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cffeb46e-2bc3-4780-8aa4-9e4ed72c234e

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6131c and 1378e78.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • services/api/test_container_pr_gate.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ibuilder
ibuilder merged commit 365013c into main Aug 28, 2026
13 checks passed
@ibuilder
ibuilder deleted the fix/container-gate-manifests-2026-08-28 branch August 28, 2026 14:18
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