fix(887): the image build builds the SPA and does not test it

`docker/Dockerfile`'s web-build stage is gitless twice over — the build context is
`web/` + `design-system/` so there is no `.git`, and `node:22-bookworm-slim` ships no
git binary. Members of the SPA suite need one or the other, so running the suite there
required naming the ones that cannot run. That list was a population nothing derived:
#883 added a third member without updating the hand-written pair of `--exclude`s, and
because `Build & push image (amd64)` is `if: github.event_name != 'pull_request'` the
resulting red was unreachable on a PR. It landed on `main` and on the `v*` tag path
instead — every image build failed, `:latest` stopped being republished, and a release
cut would have failed at the image build.

Adding a third `--exclude` re-arms the trap, so the list is removed rather than
extended: the stage now lints, typechecks and BUILDS the SPA, and the suite runs once,
unfiltered, in `docker-build.yml`'s `test` job on a real checkout. `build` carries
`needs: [test, migrations, scan]`, so no image is published past a red suite.

`scripts/tests/test_image_build_delegates_the_spa_suite.py` holds both halves — the
negative one alone would be satisfied by deleting the `needs:` edge. Three populations,
all derived: tracked Dockerfiles and workflows from the git index, and which npm scripts
ARE the suite from `web/package.json` (so `test` is in and the Playwright `test:ui-e2e`
is out, with no exemption list). Publishing jobs come from the `docker/build-push-action`
step and the Dockerfile each builds from that step's own `file:` input, which is why
`ci-image.yml` is out of scope by derivation rather than by an entry that would outlive
its reason.

Four mutants witnessed red, each by the intended test: a filtered suite run put back
into the Dockerfile, the `needs:` edge deleted, and the gating run narrowed in both the
block and the single-line `run:` step forms.

Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
This commit is contained in:
2026-08-30 13:37:58 +02:00
co-authored by Claude Opus 5
parent cf5f42edf9
commit febaad77d7
9 changed files with 858 additions and 38 deletions
@@ -13,10 +13,11 @@ import { type GitRunner, resolveTrackedSourceFiles } from './trackedSourceFiles'
* plugin's refusal. Reading the two exclusions as checkout-versus-binary is what broke the Docker
* stage once already; see `docker/Dockerfile`, which retracts that framing.
*
* `docker/Dockerfile`'s web-build stage runs on `node:22-bookworm-slim`, which ships no git at all,
* so this file is excluded there alongside the guard. Keeping them apart means
* that exclusion costs two files rather than the thirteen injected-runner tests in
* `trackedSourceFiles.test.ts`, which run fine with no git and no repository.
* Keeping these apart from `trackedSourceFiles.test.ts` confines the git prerequisite to one file:
* its thirteen injected-runner siblings run with no git and no repository. The suite as a whole is
* therefore run only where git is present. `docker/Dockerfile`'s web-build stage is not such a
* place — `node:22-bookworm-slim` ships no git at all — and since ersatztv#887 it does not run the
* suite there rather than naming the files that cannot run, which was a list nothing derived.
*
* Do NOT make these skip when git is missing. A check that answers "could not tell" by passing is a
* permanent no-op, which is the whole failure mode this plugin exists to avoid.