From f89f0407aed4015ed98092e67a9b190846522591 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 22:27:32 +0000 Subject: [PATCH] =?UTF-8?q?ci:=20image=20build=20needs=20no=20outbound=20n?= =?UTF-8?q?et=20=E2=80=94=20copy=20job-installed=20node=5Fmodules;=20base?= =?UTF-8?q?=20alpine=E2=86=92slim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 4 +++- .gitea/workflows/ci.yml | 8 ++++++-- CHANGELOG.md | 1 + Dockerfile | 31 +++++++++++++------------------ 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.dockerignore b/.dockerignore index 297b657..47be514 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ -node_modules +# node_modules is intentionally IN the build context: the Dockerfile copies it straight +# into the image (deps are installed by the CI job / dev host, which have working network; +# the runner's dind daemon has broken outbound DNS for build containers). .next data/*.db # game state DB (v0.6.0): host-side, volume-mounted at runtime — never baked in diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 98853a3..4fce476 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -36,8 +36,7 @@ jobs: with: node-version: 22 - # bun drives installs (repo ships bun.lock, no package-lock.json); pinned to the - # same major as oven/bun in the Dockerfile. + # bun drives installs (repo ships bun.lock, no package-lock.json); pinned to 1.4.2. # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). - name: Install bun run: | @@ -95,6 +94,11 @@ jobs: - name: Install browsers (local) run: npx playwright install --with-deps chromium + # Cache the base image on the shared daemon up front: a single loud failure here + # beats a 6-minute BuildKit "failed to solve" when the daemon's outbound net is down. + - name: Pull e2e base image + run: docker pull node:22-slim + # Pre-build the e2e image with retries (flaky runner network); the E2E step's own # build then becomes a fast no-op. Plain progress keeps BuildKit output visible. - name: Build e2e image (retries) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0baed..f326c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `AGENTS.md`: "Change process" + "E2E tests" rewritten for the PR workflow — feature branch off master → commits with CHANGELOG entry + version bump → PR to master → CI (build→unit→e2e) green → merge → tag on master → manual production deploy via `docker compose up -d --build`; no direct pushes to master, CI never deploys - **CI build reliability + caching** (after first dind runs): the e2e image is now built with `--progress=plain` before `up -d` so BuildKit step output lands in job logs instead of being hidden by auto progress; Dockerfile bun installs use a BuildKit cache mount (`/root/.bun/install/cache`) that persists on the runner's shared daemon, and CI restores/saves node_modules + the bun cache via actions/cache around dependency install (both steps `continue-on-error` — safe while the runner's cache server is being fixed) - **CI flaky-network handling settled on retry wrappers** (transient EAI_AGAIN failures seen in runs #10/#11): bun install, dependency install and the e2e image build each retry up to 3× with 5s between attempts before failing — run #12 with this setup got furthest (build + unit + browsers green). A short-lived "DNS diagnostics" step was tried but removed: its daemon-side probe pulled an image not present on the runner's docker daemon and stalled runs #13/#14 at Setup Node +- **E2e image build needs no outbound network from dind build containers** (DNSResolveFailed in `RUN bun install` across runs #10/#12/#15): node_modules is now installed in the CI job / dev host and copied into the build context (.dockerignore no longer excludes it; Dockerfile COPYs it instead of running any install inside the image); base images switched alpine → node:22-slim (glibc) for native-module ABI compatibility with the job-installed tree (@next/swc-*, sharp are prebuilt per platform — CI job and prod host are x86_64 glibc linux); a "Pull e2e base image" step fails fast if the daemon can't fetch node:22-slim - Verified: unit suite 234 green locally; e2e not run locally by design (would deploy to production) — first real pass happens in the PR's CI run ## v0.12.2 — 2026-09-18 14:06 diff --git a/Dockerfile b/Dockerfile index 37ffb69..6f8447b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,14 @@ -# Multi-stage build: bun for installs (fast), node for build + runtime. -# The bun install steps use a BuildKit cache mount (/root/.bun/install/cache, both stages -# run as root) so the package cache persists across builds on daemons that keep named -# caches — notably the CI runner's shared dind daemon; where it doesn't persist it costs nothing. -FROM oven/bun:1.4.2-alpine AS deps +# Two-stage build needing ZERO outbound network at image-build time: +# (a) deps are installed by the CI job / dev host (`bun install`) and copied in via the +# build context — the runner's dind daemon has broken outbound DNS for build containers, +# so any RUN that fetches packages fails; +# (b) base images must be glibc (node:22-slim), NOT alpine/musl: native modules (@next/swc-*, +# sharp) are prebuilt for the platform that ran `bun install` — CI job and prod host are +# x86_64 glibc linux. Building on another platform requires reinstalling node_modules there first. +FROM node:22-slim AS build WORKDIR /app COPY package.json bun.lock ./ -RUN --mount=type=cache,target=/root/.bun/install/cache bun install --frozen-lockfile - -# Production dependencies only — devDeps (typescript, vitest, @types) are build-time only. -FROM oven/bun:1.4.2-alpine AS prod-deps -WORKDIR /app -COPY package.json bun.lock ./ -RUN --mount=type=cache,target=/root/.bun/install/cache bun install --production --frozen-lockfile - -FROM node:22-alpine AS build -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules +COPY node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build @@ -23,10 +16,12 @@ RUN npm run build # No `output: 'standalone'`: the runtime stage keeps node_modules because # /api/mlworker/[file] reads maplibre-gl dist files from disk at runtime, and # /api/airports reads data/*.csv — both resolved relative to process.cwd(). -FROM node:22-alpine AS runtime +FROM node:22-slim AS runtime WORKDIR /app ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 -COPY --from=prod-deps /app/node_modules ./node_modules +# Full tree (dev+prod deps): a prod-only slimmed copy would need an extra install step, +# revisit if image size matters. +COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/.next ./.next COPY --from=build /app/package.json ./package.json COPY --from=build /app/data ./data