Build ErsatzTV Image / CI toolchain image resolves (push) Successful in 9s
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 21s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m52s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 6m23s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m50s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 5m5s
The guard asserted EXACT completeness over a population enumerated by a directory walk, so an untracked .ts/.tsx under web/src/ entered it and failed as unregistered on that developer's checkout while CI — which only ever checks out tracked files — stayed green. The glob still supplies file CONTENT; the POPULATION is now the git index, read by web/vite-plugins/trackedSourceFiles.ts in Vite's own Node context and handed to the app project as a virtual module. That reaches the index without admitting @types/node to tsconfig.app.json, the obstacle that deferred this in #818. Three mechanisms carry the proof, each added because the previous was measured insufficient: a closed-form restatement of the shared scope predicate (sharing no helper at any depth with what it checks); a second independent `ls-files --others` query cross-checking the population; and real-git tests that execute the derivation against a temp repository. Six residuals are stated with their MEASURED fail-directions, and testing.guard-derives-population-from-source gains a bounded exception plus the closed-form criterion. fixes #819 Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
140 lines
7.7 KiB
Docker
140 lines
7.7 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 SPA suite runs here except for two files (ersatztv#819), excluded for OVERLAPPING reasons —
|
|
# one needs the git binary, the other needs the binary AND a checkout. Reading them as two separate
|
|
# reasons is what broke this stage once already:
|
|
# * `web/src/api/pageSizeCallSites.guard.test.ts` needs a git CHECKOUT — and, through it, the
|
|
# binary. It derives its file population from `git ls-files` rather than a directory walk, and
|
|
# refuses rather than falling back. This stage's context is `web/` + `design-system/` only, so
|
|
# there is no `.git`.
|
|
# * `web/vite-plugins/trackedSourceFiles.realgit.test.ts` needs the BINARY but no checkout: it
|
|
# builds its own temp repository to prove the derivation by executing it.
|
|
# `node:22-bookworm-slim` ships no git (`command -v git` -> not found), so it dies with
|
|
# `spawnSync git ENOENT`.
|
|
# So this is NOT checkout-versus-binary, and adding a `COPY .git` would not let either run here —
|
|
# the binary would still be missing. Excluding only the first is not enough either, and a replica
|
|
# that merely deletes `.git` cannot show that: verify any change here with the git binary off
|
|
# `PATH`, not just with the directory absent.
|
|
# Everything else — all but those two files — runs fine gitless and is kept, rather than dropping
|
|
# the whole suite for one file as an earlier fix here did.
|
|
# The excluded pair is not skipped overall: `docker-build.yml`'s `Build & test (.NET)` job runs the
|
|
# whole suite on a real checkout, and `build` (the job that invokes this Dockerfile) carries
|
|
# `needs: [test, migrations, scan]`. State that chain precisely, because the `needs:` edge is not
|
|
# all of it: `Test SPA` is also gated on `docs_only` and on the #420 revalidate skip, and `build` is
|
|
# not gated on `revalidate`. On a push whose tree is byte-identical to an already-green head the
|
|
# suite is skipped and the image still builds — carried there by #420's byte-identical-tree
|
|
# argument. The `docs_only` arm cannot ship an image at all (`Build and push` is gated on it too),
|
|
# and `ci-detect-docs-only.sh` classifies by PATH SHAPE (`docs/` or `*.md`), not by directory, so a
|
|
# `web/*.md` would count as docs — there are none today, but do not restate it as "any `web/**`".
|
|
RUN npm run lint && npm run typecheck && \
|
|
npm test -- --run \
|
|
--exclude 'src/api/pageSizeCallSites.guard.test.ts' \
|
|
--exclude 'vite-plugins/trackedSourceFiles.realgit.test.ts' && \
|
|
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"]
|