From 0c921556fb60650b1f1bedd431b1ad31abecefdb Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 15:35:06 +0000 Subject: [PATCH 01/17] =?UTF-8?q?chore:=20v0.13.0=20=E2=80=94=20CI=20pipel?= =?UTF-8?q?ine=20(build=20=E2=86=92=20unit=20=E2=86=92=20e2e)=20on=20PRs?= =?UTF-8?q?=20to=20master=20+=20push,=20dind-isolated=20e2e=20stack=20(doc?= =?UTF-8?q?ker-compose.ci.yml),=20playwright=20config=20env-var=20override?= =?UTF-8?q?s=20(PW=5FREMOTE=5FBROWSER/E2E=5FDEPLOY=5FCMD/E2E=5FBASE=5FURL/?= =?UTF-8?q?E2E=5FCOMPOSE=5FARGS,=20backward-compatible),=20@playwright/tes?= =?UTF-8?q?t=20pinned=20as=20devDependency;=20AGENTS.md=20switched=20to=20?= =?UTF-8?q?pull-request=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 3 +- .gitea/workflows/ci.yml | 81 +++++++++++++++++++++++++++++++++++++++++ AGENTS.md | 15 +++++--- CHANGELOG.md | 6 +++ bun.lock | 7 ++++ docker-compose.ci.yml | 19 ++++++++++ e2e/state.spec.ts | 4 +- package.json | 3 +- playwright.config.ts | 28 ++++++++------ 9 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 docker-compose.ci.yml diff --git a/.dockerignore b/.dockerignore index 7341242..297b657 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,7 +8,8 @@ test-results .git .env .codenomad -# e2e harness: @playwright/test is a host-global install, not a project dependency +# e2e harness: the suite runs on the host / CI job, never inside the image +# (@playwright/test is a devDependency of the runner, excluded from prod-deps) e2e playwright.config.ts # docs/deployment files never affect the app build — excluding them keeps diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..c614ebc --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + ci: + name: build + unit + e2e + runs-on: ubuntu-latest + services: + docker: + image: docker:dind + ports: + - "2375:2375" + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Resolve the dind daemon endpoint up front (fail fast if the runner's dind is + # misconfigured). Gitea Actions services are sibling containers on the job network, + # reachable by service id; depending on runner config published ports may also land + # on localhost — try both and export whichever answers. The app URL follows the same + # endpoint: the flight container's port 3000 is published inside dind, so it is + # reachable wherever the daemon itself is. + - name: Resolve dind endpoint + run: | + for host in tcp://localhost:2375 tcp://docker:2375; do + for i in $(seq 1 15); do + if DOCKER_HOST="$host" docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then + echo "DOCKER_HOST=$host" >> "$GITHUB_ENV" + case "$host" in + *localhost*) echo "E2E_BASE_URL=http://localhost:3000" >> "$GITHUB_ENV" ;; + *) echo "E2E_BASE_URL=http://docker:3000" >> "$GITHUB_ENV" ;; + esac + exit 0 + fi + sleep 2 + done + done + echo "::error::no reachable dind endpoint (tried tcp://localhost:2375 and tcp://docker:2375)" >&2 + exit 1 + + - name: Setup Node + uses: actions/setup-node@v4 + 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. + - name: Install bun + run: npm i -g bun@1.4.2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build + run: npm run build + + - name: Unit tests + run: npx vitest run + + # Browsers run locally in CI (PW_REMOTE_BROWSER=0 below): the shared remote browser + # server at 192.168.1.4 cannot reach a stack inside dind, and local chromium keeps + # the job self-contained. + - name: Install browsers (local) + run: npx playwright install --with-deps chromium + + # E2E against an isolated stack inside dind — never the production instance. + # docker-compose.ci.yml mirrors docker-compose.yml minus Traefik; fresh named-volume + # DB per run. E2E_DEPLOY_CMD makes the config's webServer block deploy + wait for + # readiness; E2E_COMPOSE_ARGS points state.spec.ts' mid-run `docker compose restart + # flight` at the same file. DOCKER_HOST/E2E_BASE_URL come from GITHUB_ENV above. + - name: E2E (isolated stack in dind) + env: + PW_REMOTE_BROWSER: "0" + E2E_DEPLOY_CMD: docker compose -f docker-compose.ci.yml up -d --build + E2E_COMPOSE_ARGS: "-f docker-compose.ci.yml" + run: npx playwright test diff --git a/AGENTS.md b/AGENTS.md index 8a0e80b..a758755 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,17 @@ ## Change process (mandatory) 1. **Small, incremental changes** — work is done one change at a time, in smaller parts; each part must be tested thoroughly with unit and e2e tests covering it before moving on. -2. **Changelog entry** — every such change is listed in `CHANGELOG.md` with date, time and version (format: `## v — YYYY-MM-DD HH:MM`, then bullet points of what changed). -3. **Version bump** — the version is updated in `package.json`. -4. **Commit + tag** — a commit is created for each change, tagged with the version (e.g. `v0.1.0`), push it to remote repo. +2. **Feature branch + PR** — every change lands on a feature branch off `master` (no direct pushes to master). The branch carries the commits below; open a pull request to `master`. +3. **Changelog entry** — every such change is listed in `CHANGELOG.md` with date, time and version (format: `## v — YYYY-MM-DD HH:MM`, then bullet points of what changed). +4. **Version bump** — the version is updated in `package.json`. +5. **CI must pass** — the PR's pipeline (build → unit tests → e2e, `.gitea/workflows/ci.yml`) must be green before merging. CI never deploys to production; its e2e runs against an isolated stack inside dind. +6. **Tag + manual deploy after merge** — once merged, tag the version on `master` (e.g. `v0.13.0`). Production (`https://flight.dev.kkhost.pl`) is updated manually afterwards: `docker compose up -d --build` on the host. ## E2E tests (mandatory) -1. **Docker compose instance is the app under test** — e2e always runs against the docker-compose build behind Traefik (`https://flight.dev.kkhost.pl`), never a local `next dev` on :3000 (dev builds don't render for non-loopback clients, and a second instance splits state). -2. **Update before testing** — deploy current code first: `docker compose up -d --build`. -3. **Remote browsers** — tests run through the shared Playwright server at `192.168.1.4:3000` (Docker image tag must match this repo's @playwright/test version); plain `npx playwright test` handles deploy + readiness + remote connect (see `playwright.config.ts`). +1. **Docker compose instance is the app under test** — e2e always runs against a docker-compose build, never a local `next dev` on :3000 (dev builds don't render for non-loopback clients, and a second instance splits state). Locally that means the Traefik-routed instance at `https://flight.dev.kkhost.pl`; in CI it is an isolated stack inside dind (`docker-compose.ci.yml`) — **CI never deploys to or modifies production**. +2. **Update before testing (local)** — deploy current code first: `docker compose up -d --build`, then run the suite with plain `npx playwright test`. The config's webServer block is off by default; set `E2E_DEPLOY_CMD` to have Playwright itself run a deploy command and wait for readiness. +3. **Remote browsers** — local runs drive browsers through the shared Playwright server at `192.168.1.4:3000` (Docker image tag must match this repo's @playwright/test version, pinned in devDependencies). Set `PW_REMOTE_BROWSER=0` to run local chromium instead — CI does, because the remote server cannot reach a stack inside dind. +4. **CI env vars** (`playwright.config.ts`, all optional; defaults preserve the local flow) — `E2E_BASE_URL` (app under test), `E2E_DEPLOY_CMD` (webServer deploy command), `PW_REMOTE_BROWSER=0` (local browsers); `e2e/state.spec.ts` reads `E2E_COMPOSE_ARGS` for its mid-run container restart. ## API 1. **Live base URL** — `https://flight.dev.kkhost.pl` (the docker-compose build behind Traefik). diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d58b0c..c024be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## v0.12.3 — 2026-09-19 07:36 - **e2e: removed the unreliable curved-route assertion from `map-interactions.spec.ts`.** The "route line has >2 coordinates in the routes source" check (added with v0.12.2) failed in CI and against the live instance even though the arc renders correctly — MapLibre's `querySourceFeatures` returns tile-simplified geometry at low zoom, so coordinate counts from it are not a reliable signal for what was pushed into the source. The curved arc remains fully covered by unit tests (`lib/mapData.test.ts`: 25 sampled coordinates, exact endpoints, curvature on long routes); the spec keeps all other steps (grounded plane draws no line, marker moves across polls, plane popup with status + route) +## v0.13.0 — 2026-09-18 15:31 +- **CI + pull-request workflow** (process/infra): new `.gitea/workflows/ci.yml` runs build → unit tests → e2e on every PR to master and on push to master, in one job with a `docker:dind` service. E2E isolation: the suite targets a fresh compose stack inside dind (`docker-compose.ci.yml` — mirrors docker-compose.yml minus Traefik/dev_net; app served directly on port 3000 of the dind container; game state in a named volume so it is fresh per run and survives `state.spec.ts`'s mid-run restart) — **CI never deploys to or touches production**. The dind endpoint is resolved at job start (tries `tcp://localhost:2375`, then `tcp://docker:2375` — Gitea services are sibling containers reachable by service id on the job network) and exported as `DOCKER_HOST` + `E2E_BASE_URL`; build context is tarred by the compose client in the job, so nothing needs to be visible from inside dind +- **Playwright config made CI-aware, backward-compatible** (`playwright.config.ts`): new optional env vars — `PW_REMOTE_BROWSER=0` (local chromium instead of the shared remote server at 192.168.1.4:3000), `E2E_DEPLOY_CMD` (runs as webServer + readiness wait; unset = current local behavior with no auto-deploy); `E2E_BASE_URL` already existed. `e2e/state.spec.ts` reads `E2E_COMPOSE_ARGS` for its mid-run `docker compose restart flight` (default empty → unchanged locally) +- **@playwright/test pinned as devDependency** (`1.63.0`, matching the shared remote browser server image and the global install) so CI is self-contained; still excluded from the runtime image via .dockerignore (comment updated), prod-deps stage unaffected +- `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 +- 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 - **Bugfix (ISS-003): route leg lines were drawn as straight two-point chords instead of following the curved great-circle path planes actually fly.** Root cause in `lib/mapData.ts`: `routesGeoJson` built each flying plane's leg LineString from only its origin and destination airport coordinates, so on long routes the drawn line cut across the chord (up to ~40 km off the true arc for a ~1400 km leg) while the plane marker moved along the curved path. Fix: new pure `greatCircleArc(from, to)` samples the same slerp used by the sim (`positionAt` in `lib/sim/movement.ts`) at t = i/24 for i = 0..24 — **25 coordinates** (exact endpoints + 23 intermediate points, `ROUTE_ARC_SEGMENTS = 24`) — so the drawn line matches the curved flight path; `routesGeoJson` now uses it instead of the two-point chord diff --git a/bun.lock b/bun.lock index 6d7a2a9..9f7cfc9 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "sql.js": "^1.14.2", }, "devDependencies": { + "@playwright/test": "1.63.0", "@types/node": "^22.20.3", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", @@ -123,6 +124,8 @@ "@oxc-project/types": ["@oxc-project/types@0.150.0", "", {}, "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw=="], + "@playwright/test": ["@playwright/test@1.63.0", "", { "dependencies": { "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.9", "", { "os": "android", "cpu": "arm" }, "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.9", "", { "os": "android", "cpu": "arm64" }, "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg=="], @@ -299,6 +302,10 @@ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], + + "playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="], + "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], "potpack": ["potpack@2.1.0", "", {}, "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="], diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 0000000..516aa8d --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,19 @@ +# Isolated e2e stack for CI (.gitea/workflows/ci.yml) — runs inside the job's dind daemon, +# never on the production host. Mirrors docker-compose.yml minus Traefik: no external +# dev_net, no traefik labels; the app is served directly on port 3000 of the dind container. +# The build context is tarred by the compose client in the job and sent over DOCKER_HOST, +# so nothing here needs to be visible from inside dind; game state lives in a named volume +# (fresh per run, survives `docker compose restart flight` for e2e/state.spec.ts). +services: + flight: + build: . + image: flight-ops-ci + ports: + - "3000:3000" + environment: + FLIGHT_TEST_HOOKS: "1" + volumes: + - flight-db:/app/db + +volumes: + flight-db: diff --git a/e2e/state.spec.ts b/e2e/state.spec.ts index 6d31dc2..9010a63 100644 --- a/e2e/state.spec.ts +++ b/e2e/state.spec.ts @@ -3,8 +3,10 @@ import path from 'node:path'; import { test, expect } from '@playwright/test'; // Repo root — docker-compose.yml lives here; `docker compose restart` runs on -// this host (the test runner), browsers stay remote. +// this host (the test runner), browsers stay remote. CI points E2E_COMPOSE_ARGS at the +// isolated stack file (docker-compose.ci.yml) instead of the default one. const repoRoot = path.join(__dirname, '..'); +const composeArgs = process.env.E2E_COMPOSE_ARGS ?? ''; interface StateBody { account: { cash: number; reputation: number }; diff --git a/package.json b/package.json index 04329fb..f18905f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flight-ops", - "version": "0.12.3", + "version": "0.13.0", "private": true, "scripts": { "dev": "next dev -p 3000", @@ -19,6 +19,7 @@ "sql.js": "^1.14.2" }, "devDependencies": { + "@playwright/test": "1.63.0", "@types/node": "^22.20.3", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", diff --git a/playwright.config.ts b/playwright.config.ts index 72b01de..a823770 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,11 +1,20 @@ import { defineConfig } from '@playwright/test'; -const remoteRunner = true; +// Browsers run on the shared remote Playwright server by default (AGENTS.md). +// PW_REMOTE_BROWSER=0 → local browsers instead (CI, or boxes without LAN access to 192.168.1.4). +const remoteBrowser = process.env.PW_REMOTE_BROWSER !== '0'; -if (!process.env.PW_TEST_CONNECT_WS_ENDPOINT) { +if (!process.env.PW_TEST_CONNECT_WS_ENDPOINT && remoteBrowser) { process.env.PW_TEST_CONNECT_WS_ENDPOINT = 'ws://192.168.1.4:3000/'; } +// App under test. Default: the live compose instance behind Traefik, deployed manually +// before running the suite (AGENTS.md). When E2E_DEPLOY_CMD is set it runs as a webServer +// and the suite waits for baseURL to come up — CI uses this to target an isolated stack +// inside dind instead of production. All vars optional; unset = current local behavior. +const deployCmd = process.env.E2E_DEPLOY_CMD; +const baseUrl = process.env.E2E_BASE_URL ?? 'https://flight.dev.kkhost.pl'; + export default defineConfig({ testDir: './e2e', // Serial execution: e2e/state.spec.ts restarts the compose container mid-run, @@ -14,15 +23,10 @@ export default defineConfig({ timeout: 300_000, retries: 1, use: { - baseURL: process.env.E2E_BASE_URL ?? 'https://flight.dev.kkhost.pl', + baseURL: baseUrl, trace: 'on-first-retry', }, - webServer: remoteRunner - ? undefined - : { - command: 'docker compose up -d --build', - url: 'https://flight.dev.kkhost.pl', - reuseExistingServer: true, - timeout: 600_000, - }, -}); \ No newline at end of file + webServer: deployCmd + ? { command: deployCmd, url: baseUrl, reuseExistingServer: true, timeout: 600_000 } + : undefined, +}); -- 2.54.0 From 775b0e479a6f03a77a3f74fb763b54d3615b7cd9 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 15:47:39 +0000 Subject: [PATCH 02/17] ci: allow manual workflow dispatch --- .gitea/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c614ebc..2db53f3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [master] pull_request: branches: [master] + workflow_dispatch: jobs: ci: -- 2.54.0 From 47381f201168ea396d0b68f7b3d769f8b75f8583 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 19:10:52 +0000 Subject: [PATCH 03/17] =?UTF-8?q?ci:=20adapt=20to=20full-dind=20runner=20?= =?UTF-8?q?=E2=80=94=20drop=20dind=20service,=20resolve=20daemon=20endpoin?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 81 ++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2db53f3..f259354 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -11,37 +11,76 @@ jobs: ci: name: build + unit + e2e runs-on: ubuntu-latest - services: - docker: - image: docker:dind - ports: - - "2375:2375" steps: - name: Checkout uses: actions/checkout@v4 - # Resolve the dind daemon endpoint up front (fail fast if the runner's dind is - # misconfigured). Gitea Actions services are sibling containers on the job network, - # reachable by service id; depending on runner config published ports may also land - # on localhost — try both and export whichever answers. The app URL follows the same - # endpoint: the flight container's port 3000 is published inside dind, so it is - # reachable wherever the daemon itself is. + # Full-dind runner: job containers already reach the runner's own docker daemon + # (pre-set DOCKER_HOST and/or a mounted /var/run/docker.sock) — no dind service. + # Resolve which endpoint works, export it as DOCKER_HOST, and derive E2E_BASE_URL + # from it: for tcp/https endpoints published ports live on that host; for a unix + # socket the daemon sits on the job's docker bridge, so its published ports are + # reached via the default gateway. - name: Resolve dind endpoint run: | - for host in tcp://localhost:2375 tcp://docker:2375; do - for i in $(seq 1 15); do - if DOCKER_HOST="$host" docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then - echo "DOCKER_HOST=$host" >> "$GITHUB_ENV" - case "$host" in - *localhost*) echo "E2E_BASE_URL=http://localhost:3000" >> "$GITHUB_ENV" ;; - *) echo "E2E_BASE_URL=http://docker:3000" >> "$GITHUB_ENV" ;; - esac + set -u + + derive_url() { + ep="$1" + case "$ep" in + unix://*) + gw=$(ip route show default 2>/dev/null | awk '{print $3; exit}') + if [ -z "${gw:-}" ]; then + echo "::error::unix-socket daemon but no default gateway found (needed to reach published ports)" >&2 + return 1 + fi + echo "http://$gw:3000" ;; + tcp://*|https://*) + hostpart=$(printf '%s' "$ep" | sed -e 's|^[a-z]*://||') + hostpart=${hostpart%%:*} + if [ -z "$hostpart" ]; then return 1; fi + echo "http://$hostpart:3000" ;; + *) + echo "::error::cannot derive app URL from endpoint '$ep'" >&2 + return 1 ;; + esac + } + + # 1) A pre-set DOCKER_HOST that answers is used as-is, no probing. + if [ -n "${DOCKER_HOST:-}" ]; then + i=0 + while [ "$i" -lt 5 ]; do + if docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then + url=$(derive_url "$DOCKER_HOST") || exit 1 + echo "using pre-set DOCKER_HOST=$DOCKER_HOST (E2E_BASE_URL=$url)" + echo "DOCKER_HOST=$DOCKER_HOST" >> "$GITHUB_ENV" + echo "E2E_BASE_URL=$url" >> "$GITHUB_ENV" exit 0 fi - sleep 2 + i=$((i+1)); sleep 2 + done + fi + + # 2) Otherwise probe candidates in order (short retry each). + tried="" + for ep in "${DOCKER_HOST:-}" unix:///var/run/docker.sock tcp://localhost:2375 tcp://localhost:2376 tcp://docker:2375 tcp://docker:2376; do + [ -n "$ep" ] || continue + case " $tried " in *" $ep "*) continue ;; esac + tried="$tried $ep" + i=0 + while [ "$i" -lt 10 ]; do + if DOCKER_HOST="$ep" docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then + url=$(derive_url "$ep") || exit 1 + echo "using probed endpoint $ep (E2E_BASE_URL=$url)" + echo "DOCKER_HOST=$ep" >> "$GITHUB_ENV" + echo "E2E_BASE_URL=$url" >> "$GITHUB_ENV" + exit 0 + fi + i=$((i+1)); sleep 2 done done - echo "::error::no reachable dind endpoint (tried tcp://localhost:2375 and tcp://docker:2375)" >&2 + + echo "::error::no reachable docker daemon (tried: ${DOCKER_HOST:-}, unix:///var/run/docker.sock, tcp://localhost:2375, tcp://localhost:2376, tcp://docker:2375, tcp://docker:2376)" >&2 exit 1 - name: Setup Node -- 2.54.0 From 5ee0d89826b5a3a57447485cc49595a8e3639a0d Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 19:53:05 +0000 Subject: [PATCH 04/17] =?UTF-8?q?ci:=20v0.13.0=20=E2=80=94=20e2e=20via=20s?= =?UTF-8?q?hared=20docker=20network=20(flight-ci-e2e)=20instead=20of=20end?= =?UTF-8?q?point-derived=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 73 +++++++++++++++++++---------------------- docker-compose.ci.yml | 21 ++++++++---- e2e/state.spec.ts | 4 +-- 3 files changed, 51 insertions(+), 47 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f259354..fad102a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,44 +17,18 @@ jobs: # Full-dind runner: job containers already reach the runner's own docker daemon # (pre-set DOCKER_HOST and/or a mounted /var/run/docker.sock) — no dind service. - # Resolve which endpoint works, export it as DOCKER_HOST, and derive E2E_BASE_URL - # from it: for tcp/https endpoints published ports live on that host; for a unix - # socket the daemon sits on the job's docker bridge, so its published ports are - # reached via the default gateway. - - name: Resolve dind endpoint + # Find a working endpoint and export it as DOCKER_HOST for all later steps. + - name: Resolve docker daemon run: | set -u - derive_url() { - ep="$1" - case "$ep" in - unix://*) - gw=$(ip route show default 2>/dev/null | awk '{print $3; exit}') - if [ -z "${gw:-}" ]; then - echo "::error::unix-socket daemon but no default gateway found (needed to reach published ports)" >&2 - return 1 - fi - echo "http://$gw:3000" ;; - tcp://*|https://*) - hostpart=$(printf '%s' "$ep" | sed -e 's|^[a-z]*://||') - hostpart=${hostpart%%:*} - if [ -z "$hostpart" ]; then return 1; fi - echo "http://$hostpart:3000" ;; - *) - echo "::error::cannot derive app URL from endpoint '$ep'" >&2 - return 1 ;; - esac - } - # 1) A pre-set DOCKER_HOST that answers is used as-is, no probing. if [ -n "${DOCKER_HOST:-}" ]; then i=0 while [ "$i" -lt 5 ]; do if docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then - url=$(derive_url "$DOCKER_HOST") || exit 1 - echo "using pre-set DOCKER_HOST=$DOCKER_HOST (E2E_BASE_URL=$url)" + echo "using pre-set DOCKER_HOST=$DOCKER_HOST" echo "DOCKER_HOST=$DOCKER_HOST" >> "$GITHUB_ENV" - echo "E2E_BASE_URL=$url" >> "$GITHUB_ENV" exit 0 fi i=$((i+1)); sleep 2 @@ -70,10 +44,8 @@ jobs: i=0 while [ "$i" -lt 10 ]; do if DOCKER_HOST="$ep" docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then - url=$(derive_url "$ep") || exit 1 - echo "using probed endpoint $ep (E2E_BASE_URL=$url)" + echo "using probed endpoint $ep" echo "DOCKER_HOST=$ep" >> "$GITHUB_ENV" - echo "E2E_BASE_URL=$url" >> "$GITHUB_ENV" exit 0 fi i=$((i+1)); sleep 2 @@ -83,6 +55,20 @@ jobs: echo "::error::no reachable docker daemon (tried: ${DOCKER_HOST:-}, unix:///var/run/docker.sock, tcp://localhost:2375, tcp://localhost:2376, tcp://docker:2375, tcp://docker:2376)" >&2 exit 1 + # Job and app containers share the daemon, so put them on one user-defined network + # and reach the app by service DNS — no endpoint-derived URLs (the job's netns has + # no default route on this runner). The alias `app` is declared in docker-compose.ci.yml. + - name: Prepare e2e network + run: | + set -u + docker network inspect flight-ci-e2e >/dev/null 2>&1 || docker network create flight-ci-e2e + if ! docker network connect flight-ci-e2e "$(hostname)"; then + echo "::error::failed to attach job container '$(hostname)' to flight-ci-e2e — the job must run in a container on the shared daemon (check runner config)" >&2 + exit 1 + fi + docker network inspect flight-ci-e2e --format 'containers on flight-ci-e2e: {{range .Containers}}{{.Name}} {{end}}' + echo "E2E_BASE_URL=http://app:3000" >> "$GITHUB_ENV" + - name: Setup Node uses: actions/setup-node@v4 with: @@ -108,14 +94,23 @@ jobs: - name: Install browsers (local) run: npx playwright install --with-deps chromium - # E2E against an isolated stack inside dind — never the production instance. - # docker-compose.ci.yml mirrors docker-compose.yml minus Traefik; fresh named-volume - # DB per run. E2E_DEPLOY_CMD makes the config's webServer block deploy + wait for - # readiness; E2E_COMPOSE_ARGS points state.spec.ts' mid-run `docker compose restart - # flight` at the same file. DOCKER_HOST/E2E_BASE_URL come from GITHUB_ENV above. + # E2E against an isolated stack on the shared daemon — never the production instance. + # docker-compose.ci.yml joins the flight-ci-e2e network prepared above; project name + # flight-ci keeps it distinct from production. E2E_DEPLOY_CMD makes the config's + # webServer block deploy + wait for readiness (http://app:3000 via service DNS); + # E2E_COMPOSE_ARGS points state.spec.ts' mid-run `docker compose restart flight` at + # the same file + project. DOCKER_HOST/E2E_BASE_URL come from GITHUB_ENV above. - name: E2E (isolated stack in dind) env: PW_REMOTE_BROWSER: "0" - E2E_DEPLOY_CMD: docker compose -f docker-compose.ci.yml up -d --build - E2E_COMPOSE_ARGS: "-f docker-compose.ci.yml" + E2E_DEPLOY_CMD: docker compose -f docker-compose.ci.yml -p flight-ci up -d --build + E2E_COMPOSE_ARGS: "-f docker-compose.ci.yml -p flight-ci" run: npx playwright test + + # The daemon is shared with production — never leave our stack or network behind. + # -v drops the named DB volume so every run seeds fresh (no schema drift between PRs). + - name: Cleanup e2e stack + network + if: always() + run: | + docker compose -f docker-compose.ci.yml -p flight-ci down --remove-orphans -v || true + docker network remove flight-ci-e2e 2>/dev/null || true diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 516aa8d..1724adf 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,19 +1,28 @@ -# Isolated e2e stack for CI (.gitea/workflows/ci.yml) — runs inside the job's dind daemon, -# never on the production host. Mirrors docker-compose.yml minus Traefik: no external -# dev_net, no traefik labels; the app is served directly on port 3000 of the dind container. +# Isolated e2e stack for CI (.gitea/workflows/ci.yml) — runs on the runner's shared dind +# daemon under project name flight-ci (distinct from production). Joins the external +# network flight-ci-e2e that the workflow attaches the job container to; the app is reached +# via service DNS as http://app:3000 (alias declared below) — no host port mapping needed. # The build context is tarred by the compose client in the job and sent over DOCKER_HOST, # so nothing here needs to be visible from inside dind; game state lives in a named volume -# (fresh per run, survives `docker compose restart flight` for e2e/state.spec.ts). +# (fresh per run — CI cleanup removes it — survives `docker compose restart flight` for +# e2e/state.spec.ts). services: flight: build: . image: flight-ops-ci - ports: - - "3000:3000" environment: FLIGHT_TEST_HOOKS: "1" volumes: - flight-db:/app/db + networks: + flight-ci-e2e: + aliases: + - app + +networks: + flight-ci-e2e: + name: flight-ci-e2e + external: true volumes: flight-db: diff --git a/e2e/state.spec.ts b/e2e/state.spec.ts index 9010a63..bfb9f6b 100644 --- a/e2e/state.spec.ts +++ b/e2e/state.spec.ts @@ -4,7 +4,7 @@ import { test, expect } from '@playwright/test'; // Repo root — docker-compose.yml lives here; `docker compose restart` runs on // this host (the test runner), browsers stay remote. CI points E2E_COMPOSE_ARGS at the -// isolated stack file (docker-compose.ci.yml) instead of the default one. +// isolated stack file + project (-f docker-compose.ci.yml -p flight-ci) instead of the default one. const repoRoot = path.join(__dirname, '..'); const composeArgs = process.env.E2E_COMPOSE_ARGS ?? ''; @@ -48,7 +48,7 @@ test('state survives a container restart (sim clock + account persisted)', async expect(resBefore.status()).toBe(200); const before = (await resBefore.json()) as StateBody; - execSync('docker compose restart flight', { cwd: repoRoot, stdio: 'pipe' }); + execSync(`docker compose ${composeArgs} restart flight`, { cwd: repoRoot, stdio: 'pipe' }); // Wait for the app to come back, capturing the post-restart snapshot. let after: StateBody | undefined; -- 2.54.0 From 543aad310c7756a9e1c79a07657ee605c0c2ab5b Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 20:00:40 +0000 Subject: [PATCH 05/17] =?UTF-8?q?ci:=20v0.13.0=20=E2=80=94=20drop=20daemon?= =?UTF-8?q?=20probing;=20runner=20provides=20docker=20out=20of=20the=20box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 50 +++++------------------------------------ 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index fad102a..f1e2d4c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -15,49 +15,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Full-dind runner: job containers already reach the runner's own docker daemon - # (pre-set DOCKER_HOST and/or a mounted /var/run/docker.sock) — no dind service. - # Find a working endpoint and export it as DOCKER_HOST for all later steps. - - name: Resolve docker daemon - run: | - set -u - - # 1) A pre-set DOCKER_HOST that answers is used as-is, no probing. - if [ -n "${DOCKER_HOST:-}" ]; then - i=0 - while [ "$i" -lt 5 ]; do - if docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then - echo "using pre-set DOCKER_HOST=$DOCKER_HOST" - echo "DOCKER_HOST=$DOCKER_HOST" >> "$GITHUB_ENV" - exit 0 - fi - i=$((i+1)); sleep 2 - done - fi - - # 2) Otherwise probe candidates in order (short retry each). - tried="" - for ep in "${DOCKER_HOST:-}" unix:///var/run/docker.sock tcp://localhost:2375 tcp://localhost:2376 tcp://docker:2375 tcp://docker:2376; do - [ -n "$ep" ] || continue - case " $tried " in *" $ep "*) continue ;; esac - tried="$tried $ep" - i=0 - while [ "$i" -lt 10 ]; do - if DOCKER_HOST="$ep" docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then - echo "using probed endpoint $ep" - echo "DOCKER_HOST=$ep" >> "$GITHUB_ENV" - exit 0 - fi - i=$((i+1)); sleep 2 - done - done - - echo "::error::no reachable docker daemon (tried: ${DOCKER_HOST:-}, unix:///var/run/docker.sock, tcp://localhost:2375, tcp://localhost:2376, tcp://docker:2375, tcp://docker:2376)" >&2 - exit 1 - - # Job and app containers share the daemon, so put them on one user-defined network - # and reach the app by service DNS — no endpoint-derived URLs (the job's netns has - # no default route on this runner). The alias `app` is declared in docker-compose.ci.yml. + # The runner provides a working docker CLI out of the box (full-dind) — no endpoint + # resolution needed. Job and app containers share the daemon, so put them on one + # user-defined network and reach the app by service DNS (the job's netns has no + # default route here, so host-IP URLs are unreliable). The alias `app` is declared + # in docker-compose.ci.yml. - name: Prepare e2e network run: | set -u @@ -99,7 +61,7 @@ jobs: # flight-ci keeps it distinct from production. E2E_DEPLOY_CMD makes the config's # webServer block deploy + wait for readiness (http://app:3000 via service DNS); # E2E_COMPOSE_ARGS points state.spec.ts' mid-run `docker compose restart flight` at - # the same file + project. DOCKER_HOST/E2E_BASE_URL come from GITHUB_ENV above. + # the same file + project. E2E_BASE_URL comes from GITHUB_ENV (Prepare e2e network). - name: E2E (isolated stack in dind) env: PW_REMOTE_BROWSER: "0" -- 2.54.0 From 579d425b644c65dabf84a2c2daed1bd38a6b5b0a Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 20:31:37 +0000 Subject: [PATCH 06/17] =?UTF-8?q?ci:=20v0.13.0=20=E2=80=94=20plain-progres?= =?UTF-8?q?s=20image=20build,=20bun=20cache=20mounts=20+=20actions/cache?= =?UTF-8?q?=20around=20dep=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 24 +++++++++++++++++++++++- CHANGELOG.md | 1 + Dockerfile | 10 +++++----- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f1e2d4c..4da5bad 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -41,9 +41,29 @@ jobs: - name: Install bun run: npm i -g bun@1.4.2 + # Job-level dep cache via the runner's built-in cache server. continue-on-error on + # both steps: an unreachable/broken cache server must never break the run. + - name: Restore deps cache + uses: actions/cache@v4 + continue-on-error: true + with: + path: | + node_modules + $HOME/.bun/install/cache + key: ${{ runner.os }}-deps-bun-${{ hashFiles('bun.lock') }} + - name: Install dependencies run: bun install --frozen-lockfile + - name: Save deps cache + uses: actions/cache@v4 + continue-on-error: true + with: + path: | + node_modules + $HOME/.bun/install/cache + key: ${{ runner.os }}-deps-bun-${{ hashFiles('bun.lock') }} + - name: Build run: npm run build @@ -65,7 +85,9 @@ jobs: - name: E2E (isolated stack in dind) env: PW_REMOTE_BROWSER: "0" - E2E_DEPLOY_CMD: docker compose -f docker-compose.ci.yml -p flight-ci up -d --build + # Build separately with plain progress so BuildKit step output is visible in the + # job log (auto progress hides it), then start without rebuilding. + E2E_DEPLOY_CMD: "docker compose -f docker-compose.ci.yml -p flight-ci build --progress=plain && docker compose -f docker-compose.ci.yml -p flight-ci up -d" E2E_COMPOSE_ARGS: "-f docker-compose.ci.yml -p flight-ci" run: npx playwright test diff --git a/CHANGELOG.md b/CHANGELOG.md index c024be3..339c2e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - **Playwright config made CI-aware, backward-compatible** (`playwright.config.ts`): new optional env vars — `PW_REMOTE_BROWSER=0` (local chromium instead of the shared remote server at 192.168.1.4:3000), `E2E_DEPLOY_CMD` (runs as webServer + readiness wait; unset = current local behavior with no auto-deploy); `E2E_BASE_URL` already existed. `e2e/state.spec.ts` reads `E2E_COMPOSE_ARGS` for its mid-run `docker compose restart flight` (default empty → unchanged locally) - **@playwright/test pinned as devDependency** (`1.63.0`, matching the shared remote browser server image and the global install) so CI is self-contained; still excluded from the runtime image via .dockerignore (comment updated), prod-deps stage unaffected - `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) - 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 41b62cc..37ffb69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,17 @@ # Multi-stage build: bun for installs (fast), node for build + runtime. -# No BuildKit type=cache mounts: they don't persist across builds on this daemon -# (Docker 29 + containerd image store), and bun install is fast enough (~3s cold) -# that cross-build tarball caching isn't worth the complexity. +# 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 WORKDIR /app COPY package.json bun.lock ./ -RUN bun install --frozen-lockfile +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 bun install --production --frozen-lockfile +RUN --mount=type=cache,target=/root/.bun/install/cache bun install --production --frozen-lockfile FROM node:22-alpine AS build WORKDIR /app -- 2.54.0 From d0149c6ab32fdf27a34ee75b411414f27017b4dc Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 20:43:15 +0000 Subject: [PATCH 07/17] =?UTF-8?q?ci:=20v0.13.0=20=E2=80=94=20retry=20wrapp?= =?UTF-8?q?ers=20for=20flaky=20runner=20network=20(bun=20install,=20dep=20?= =?UTF-8?q?install,=20e2e=20image=20build)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++-- CHANGELOG.md | 1 + 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4da5bad..98853a3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -38,8 +38,18 @@ jobs: # bun drives installs (repo ships bun.lock, no package-lock.json); pinned to the # same major as oven/bun in the Dockerfile. + # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). - name: Install bun - run: npm i -g bun@1.4.2 + run: | + set -u + ok=0 + for i in 1 2 3; do + if npm i -g bun@1.4.2; then ok=1; break; fi + echo "::warning::bun install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::bun install failed after 3 attempts" >&2; exit 1; } + bun --version # Job-level dep cache via the runner's built-in cache server. continue-on-error on # both steps: an unreachable/broken cache server must never break the run. @@ -52,8 +62,17 @@ jobs: $HOME/.bun/install/cache key: ${{ runner.os }}-deps-bun-${{ hashFiles('bun.lock') }} + # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). - name: Install dependencies - run: bun install --frozen-lockfile + run: | + set -u + ok=0 + for i in 1 2 3; do + if bun install --frozen-lockfile; then ok=1; break; fi + echo "::warning::dependency install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::dependency install failed after 3 attempts" >&2; exit 1; } - name: Save deps cache uses: actions/cache@v4 @@ -76,6 +95,19 @@ jobs: - name: Install browsers (local) run: npx playwright install --with-deps chromium + # 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) + run: | + set -u + ok=0 + for i in 1 2 3; do + if docker compose -f docker-compose.ci.yml -p flight-ci build --progress=plain; then ok=1; break; fi + echo "::warning::e2e image build attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::e2e image build failed after 3 attempts" >&2; exit 1; } + # E2E against an isolated stack on the shared daemon — never the production instance. # docker-compose.ci.yml joins the flight-ci-e2e network prepared above; project name # flight-ci keeps it distinct from production. E2E_DEPLOY_CMD makes the config's diff --git a/CHANGELOG.md b/CHANGELOG.md index 339c2e5..86c10e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - **@playwright/test pinned as devDependency** (`1.63.0`, matching the shared remote browser server image and the global install) so CI is self-contained; still excluded from the runtime image via .dockerignore (comment updated), prod-deps stage unaffected - `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 retry wrappers for the flaky runner network/DNS** (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; the image is pre-built by a dedicated retried step so the E2E step's own build becomes a fast no-op - 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 -- 2.54.0 From 54c7a0c5f54b3623c81ca9a42c42b34f953cbdcb Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 21:14:34 +0000 Subject: [PATCH 08/17] =?UTF-8?q?ci:=20v0.13.0=20=E2=80=94=20drop=20retry?= =?UTF-8?q?=20wrappers;=20add=20non-fatal=20DNS=20diagnostics=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 49 ++++++++++++++--------------------------- CHANGELOG.md | 2 +- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 98853a3..7525649 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -31,6 +31,21 @@ jobs: docker network inspect flight-ci-e2e --format 'containers on flight-ci-e2e: {{range .Containers}}{{.Name}} {{end}}' echo "E2E_BASE_URL=http://app:3000" >> "$GITHUB_ENV" + # Non-fatal DNS diagnostics (runner outbound DNS is flaky — EAI_AGAIN seen in CI): + # logs job-container vs daemon-side resolver config + per-host lookups so the next + # failure shows which layer is broken. Every probe is guarded; this step never fails. + - name: DNS diagnostics + run: | + set -u + echo '=== job container ===' + cat /etc/resolv.conf || echo 'resolv.conf FAILED' + for h in registry.npmjs.org cdn.playwright.dev github.com docker.io; do + getent hosts "$h" >/dev/null 2>&1 && echo "getent $h OK" || echo "getent $h FAILED" + done + curl -so /dev/null --max-time 10 -w 'registry.npmjs.org https %{time_total}s\n' https://registry.npmjs.org || echo 'curl registry FAILED' + echo '=== daemon-side (default bridge) ===' + timeout 90 docker run --rm alpine sh -c 'cat /etc/resolv.conf; nslookup registry.npmjs.org >/dev/null 2>&1 && echo "daemon-bridge DNS OK" || echo "daemon-bridge DNS FAILED"' || echo 'daemon-side probe FAILED' + - name: Setup Node uses: actions/setup-node@v4 with: @@ -38,17 +53,9 @@ jobs: # bun drives installs (repo ships bun.lock, no package-lock.json); pinned to the # same major as oven/bun in the Dockerfile. - # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). - name: Install bun run: | - set -u - ok=0 - for i in 1 2 3; do - if npm i -g bun@1.4.2; then ok=1; break; fi - echo "::warning::bun install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 - sleep 5 - done - [ "$ok" = "1" ] || { echo "::error::bun install failed after 3 attempts" >&2; exit 1; } + npm i -g bun@1.4.2 bun --version # Job-level dep cache via the runner's built-in cache server. continue-on-error on @@ -62,17 +69,8 @@ jobs: $HOME/.bun/install/cache key: ${{ runner.os }}-deps-bun-${{ hashFiles('bun.lock') }} - # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). - name: Install dependencies - run: | - set -u - ok=0 - for i in 1 2 3; do - if bun install --frozen-lockfile; then ok=1; break; fi - echo "::warning::dependency install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 - sleep 5 - done - [ "$ok" = "1" ] || { echo "::error::dependency install failed after 3 attempts" >&2; exit 1; } + run: bun install --frozen-lockfile - name: Save deps cache uses: actions/cache@v4 @@ -95,19 +93,6 @@ jobs: - name: Install browsers (local) run: npx playwright install --with-deps chromium - # 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) - run: | - set -u - ok=0 - for i in 1 2 3; do - if docker compose -f docker-compose.ci.yml -p flight-ci build --progress=plain; then ok=1; break; fi - echo "::warning::e2e image build attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 - sleep 5 - done - [ "$ok" = "1" ] || { echo "::error::e2e image build failed after 3 attempts" >&2; exit 1; } - # E2E against an isolated stack on the shared daemon — never the production instance. # docker-compose.ci.yml joins the flight-ci-e2e network prepared above; project name # flight-ci keeps it distinct from production. E2E_DEPLOY_CMD makes the config's diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c10e3..ade69eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - **@playwright/test pinned as devDependency** (`1.63.0`, matching the shared remote browser server image and the global install) so CI is self-contained; still excluded from the runtime image via .dockerignore (comment updated), prod-deps stage unaffected - `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 retry wrappers for the flaky runner network/DNS** (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; the image is pre-built by a dedicated retried step so the E2E step's own build becomes a fast no-op +- **CI DNS diagnostics, retries removed** (transient EAI_AGAIN failures seen in runs #10/#11): retry wrappers around bun/dep install and the e2e image build were dropped — the flaky runner DNS is a config problem to be fixed on the runner side, not papered over; instead a non-fatal "DNS diagnostics" step logs job-container vs daemon-side resolver config plus per-host lookups (registry.npmjs.org, cdn.playwright.dev, github.com, docker.io) so the next flaky run shows exactly which layer is broken - 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 -- 2.54.0 From c30f5f650f1db0e734391d25e4083cb20dd166f1 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 21:33:35 +0000 Subject: [PATCH 09/17] =?UTF-8?q?ci:=20v0.13.0=20=E2=80=94=20DNS=20diagnos?= =?UTF-8?q?tics=20daemon-side=20probe=20uses=20local=20node:22-alpine=20(n?= =?UTF-8?q?o=20image=20pull)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 3 ++- CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7525649..fa0a4c7 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -44,7 +44,8 @@ jobs: done curl -so /dev/null --max-time 10 -w 'registry.npmjs.org https %{time_total}s\n' https://registry.npmjs.org || echo 'curl registry FAILED' echo '=== daemon-side (default bridge) ===' - timeout 90 docker run --rm alpine sh -c 'cat /etc/resolv.conf; nslookup registry.npmjs.org >/dev/null 2>&1 && echo "daemon-bridge DNS OK" || echo "daemon-bridge DNS FAILED"' || echo 'daemon-side probe FAILED' + # node:22-alpine is already local on the shared daemon (CI builds use it) — no pull needed when the network is down. + timeout 90 docker run --rm node:22-alpine sh -c "cat /etc/resolv.conf; node -e \"require('dns').lookup('registry.npmjs.org',(e,a)=>{console.log(e?'daemon-bridge DNS FAILED':'daemon-bridge DNS OK '+a);process.exit(e?1:0)})\"" || echo 'daemon-side probe FAILED' - name: Setup Node uses: actions/setup-node@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index ade69eb..5c54899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - **@playwright/test pinned as devDependency** (`1.63.0`, matching the shared remote browser server image and the global install) so CI is self-contained; still excluded from the runtime image via .dockerignore (comment updated), prod-deps stage unaffected - `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 DNS diagnostics, retries removed** (transient EAI_AGAIN failures seen in runs #10/#11): retry wrappers around bun/dep install and the e2e image build were dropped — the flaky runner DNS is a config problem to be fixed on the runner side, not papered over; instead a non-fatal "DNS diagnostics" step logs job-container vs daemon-side resolver config plus per-host lookups (registry.npmjs.org, cdn.playwright.dev, github.com, docker.io) so the next flaky run shows exactly which layer is broken +- **CI DNS diagnostics, retries removed** (transient EAI_AGAIN failures seen in runs #10/#11): retry wrappers around bun/dep install and the e2e image build were dropped — the flaky runner DNS is a config problem to be fixed on the runner side, not papered over; instead a non-fatal "DNS diagnostics" step logs job-container vs daemon-side resolver config plus per-host lookups (registry.npmjs.org, cdn.playwright.dev, github.com, docker.io); the daemon-side probe runs in a throwaway node:22-alpine container — image already local on the shared daemon, no pull needed when the network is down — so the next flaky run shows exactly which layer is broken - 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 -- 2.54.0 From 14aaf2690095c1de0ef43593bf6dc6d35fa32378 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 21:49:00 +0000 Subject: [PATCH 10/17] =?UTF-8?q?ci:=20revert=20to=20run-12=20workflow=20s?= =?UTF-8?q?tate=20=E2=80=94=20retries=20restored,=20diagnostics=20removed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 50 ++++++++++++++++++++++++++--------------- CHANGELOG.md | 2 +- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index fa0a4c7..98853a3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -31,22 +31,6 @@ jobs: docker network inspect flight-ci-e2e --format 'containers on flight-ci-e2e: {{range .Containers}}{{.Name}} {{end}}' echo "E2E_BASE_URL=http://app:3000" >> "$GITHUB_ENV" - # Non-fatal DNS diagnostics (runner outbound DNS is flaky — EAI_AGAIN seen in CI): - # logs job-container vs daemon-side resolver config + per-host lookups so the next - # failure shows which layer is broken. Every probe is guarded; this step never fails. - - name: DNS diagnostics - run: | - set -u - echo '=== job container ===' - cat /etc/resolv.conf || echo 'resolv.conf FAILED' - for h in registry.npmjs.org cdn.playwright.dev github.com docker.io; do - getent hosts "$h" >/dev/null 2>&1 && echo "getent $h OK" || echo "getent $h FAILED" - done - curl -so /dev/null --max-time 10 -w 'registry.npmjs.org https %{time_total}s\n' https://registry.npmjs.org || echo 'curl registry FAILED' - echo '=== daemon-side (default bridge) ===' - # node:22-alpine is already local on the shared daemon (CI builds use it) — no pull needed when the network is down. - timeout 90 docker run --rm node:22-alpine sh -c "cat /etc/resolv.conf; node -e \"require('dns').lookup('registry.npmjs.org',(e,a)=>{console.log(e?'daemon-bridge DNS FAILED':'daemon-bridge DNS OK '+a);process.exit(e?1:0)})\"" || echo 'daemon-side probe FAILED' - - name: Setup Node uses: actions/setup-node@v4 with: @@ -54,9 +38,17 @@ jobs: # bun drives installs (repo ships bun.lock, no package-lock.json); pinned to the # same major as oven/bun in the Dockerfile. + # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). - name: Install bun run: | - npm i -g bun@1.4.2 + set -u + ok=0 + for i in 1 2 3; do + if npm i -g bun@1.4.2; then ok=1; break; fi + echo "::warning::bun install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::bun install failed after 3 attempts" >&2; exit 1; } bun --version # Job-level dep cache via the runner's built-in cache server. continue-on-error on @@ -70,8 +62,17 @@ jobs: $HOME/.bun/install/cache key: ${{ runner.os }}-deps-bun-${{ hashFiles('bun.lock') }} + # Retried: the runner's outbound network/DNS is flaky (transient EAI_AGAIN seen in CI). - name: Install dependencies - run: bun install --frozen-lockfile + run: | + set -u + ok=0 + for i in 1 2 3; do + if bun install --frozen-lockfile; then ok=1; break; fi + echo "::warning::dependency install attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::dependency install failed after 3 attempts" >&2; exit 1; } - name: Save deps cache uses: actions/cache@v4 @@ -94,6 +95,19 @@ jobs: - name: Install browsers (local) run: npx playwright install --with-deps chromium + # 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) + run: | + set -u + ok=0 + for i in 1 2 3; do + if docker compose -f docker-compose.ci.yml -p flight-ci build --progress=plain; then ok=1; break; fi + echo "::warning::e2e image build attempt $i/3 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::e2e image build failed after 3 attempts" >&2; exit 1; } + # E2E against an isolated stack on the shared daemon — never the production instance. # docker-compose.ci.yml joins the flight-ci-e2e network prepared above; project name # flight-ci keeps it distinct from production. E2E_DEPLOY_CMD makes the config's diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c54899..fc0baed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - **@playwright/test pinned as devDependency** (`1.63.0`, matching the shared remote browser server image and the global install) so CI is self-contained; still excluded from the runtime image via .dockerignore (comment updated), prod-deps stage unaffected - `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 DNS diagnostics, retries removed** (transient EAI_AGAIN failures seen in runs #10/#11): retry wrappers around bun/dep install and the e2e image build were dropped — the flaky runner DNS is a config problem to be fixed on the runner side, not papered over; instead a non-fatal "DNS diagnostics" step logs job-container vs daemon-side resolver config plus per-host lookups (registry.npmjs.org, cdn.playwright.dev, github.com, docker.io); the daemon-side probe runs in a throwaway node:22-alpine container — image already local on the shared daemon, no pull needed when the network is down — so the next flaky run shows exactly which layer is broken +- **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 - 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 -- 2.54.0 From f89f0407aed4015ed98092e67a9b190846522591 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 22:27:32 +0000 Subject: [PATCH 11/17] =?UTF-8?q?ci:=20image=20build=20needs=20no=20outbou?= =?UTF-8?q?nd=20net=20=E2=80=94=20copy=20job-installed=20node=5Fmodules;?= =?UTF-8?q?=20base=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 -- 2.54.0 From b112339dd35fff21600b50cee8cd1f7f75a81c62 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 22:49:14 +0000 Subject: [PATCH 12/17] =?UTF-8?q?ci:=20keep=20e2e=20webServer=20alive=20?= =?UTF-8?q?=E2=80=94=20up=20-d=20returns=20immediately,=20playwright=20nee?= =?UTF-8?q?ds=20a=20long-running=20cmd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 7 +++++-- CHANGELOG.md | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4fce476..72f690f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -122,8 +122,11 @@ jobs: env: PW_REMOTE_BROWSER: "0" # Build separately with plain progress so BuildKit step output is visible in the - # job log (auto progress hides it), then start without rebuilding. - E2E_DEPLOY_CMD: "docker compose -f docker-compose.ci.yml -p flight-ci build --progress=plain && docker compose -f docker-compose.ci.yml -p flight-ci up -d" + # job log (auto progress hides it). Playwright's webServer needs a long-running + # command — `up -d` returns immediately and triggers "exited early"; `logs -f` + # keeps it alive, streams app logs into CI output for debugging, and is killed with + # the job (an orphaned `logs -f` dies on its own when cleanup removes the container). + E2E_DEPLOY_CMD: "docker compose -f docker-compose.ci.yml -p flight-ci build --progress=plain && docker compose -f docker-compose.ci.yml -p flight-ci up -d && docker compose -f docker-compose.ci.yml -p flight-ci logs -f" E2E_COMPOSE_ARGS: "-f docker-compose.ci.yml -p flight-ci" run: npx playwright test diff --git a/CHANGELOG.md b/CHANGELOG.md index f326c6e..dfb1f90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - **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 +- **CI e2e deploy command stays alive** (`E2E_DEPLOY_CMD` now ends with `docker compose … logs -f`): Playwright's webServer requires a long-running process — `up -d` returns immediately and triggered "Process from config.webServer exited early" (run #16); `logs -f` also streams app logs into CI output for debugging - 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 -- 2.54.0 From 0bfbd2a2e8960989af51be68234c3023fa990518 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 23:22:42 +0000 Subject: [PATCH 13/17] =?UTF-8?q?ci:=20e2e=20=E2=80=94=20bypass=20proxy=20?= =?UTF-8?q?for=20in-network=20app=20host=20+=20connectivity=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 17 ++++++++++++++++- CHANGELOG.md | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 72f690f..2e290eb 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -128,7 +128,22 @@ jobs: # the job (an orphaned `logs -f` dies on its own when cleanup removes the container). E2E_DEPLOY_CMD: "docker compose -f docker-compose.ci.yml -p flight-ci build --progress=plain && docker compose -f docker-compose.ci.yml -p flight-ci up -d && docker compose -f docker-compose.ci.yml -p flight-ci logs -f" E2E_COMPOSE_ARGS: "-f docker-compose.ci.yml -p flight-ci" - run: npx playwright test + # Chromium honors proxy env vars on Linux (node-side HTTP does not) — keep the + # in-network app host out of any proxy path. + NO_PROXY: app,localhost,127.0.0.1,.kkhost.pl + no_proxy: app,localhost,127.0.0.1,.kkhost.pl + run: | + # --- e2e connectivity diagnostics (non-fatal; evidence for ERR_SSL_PROTOCOL_ERROR hunt) --- + echo "=== proxy env ===" + env | grep -iE '^(http_proxy|https_proxy|all_proxy|no_proxy)=' || echo "(none set)" + echo "=== /etc/hosts ===" + cat /etc/hosts + echo "=== DNS for app ===" + getent hosts app || echo "getent: no result" + echo "=== curl http://app:3000/api/version (node-side) ===" + timeout 15 curl -sv http://app:3000/api/version 2>&1 | head -40 || true + # --- end diagnostics --- + npx playwright test # The daemon is shared with production — never leave our stack or network behind. # -v drops the named DB volume so every run seeds fresh (no schema drift between PRs). diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb1f90..eef082b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - **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 - **CI e2e deploy command stays alive** (`E2E_DEPLOY_CMD` now ends with `docker compose … logs -f`): Playwright's webServer requires a long-running process — `up -d` returns immediately and triggered "Process from config.webServer exited early" (run #16); `logs -f` also streams app logs into CI output for debugging +- **CI e2e step bypasses proxies for the in-network app host** (`NO_PROXY`/`no_proxy=app,localhost,127.0.0.1,.kkhost.pl`) and prints proxy/DNS/curl diagnostics before the suite — run #18 showed Chromium getting `ERR_SSL_PROTOCOL_ERROR` on http://app:3000 while node-side HTTP worked (9 API-only tests passed), pointing at a TLS-terminating proxy env var that Chromium honors but Node's HTTP client ignores - 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 -- 2.54.0 From e694011302b4712ec94a9061ebca39925dfc73e5 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 23:56:45 +0000 Subject: [PATCH 14/17] =?UTF-8?q?ci:=20e2e=20=E2=80=94=20static=20in-netwo?= =?UTF-8?q?rk=20IP=20for=20the=20app=20(bypass=20DNS)=20+=20post-deploy=20?= =?UTF-8?q?chromium=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 35 ++++++++++++++++++----------------- CHANGELOG.md | 1 + docker-compose.ci.yml | 5 ++++- e2e/ci-diagnostics.ts | 36 ++++++++++++++++++++++++++++++++++++ playwright.config.ts | 1 + 5 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 e2e/ci-diagnostics.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2e290eb..9cef9df 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -17,19 +17,30 @@ jobs: # The runner provides a working docker CLI out of the box (full-dind) — no endpoint # resolution needed. Job and app containers share the daemon, so put them on one - # user-defined network and reach the app by service DNS (the job's netns has no - # default route here, so host-IP URLs are unreliable). The alias `app` is declared - # in docker-compose.ci.yml. + # user-defined network with a pinned subnet; the app gets a STATIC IP (172.39.0.10, + # declared in docker-compose.ci.yml) because Chromium got ERR_SSL_PROTOCOL_ERROR via + # the DNS alias while node-side HTTP to the same name worked — bypassing DNS removes + # that variable entirely. A stale network with a different subnet is recreated. - name: Prepare e2e network run: | set -u - docker network inspect flight-ci-e2e >/dev/null 2>&1 || docker network create flight-ci-e2e + SUBNET=172.39.0.0/24 + if docker network inspect flight-ci-e2e >/dev/null 2>&1; then + cur=$(docker network inspect flight-ci-e2e --format '{{(index .IPAM.Config 0).Subnet}}') + if [ "$cur" != "$SUBNET" ]; then + echo "stale flight-ci-e2e with subnet $cur — recreating with $SUBNET" + docker network rm flight-ci-e2e || true + docker network create --subnet "$SUBNET" flight-ci-e2e + fi + else + docker network create --subnet "$SUBNET" flight-ci-e2e + fi if ! docker network connect flight-ci-e2e "$(hostname)"; then echo "::error::failed to attach job container '$(hostname)' to flight-ci-e2e — the job must run in a container on the shared daemon (check runner config)" >&2 exit 1 fi docker network inspect flight-ci-e2e --format 'containers on flight-ci-e2e: {{range .Containers}}{{.Name}} {{end}}' - echo "E2E_BASE_URL=http://app:3000" >> "$GITHUB_ENV" + echo "E2E_BASE_URL=http://172.39.0.10:3000" >> "$GITHUB_ENV" - name: Setup Node uses: actions/setup-node@v4 @@ -132,18 +143,8 @@ jobs: # in-network app host out of any proxy path. NO_PROXY: app,localhost,127.0.0.1,.kkhost.pl no_proxy: app,localhost,127.0.0.1,.kkhost.pl - run: | - # --- e2e connectivity diagnostics (non-fatal; evidence for ERR_SSL_PROTOCOL_ERROR hunt) --- - echo "=== proxy env ===" - env | grep -iE '^(http_proxy|https_proxy|all_proxy|no_proxy)=' || echo "(none set)" - echo "=== /etc/hosts ===" - cat /etc/hosts - echo "=== DNS for app ===" - getent hosts app || echo "getent: no result" - echo "=== curl http://app:3000/api/version (node-side) ===" - timeout 15 curl -sv http://app:3000/api/version 2>&1 | head -40 || true - # --- end diagnostics --- - npx playwright test + # Connectivity diagnostics now run post-deploy in e2e/ci-diagnostics.ts (globalSetup). + run: npx playwright test # The daemon is shared with production — never leave our stack or network behind. # -v drops the named DB volume so every run seeds fresh (no schema drift between PRs). diff --git a/CHANGELOG.md b/CHANGELOG.md index eef082b..f72228a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - **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 - **CI e2e deploy command stays alive** (`E2E_DEPLOY_CMD` now ends with `docker compose … logs -f`): Playwright's webServer requires a long-running process — `up -d` returns immediately and triggered "Process from config.webServer exited early" (run #16); `logs -f` also streams app logs into CI output for debugging - **CI e2e step bypasses proxies for the in-network app host** (`NO_PROXY`/`no_proxy=app,localhost,127.0.0.1,.kkhost.pl`) and prints proxy/DNS/curl diagnostics before the suite — run #18 showed Chromium getting `ERR_SSL_PROTOCOL_ERROR` on http://app:3000 while node-side HTTP worked (9 API-only tests passed), pointing at a TLS-terminating proxy env var that Chromium honors but Node's HTTP client ignores +- **CI e2e targets the app at a static in-network IP** (`172.39.0.10`, pinned subnet `172.39.0.0/24` created by "Prepare e2e network"; stale networks with other subnets are recreated) instead of the DNS alias — Chromium got `ERR_SSL_PROTOCOL_ERROR` via the hostname while node-side HTTP worked, so DNS is removed from the equation; post-deploy connectivity diagnostics added as a CI-gated globalSetup (`e2e/ci-diagnostics.ts`, no-op locally) comparing node resolution/HTTP against Chromium navigation on both hostname and static IP - 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/docker-compose.ci.yml b/docker-compose.ci.yml index 1724adf..12615ab 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,7 +1,9 @@ # Isolated e2e stack for CI (.gitea/workflows/ci.yml) — runs on the runner's shared dind # daemon under project name flight-ci (distinct from production). Joins the external # network flight-ci-e2e that the workflow attaches the job container to; the app is reached -# via service DNS as http://app:3000 (alias declared below) — no host port mapping needed. +# at its STATIC IP http://172.39.0.10:3000 — DNS bypassed for the browser because Chromium +# got ERR_SSL_PROTOCOL_ERROR via the hostname while node-side HTTP worked (the `app` alias +# stays as a convenience). Single deterministic address; survives `docker compose restart flight`. # The build context is tarred by the compose client in the job and sent over DOCKER_HOST, # so nothing here needs to be visible from inside dind; game state lives in a named volume # (fresh per run — CI cleanup removes it — survives `docker compose restart flight` for @@ -18,6 +20,7 @@ services: flight-ci-e2e: aliases: - app + ipv4_address: 172.39.0.10 networks: flight-ci-e2e: diff --git a/e2e/ci-diagnostics.ts b/e2e/ci-diagnostics.ts new file mode 100644 index 0000000..1ae1c99 --- /dev/null +++ b/e2e/ci-diagnostics.ts @@ -0,0 +1,36 @@ +// CI-only connectivity diagnostics (globalSetup runs after the e2e stack is ready). +// Evidence for the Chromium ERR_SSL_PROTOCOL_ERROR hunt: compares node-side resolution/HTTP +// against Chromium's own navigation, hostname vs static IP. Never fails the run. +import { promises as dnsPromises } from 'node:dns'; +import { chromium } from '@playwright/test'; + +export default async function ciDiagnostics() { + if (process.env.PW_REMOTE_BROWSER !== '0') return; // local runs: no-op + const base = process.env.E2E_BASE_URL ?? ''; + try { + console.log('[diag] node resolve4(app):', JSON.stringify(await dnsPromises.resolve4('app').catch((e) => `failed: ${e.code}`))); + try { + const r = await fetch(`${base}/api/version`); + console.log('[diag] node fetch', base, '->', r.status, JSON.stringify(await r.json().catch(() => null))); + } catch (e) { + console.log('[diag] node fetch failed:', String(e)); + } + const browser = await chromium.launch(); + try { + for (const url of ['http://app:3000/', base]) { + const page = await (await browser.newContext()).newPage(); + try { + await page.goto(url, { timeout: 15_000 }); + console.log('[diag] chromium goto', url, '-> OK, title:', await page.title().catch(() => '?')); + } catch (e) { + console.log('[diag] chromium goto', url, '-> FAILED:', String(e).split('\n')[0]); + } + await page.context().close(); + } + } finally { + await browser.close(); + } + } catch (e) { + console.log('[diag] diagnostics error (non-fatal):', String(e)); + } +} diff --git a/playwright.config.ts b/playwright.config.ts index a823770..5086e7d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,6 +17,7 @@ const baseUrl = process.env.E2E_BASE_URL ?? 'https://flight.dev.kkhost.pl'; export default defineConfig({ testDir: './e2e', + globalSetup: './e2e/ci-diagnostics.ts', // Serial execution: e2e/state.spec.ts restarts the compose container mid-run, // which would break any map test running concurrently in another worker. workers: 1, -- 2.54.0 From 35493f92f0b9c24836b36560514ba0152a16f05e Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Sat, 19 Sep 2026 00:20:27 +0000 Subject: [PATCH 15/17] ci: cache playwright browsers across runs; drop no-op save step --- .gitea/workflows/ci.yml | 18 ++++++++++-------- CHANGELOG.md | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9cef9df..598ac62 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -84,14 +84,7 @@ jobs: done [ "$ok" = "1" ] || { echo "::error::dependency install failed after 3 attempts" >&2; exit 1; } - - name: Save deps cache - uses: actions/cache@v4 - continue-on-error: true - with: - path: | - node_modules - $HOME/.bun/install/cache - key: ${{ runner.os }}-deps-bun-${{ hashFiles('bun.lock') }} + # No explicit save step: caches marked by the restore steps are uploaded automatically at job end. - name: Build run: npm run build @@ -102,6 +95,15 @@ jobs: # Browsers run locally in CI (PW_REMOTE_BROWSER=0 below): the shared remote browser # server at 192.168.1.4 cannot reach a stack inside dind, and local chromium keeps # the job self-contained. + # Browser binaries (~187 MiB) cached across runs: the runner's uplink is flaky and + # cdn.playwright.dev downloads time out mid-transfer; once seeded, installs skip the download. + - name: Restore browser cache + uses: actions/cache@v4 + continue-on-error: true + with: + path: $HOME/.cache/ms-playwright + key: ${{ runner.os }}-pw-browsers-${{ hashFiles('package.json') }} + - name: Install browsers (local) run: npx playwright install --with-deps chromium diff --git a/CHANGELOG.md b/CHANGELOG.md index f72228a..b5d6691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - **CI e2e deploy command stays alive** (`E2E_DEPLOY_CMD` now ends with `docker compose … logs -f`): Playwright's webServer requires a long-running process — `up -d` returns immediately and triggered "Process from config.webServer exited early" (run #16); `logs -f` also streams app logs into CI output for debugging - **CI e2e step bypasses proxies for the in-network app host** (`NO_PROXY`/`no_proxy=app,localhost,127.0.0.1,.kkhost.pl`) and prints proxy/DNS/curl diagnostics before the suite — run #18 showed Chromium getting `ERR_SSL_PROTOCOL_ERROR` on http://app:3000 while node-side HTTP worked (9 API-only tests passed), pointing at a TLS-terminating proxy env var that Chromium honors but Node's HTTP client ignores - **CI e2e targets the app at a static in-network IP** (`172.39.0.10`, pinned subnet `172.39.0.0/24` created by "Prepare e2e network"; stale networks with other subnets are recreated) instead of the DNS alias — Chromium got `ERR_SSL_PROTOCOL_ERROR` via the hostname while node-side HTTP worked, so DNS is removed from the equation; post-deploy connectivity diagnostics added as a CI-gated globalSetup (`e2e/ci-diagnostics.ts`, no-op locally) comparing node resolution/HTTP against Chromium navigation on both hostname and static IP +- **CI caches Playwright browser binaries across runs** (`$HOME/.cache/ms-playwright` via actions/cache): the flaky runner uplink makes the ~187 MiB Chrome-for-Testing download unreliable (runs #20/#21 died mid-download at "Install browsers"); also removed the no-op "Save deps cache" step — on this runner caches marked by a restore step are uploaded automatically at job end, so an explicit save never did anything - 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 -- 2.54.0 From aa5eabeb789925b0775f6942336a1e3895916c9e Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Sat, 19 Sep 2026 06:49:30 +0000 Subject: [PATCH 16/17] =?UTF-8?q?ci:=20bounded=20retries=20around=20playwr?= =?UTF-8?q?ight=20browser=20install=20=E2=80=94=20flaky=20CDN,=20progress?= =?UTF-8?q?=20accumulates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 13 ++++++++++++- CHANGELOG.md | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 598ac62..b0bbce6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -104,8 +104,19 @@ jobs: path: $HOME/.cache/ms-playwright key: ${{ runner.os }}-pw-browsers-${{ hashFiles('package.json') }} + # Retried: cdn.playwright.dev is flaky from this runner — Playwright's internal 5-retry + # window (~40-90s) is often not enough, but runs get through with a longer one. Progress + # accumulates across attempts: playwright skips already-installed browsers. - name: Install browsers (local) - run: npx playwright install --with-deps chromium + run: | + set -u + ok=0 + for i in 1 2 3 4 5 6; do + if npx playwright install --with-deps chromium; then ok=1; break; fi + echo "::warning::browser install attempt $i/6 failed (flaky cdn.playwright.dev) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::browser install failed after 6 attempts" >&2; exit 1; } # 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index b5d6691..e4d4782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - **CI e2e step bypasses proxies for the in-network app host** (`NO_PROXY`/`no_proxy=app,localhost,127.0.0.1,.kkhost.pl`) and prints proxy/DNS/curl diagnostics before the suite — run #18 showed Chromium getting `ERR_SSL_PROTOCOL_ERROR` on http://app:3000 while node-side HTTP worked (9 API-only tests passed), pointing at a TLS-terminating proxy env var that Chromium honors but Node's HTTP client ignores - **CI e2e targets the app at a static in-network IP** (`172.39.0.10`, pinned subnet `172.39.0.0/24` created by "Prepare e2e network"; stale networks with other subnets are recreated) instead of the DNS alias — Chromium got `ERR_SSL_PROTOCOL_ERROR` via the hostname while node-side HTTP worked, so DNS is removed from the equation; post-deploy connectivity diagnostics added as a CI-gated globalSetup (`e2e/ci-diagnostics.ts`, no-op locally) comparing node resolution/HTTP against Chromium navigation on both hostname and static IP - **CI caches Playwright browser binaries across runs** (`$HOME/.cache/ms-playwright` via actions/cache): the flaky runner uplink makes the ~187 MiB Chrome-for-Testing download unreliable (runs #20/#21 died mid-download at "Install browsers"); also removed the no-op "Save deps cache" step — on this runner caches marked by a restore step are uploaded automatically at job end, so an explicit save never did anything +- **CI browser install runs in a bounded 6-attempt loop** — cdn.playwright.dev is flaky from the runner and Playwright's internal retry window (~40–90s) was often insufficient (runs #21–#23), while longer windows get through (runs #12/#18); already-installed browsers are skipped so progress accumulates across attempts - 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 -- 2.54.0 From 97f82dd4645d7ac985e3a25560ab220fec3905bc Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Sat, 19 Sep 2026 07:55:29 +0000 Subject: [PATCH 17/17] =?UTF-8?q?ci:=20self-hosted=20map=20assets=20for=20?= =?UTF-8?q?e2e=20=E2=80=94=20download=20+=20cache=20pack,=20mount=20./publ?= =?UTF-8?q?ic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + docker-compose.ci.yml | 4 ++++ 3 files changed, 35 insertions(+) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b0bbce6..f05202c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -86,6 +86,36 @@ jobs: # No explicit save step: caches marked by the restore steps are uploaded automatically at job end. + # Map e2e tests abort ALL *.openfreemap.org traffic (page.route → route.abort()), assuming + # styles/fonts/sprites are SELF-HOSTED from /styles, /fonts, /sprites — locally that pack is + # produced by `npm run assets:download` and reaches the app via the compose volume mount. + # CI must reproduce it or lib/mapStyles.ts probes /map-assets-manifest.json, finds nothing, + # falls back to remote OFM styles — which the tests abort → style never loads (the + # deterministic map.spec.ts wheel-zoom + style-switcher failures). The script needs no env + # vars/args and writes into ./public (git-ignored, docker-excluded; the app container gets + # it via the volume mount in docker-compose.ci.yml). + - name: Restore map assets cache + uses: actions/cache@v4 + continue-on-error: true + with: + path: public/ + key: ${{ runner.os }}-map-assets-${{ hashFiles('scripts/download-map-assets.mjs') }} + + # Retried: the runner's uplink is flaky (same transient EAI_AGAIN pattern as dep install). + # The job-end cache upload seeds public/ for future runs; a partial pack without + # map-assets-manifest.json is harmless — the app probes that manifest and falls back to + # remote styles when it's missing. + - name: Download map assets (retries) + run: | + set -u + ok=0 + for i in 1 2 3 4 5 6; do + if npm run assets:download; then ok=1; break; fi + echo "::warning::map assets download attempt $i/6 failed (likely transient network) — retrying in 5s" >&2 + sleep 5 + done + [ "$ok" = "1" ] || { echo "::error::map assets download failed after 6 attempts" >&2; exit 1; } + - name: Build run: npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d4782..6e32279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - **CI caches Playwright browser binaries across runs** (`$HOME/.cache/ms-playwright` via actions/cache): the flaky runner uplink makes the ~187 MiB Chrome-for-Testing download unreliable (runs #20/#21 died mid-download at "Install browsers"); also removed the no-op "Save deps cache" step — on this runner caches marked by a restore step are uploaded automatically at job end, so an explicit save never did anything - **CI browser install runs in a bounded 6-attempt loop** — cdn.playwright.dev is flaky from the runner and Playwright's internal retry window (~40–90s) was often insufficient (runs #21–#23), while longer windows get through (runs #12/#18); already-installed browsers are skipped so progress accumulates across attempts - 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 +- **CI e2e now runs in self-hosted map-assets mode** — the workflow downloads the OpenFreeMap asset pack (`npm run assets:download` → styles/fonts/sprites into git-ignored `./public`, bounded 6-attempt retry like the other flaky-network steps) and caches it via actions/cache keyed on the download script (job-end upload seeds future runs; a partial pack without manifest is harmless — app falls back to remote); `docker-compose.ci.yml` mounts `./public:/app/public` mirroring docker-compose.yml, so map tests that abort all *.openfreemap.org traffic get self-hosted styles/fonts/sprites like local dev (fixes the deterministic map.spec.ts wheel-zoom + style-switcher failures) ## v0.12.2 — 2026-09-18 14:06 - **Bugfix (ISS-003): route leg lines were drawn as straight two-point chords instead of following the curved great-circle path planes actually fly.** Root cause in `lib/mapData.ts`: `routesGeoJson` built each flying plane's leg LineString from only its origin and destination airport coordinates, so on long routes the drawn line cut across the chord (up to ~40 km off the true arc for a ~1400 km leg) while the plane marker moved along the curved path. Fix: new pure `greatCircleArc(from, to)` samples the same slerp used by the sim (`positionAt` in `lib/sim/movement.ts`) at t = i/24 for i = 0..24 — **25 coordinates** (exact endpoints + 23 intermediate points, `ROUTE_ARC_SEGMENTS = 24`) — so the drawn line matches the curved flight path; `routesGeoJson` now uses it instead of the two-point chord diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 12615ab..3e89c74 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -15,6 +15,10 @@ services: environment: FLIGHT_TEST_HOOKS: "1" volumes: + # Self-hosted map-asset pack (styles/fonts/sprites): the workflow downloads it into ./public + # before build; absent/empty → app falls back to remote OFM styles. Mirrors docker-compose.yml's + # mount so e2e runs in self-hosted mode like local dev (map tests abort openfreemap.org traffic). + - "./public:/app/public" - flight-db:/app/db networks: flight-ci-e2e: -- 2.54.0