# HyperFrames Producer - Regression Test Image # # Matches the production rendering environment (same Chromium, fonts, FFmpeg) # but includes the full source + devDependencies for running the test harness. # # This ensures golden baselines match what production actually renders. # # Usage: # docker build -f Dockerfile.test -t hyperframes-producer:test . # docker run --rm -v ./packages/producer/tests:/app/packages/producer/tests hyperframes-producer:test # docker run --rm -v ./packages/producer/tests:/app/packages/producer/tests hyperframes-producer:test --update FROM node:22.23.3-bookworm-slim # ── System dependencies (identical to production) ──────────────────────────── RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ unzip \ xz-utils \ chromium \ libgbm1 \ libnss3 \ libatk-bridge2.0-0 \ libdrm2 \ libxcomposite1 \ libxdamage1 \ libxrandr2 \ libcups2 \ libasound2 \ libpangocairo-1.0-0 \ libxshmfence1 \ libgtk-3-0 \ # Font support — matches production fonts-liberation \ fonts-noto-color-emoji \ fonts-noto-cjk \ fonts-noto-core \ fonts-noto-extra \ fonts-noto-ui-core \ fonts-freefont-ttf \ fonts-dejavu-core \ fontconfig \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean \ && fc-cache -fv # ── FFmpeg (pinned) ────────────────────────────────────────────────────────── # One pinned static build (BtbN n8.0.1) instead of Debian's apt ffmpeg, so # regression baselines and the shipped images use the same FFmpeg version. # Bump the URL and sha256 together, and keep in sync with the other Dockerfiles # and .github/actions/install-ffmpeg-linux/action.yml. ARG TARGETARCH ARG FFMPEG_RELEASE=https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-02-28-12-59 ARG FFMPEG_BUILD=ffmpeg-n8.0.1-66-g27b8d1a017 ARG FFMPEG_SHA256_AMD64=26d2ecd0cefba99f9d1ea92252a3a4f743a58df4c1c3cf9f8bbaaa40b1fca5b0 ARG FFMPEG_SHA256_ARM64=d72cc324cb65dd92a9ffe699cc95ccbacfef4def8d3ad9fe2b5164a6a71b9595 RUN case "$TARGETARCH" in \ amd64) FF_ARCH=linux64; FF_SHA256="$FFMPEG_SHA256_AMD64" ;; \ arm64) FF_ARCH=linuxarm64; FF_SHA256="$FFMPEG_SHA256_ARM64" ;; \ *) echo "unsupported TARGETARCH '$TARGETARCH'" >&2; exit 1 ;; \ esac \ && curl -fsSL -o /tmp/ffmpeg.tar.xz "$FFMPEG_RELEASE/$FFMPEG_BUILD-$FF_ARCH-gpl-8.0.tar.xz" \ && echo "$FF_SHA256 /tmp/ffmpeg.tar.xz" | sha256sum -c - \ && tar -xJf /tmp/ffmpeg.tar.xz -C /usr/local/bin --strip-components=2 --wildcards '*/bin/ffmpeg' '*/bin/ffprobe' \ && rm /tmp/ffmpeg.tar.xz \ && ffmpeg -version | grep -F "ffmpeg version ${FFMPEG_BUILD#ffmpeg-}" \ && ffprobe -version | grep -F "ffprobe version ${FFMPEG_BUILD#ffmpeg-}" # Use system Chromium (same as production) ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV CONTAINER=true # Install chrome-headless-shell for deterministic BeginFrame rendering. # This lightweight Chrome binary supports HeadlessExperimental.beginFrame. # Install to ~/.cache/puppeteer/ where resolveHeadlessShellPath() looks. # # Pinned to a specific build (NOT @stable) so the regression-test golden # baselines in packages/producer/tests/*/output/output.mp4 stay reproducible. # Each Chrome stable bump shifts pixel output enough to fail PSNR. Bump this # version together with regenerating baselines via `docker:test:update`. RUN npx --yes @puppeteer/browsers install chrome-headless-shell@148.0.7778.167 \ --path /root/.cache/puppeteer \ && find /root/.cache/puppeteer/chrome-headless-shell -name "chrome-headless-shell" -type f \ && echo "chrome-headless-shell installed" WORKDIR /app # Install bun RUN curl -fsSL https://bun.sh/install | BUN_INSTALL="/root/.bun" bash -s "bun-v1.3.13" ENV PATH="/root/.bun/bin:$PATH" # Install dependencies (full, including devDependencies for tsx + test harness). # Every workspace member (packages/*) must be COPYed here — `bun install # --frozen-lockfile` treats any member missing from the build context as a # lockfile change and fails. COPY package.json bun.lock ./ COPY packages/parsers/package.json packages/parsers/package.json COPY packages/lint/package.json packages/lint/package.json COPY packages/studio-server/package.json packages/studio-server/package.json COPY packages/core/package.json packages/core/package.json COPY packages/engine/package.json packages/engine/package.json COPY packages/player/package.json packages/player/package.json COPY packages/producer/package.json packages/producer/package.json COPY packages/cli/package.json packages/cli/package.json COPY packages/studio/package.json packages/studio/package.json COPY packages/shader-transitions/package.json packages/shader-transitions/package.json COPY packages/aws-lambda/package.json packages/aws-lambda/package.json COPY packages/gcp-cloud-run/package.json packages/gcp-cloud-run/package.json COPY packages/sdk/package.json packages/sdk/package.json COPY packages/sdk-playground/package.json packages/sdk-playground/package.json COPY packages/inspector/package.json packages/inspector/package.json # A corrupt tarball extraction (e.g. run 35468046055: "Fail extracting tarball # for aws-cdk-lib") can print an error and then hang instead of exiting, once # eating a full 60-minute job timeout. Bound each attempt and retry once. RUN timeout 300 bun install --frozen-lockfile \ || timeout 300 bun install --frozen-lockfile # Copy source in dependency order, running each build as soon as its own # inputs are present. # # Every package used to be copied here before any build ran, which put the # `COPY packages/producer/` layer above the core build. Docker invalidates # every layer below a changed one, so a producer-only change rebuilt core — # which cannot depend on producer. On CI run 30229469233 those two layers # cost 86s and 63s of the ~4m image build, in all 8 shards, on every PR. # Locally the same producer-only rebuild goes from 18s to 1s after this split. # # Keep the ordering dependency-correct: anything the core build reads must be # copied above it, and packages nothing above depends on stay below. # Core's build runs the shared post-emit import rewrite from the repo root. COPY scripts/rewrite-relative-import-extensions.mjs scripts/rewrite-relative-import-extensions.mjs COPY packages/parsers/ packages/parsers/ COPY packages/lint/ packages/lint/ COPY packages/studio-server/ packages/studio-server/ COPY packages/core/ packages/core/ # Build workspace packages so "node" export conditions resolve to built dist RUN bun run --filter '@hyperframes/{parsers,lint,studio-server}' build \ && bun run --cwd packages/core build # Build core runtime artifacts (needed by renderer) RUN bun run --filter @hyperframes/core build:hyperframes-runtime:modular # Nothing above reads these, so they land after the core build to keep it cached. COPY packages/engine/ packages/engine/ COPY packages/producer/ packages/producer/ WORKDIR /app/packages/producer # Skip fixtures tagged `transparency` (Chrome alpha-channel PSNR quirks not # reproducible on the CI image), `webgpu` (needs a hardware adapter; CI has # only swiftshader, which cannot render canvas WebGPU) and # `field-signal-reproducer` (known-broken # fixture per PR #2512 — codifies a real bug awaiting a fix). Mirrors the # `--exclude-tags` in the local `test:regression*` scripts in # packages/producer/package.json so `bun run docker:test*` and the aws-lambda # smoke tests skip the same set. Docker CMD args from `docker run ...` # are appended after these flags, so positional test names (e.g. shard args # `hdr-regression style-5-prod ...` in .github/workflows/regression.yml) still # resolve normally; the harness applies excludeTags whether testNames is empty # or non-empty (see discoverTestSuites in src/regression-harness.ts). ENTRYPOINT ["bunx", "tsx", "src/regression-harness.ts", "--", "--sequential", "--exclude-tags", "transparency,field-signal-reproducer,webgpu"]