Files
ersatztv/docker/Dockerfile
T
timothyandClaude Opus 5 8141ac7807 fix(887): the rule applied to one config family and not its sibling
Round 7. BLOCKER empty; three real holes, two of them introduced by round six's own
fixes, which is this repo's recorded `each-fix-round-introduces-an-adjacent-defect`
happening inside a single commit.

**`web/vite.config.js` outranks the pinned `.ts` for `vite build`.** Read from the pinned
tarball rather than remembered: vite 8.1.3's `DEFAULT_CONFIG_FILES` is
`["vite.config.js", "vite.config.mjs", "vite.config.ts", …]`. So a `vite.config.js` whose
plugin shells out to the suite runs it in the gitless stage — measured with all 1411
tests green. The governing rule was already written down one family over ("pinning one
config is worthless while a second can outrank it") and had been applied to
`vitest.config.*` and not to this one. The refusal now covers both families.

**The decoy fix counted one SPELLING of the marker.** `text.count("test: {")` is defeated
by writing the real block `test:  {` with two spaces beside a decoy that matches exactly:
count is 1, the comparison takes the decoy, and the live block filters out precisely the
specs #883 broke on. Now `re.finditer(r"\btest:\s*\{")`.

**The plugin pin added in that same commit shipped the identical decoy hole** it was
written next door to fix — a raw `text.count(PINNED_VITE_PLUGINS) == 1` with a decoy
above `defineConfig`. Both `vite.config.ts` pins now share ONE bracket walk and ONE
whitespace-tolerant uniqueness rule, so they cannot drift apart again.

PROSE, and this one is a false completion claim in my own previous commit message: I said
the `PUBLISH_ACTION`/anti-vacuity sentence and the singular "only an `ENV`" residual were
corrected. They were — in the record and the inventory row, and NOT in the guard
docstring, which is the artifact a code reader hits first. Both are now fixed there too,
the route COUNT is removed from the docstring and the record and kept in ONE place, and
the residual that stated its own false version before retracting it now states the
boundary once.

Also: the `--from=` branch never reached the JSON exec-form parser, so
`COPY --from=web-build ["/source/web", "/dest"]` left the receiving stage unpinned; the
revalidate arm of the gating `if:` is now described as a DEPENDENCY on
`ci-detect-already-validated.sh` (graded `MUTATION: NONE`) rather than as something
asserted here, since only the `docs_only` arm is; and the plugin-bodies residual now says
there are TWO plugins, `react()`'s being third-party and unmitigated.

Battery 64 -> 68, 0 missed. One of those four exists because the battery itself briefly
reported NOTHING and exited 0 after a bad splice deleted its `main()` — it now carries an
anti-vacuity assert on its own mutant count.

Refs: #887
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019T79beF1Ufid3dXju4yqkF
2026-08-30 15:34:31 +02:00

144 lines
8.0 KiB
Docker

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-amd64 AS dotnet-runtime
FROM node:22-bookworm-slim AS web-build
WORKDIR /source
COPY web/package*.json ./web/
WORKDIR /source/web
RUN npm ci
WORKDIR /source
COPY design-system/. ./design-system/
COPY web/. ./web/
WORKDIR /source/web
# THE VITEST SUITE IS NOT RUN IN THIS STAGE, AND MUST NOT BE RE-ADDED (ersatztv#887).
# This stage is gitless in both senses at once. It copies only `web/` and `design-system/`, so it
# holds no `.git` — a property of the STAGE, not of the context, which is the repository root
# (`context: .`) and whose `.dockerignore` does not exclude `.git`: the directory is there to be
# copied and simply is not. And `node:22-bookworm-slim` ships no git binary (`command -v git` -> not
# found), which is why a `COPY .git` would not help either. Members of the SPA suite need one or the
# other: `pageSizeCallSites.guard.test.ts` and `completeAnnotations.guard.test.ts` derive their file
# population from `git ls-files` and refuse to fall back to a directory walk
# (`testing.guard-derives-population-from-source`), and `trackedSourceFiles.realgit.test.ts` builds
# its own temp repository. (Not every `*.guard.test.ts` — `completeRequest.guard.test.ts` needs no
# git at all.)
#
# Running the suite here anyway therefore costs a hand-maintained list of the members that cannot
# run — and that list is a population nothing derives. It went stale the first time a guard was
# added without updating it, and because `Build & push image (amd64)` is `if: github.event_name !=
# 'pull_request'`, the resulting red is unreachable on a PR and lands on `main` and on the `v*` tag
# path: every image build failed and `:latest` stopped being republished. Excluding one more file
# re-arms that; the list is removed instead.
#
# WHAT STILL VALIDATES THE SUITE. `docker-build.yml`'s `test` job runs it UNFILTERED on a real
# checkout, and `build` — the job that invokes this Dockerfile — carries `needs: [test, migrations,
# scan]`, so an image cannot be published past a red suite. Two skips exist inside `test` and
# neither leaves an image unvalidated: `docs_only` also gates `Build and push`, so that arm ships
# nothing — and that arm IS asserted, by `test_the_DOCS_ONLY_arm_cannot_publish_an_image`. The #420
# revalidate skip fires only on a tree byte-identical to a head that already carried a green combined
# status; that arm is NOT asserted here — it rests on `scripts/ci-detect-already-validated.sh`, which
# carries no mutation proof of its own, so it is a dependency rather than a check. Held by
# `scripts/tests/test_image_build_delegates_the_spa_suite.py`. That guard PINS the command line
# below as text: any edit to it reddens, whatever the edit says. Update the pin in the same commit,
# and do not use it to re-add a suite run.
#
# `lint` and `typecheck` stay. They are gitless-safe with no member that is not, so they carry no
# list and no trap; whether the image build should run them at all is a separate question this
# does not answer.
RUN npm run lint && npm run typecheck && npm run build
FROM --platform=linux/amd64 192.168.1.95:3000/timothy/ersatztv-ffmpeg:8.1.2 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip && \
python3 -m pip install --target=/app/pythonlibs --no-cache-dir streamlink && \
apt-get clean -y && \
rm -rf /var/lib/apt/lists/*
# https://hub.docker.com/_/microsoft-dotnet
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble-amd64 AS build
RUN apt-get update && apt-get install -y ca-certificates gnupg default-jre-headless python3-pip
WORKDIR /source
# download the openapi-generator jar first so this layer is cached independently
# of the openapi spec dir below -- otherwise every spec change (e.g. v1.json)
# busts the layer and re-downloads the ~30MB jar (ersatztv#190)
RUN wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.15.0/openapi-generator-cli-7.15.0.jar
# generate openapi client
COPY ErsatzTV/wwwroot/openapi/. /app/ErsatzTV/wwwroot/openapi/
RUN java -jar openapi-generator-cli-7.15.0.jar generate -i /app/ErsatzTV/wwwroot/openapi/scripted-schedule.json -g python -o /app/etv-client --package-name etv_client
RUN rm -rf openapi-generator-cli-7.15.0.jar /app/ErsatzTV
RUN python3 -m pip install --target=/app/pythonlibs /app/etv-client
RUN rm -rf /app/etv-client
COPY scripts/scripted-schedules/. /app/scripted-schedules/
# copy csproj and restore as distinct layers
COPY *.sln .
# repo-wide build config (MSBuild props/targets incl. NuGet-audit warning
# exemptions, SDK pin, analyzer severities, and the Central Package Management
# version manifest) must be present before restore so the image build matches
# local/CI builds. Directory.Packages.props is REQUIRED here: with CPM the csproj
# carry no versions, so restore fails without the central manifest.
COPY Directory.Build.props Directory.Build.targets Directory.Packages.props global.json .editorconfig ./
COPY eng/analyzers/sdk-all-suggestion.globalconfig ./eng/analyzers/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
COPY ErsatzTV.Core.Nullable/*.csproj ./ErsatzTV.Core.Nullable/
COPY ErsatzTV.FFmpeg/*.csproj ./ErsatzTV.FFmpeg/
COPY ErsatzTV.Infrastructure/*.csproj ./ErsatzTV.Infrastructure/
COPY ErsatzTV.Infrastructure.Sqlite/*.csproj ./ErsatzTV.Infrastructure.Sqlite/
COPY ErsatzTV.Infrastructure.MySql/*.csproj ./ErsatzTV.Infrastructure.MySql/
COPY ErsatzTV.Scanner/*.csproj ./ErsatzTV.Scanner/
# Disable the persistent Roslyn/MSBuild compiler servers for the in-image build (ersatztv#406,
# server-management#604). The workflow sets these as env for the runner-side dotnet jobs, but this
# stage compiles inside `docker build`, so the workflow's env does NOT reach it — the restore and
# the two publishes below would otherwise spin up their own VBCSCompiler and hold its heap. This is
# the `build` job that server-management#570 measured pegging 5.999/6 GiB, so it is the one that
# most needs this.
#
# Placed here rather than at the top of the stage on purpose: an ENV invalidates every layer below
# it, and the wget of the ~30MB openapi-generator jar above is deliberately ordered early to stay
# cached (ersatztv#190). Nothing between that wget and this line compiles, so this is the earliest
# point where the ENV is free.
#
# Build-stage only: the final image is FROM runtime-base and only COPY --from=build /app (files,
# not ENV), so none of this lands in the shipped image or affects runtime.
ENV UseSharedCompilation=false \
DOTNET_CLI_USE_MSBUILD_SERVER=0 \
MSBUILDDISABLENODEREUSE=1
RUN dotnet restore -r linux-x64 ErsatzTV/ErsatzTV.csproj
# copy everything else and build app
COPY ErsatzTV/. ./ErsatzTV/
COPY --from=web-build /source/ErsatzTV/wwwroot/app/. ./ErsatzTV/wwwroot/app/
COPY ErsatzTV.Application/. ./ErsatzTV.Application/
COPY ErsatzTV.Core/. ./ErsatzTV.Core/
COPY ErsatzTV.Core.Nullable/. ./ErsatzTV.Core.Nullable/
COPY ErsatzTV.FFmpeg/. ./ErsatzTV.FFmpeg/
COPY ErsatzTV.Infrastructure/. ./ErsatzTV.Infrastructure/
COPY ErsatzTV.Infrastructure.Sqlite/. ./ErsatzTV.Infrastructure.Sqlite/
COPY ErsatzTV.Infrastructure.MySql/. ./ErsatzTV.Infrastructure.MySql/
COPY ErsatzTV.Scanner/. ./ErsatzTV.Scanner/
ARG INFO_VERSION="unknown"
ARG BUILD_CONFIG="release"
WORKDIR /source/ErsatzTV.Scanner
RUN dotnet publish ErsatzTV.Scanner.csproj -c ${BUILD_CONFIG} -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
WORKDIR /source/ErsatzTV
RUN sed -i '/Scanner/d' ErsatzTV.csproj
RUN dotnet publish ErsatzTV.csproj -c ${BUILD_CONFIG} -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
# final stage/image
FROM runtime-base
ENV FONTCONFIG_PATH=/etc/fonts
RUN fc-cache update
WORKDIR /app
COPY --from=build /app ./
ENV PYTHONPATH=/app/pythonlibs
ENV ETV_CONFIG_FOLDER=/config
ENV ETV_TRANSCODE_FOLDER=/transcode
ENV ETV_DISABLE_VULKAN=1
ENTRYPOINT ["./ErsatzTV"]