Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2e7a7430 | ||
|
|
97f82dd464 | ||
|
|
aa5eabeb78 | ||
|
|
35493f92f0 | ||
|
|
e694011302 | ||
|
|
0bfbd2a2e8 | ||
|
|
b112339dd3 | ||
|
|
f89f0407ae | ||
|
|
14aaf26900 | ||
|
|
c30f5f650f | ||
|
|
54c7a0c5f5 | ||
|
|
d0149c6ab3 | ||
|
|
579d425b64 | ||
|
|
543aad310c | ||
|
|
5ee0d89826 | ||
|
|
47381f2011 | ||
|
|
775b0e479a | ||
|
|
0c921556fb |
+5
-2
@@ -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
|
.next
|
||||||
data/*.db
|
data/*.db
|
||||||
# game state DB (v0.6.0): host-side, volume-mounted at runtime — never baked in
|
# game state DB (v0.6.0): host-side, volume-mounted at runtime — never baked in
|
||||||
@@ -8,7 +10,8 @@ test-results
|
|||||||
.git
|
.git
|
||||||
.env
|
.env
|
||||||
.codenomad
|
.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
|
e2e
|
||||||
playwright.config.ts
|
playwright.config.ts
|
||||||
# docs/deployment files never affect the app build — excluding them keeps
|
# docs/deployment files never affect the app build — excluding them keeps
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
name: build + unit + e2e
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 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 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
|
||||||
|
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://172.39.0.10:3000" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- 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 1.4.2.
|
||||||
|
# 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; }
|
||||||
|
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.
|
||||||
|
- 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') }}
|
||||||
|
|
||||||
|
# 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; }
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
# 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') }}
|
||||||
|
|
||||||
|
# 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: |
|
||||||
|
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.
|
||||||
|
- 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)
|
||||||
|
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
|
||||||
|
# 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. E2E_BASE_URL comes from GITHUB_ENV (Prepare e2e network).
|
||||||
|
- name: E2E (isolated stack in dind)
|
||||||
|
env:
|
||||||
|
PW_REMOTE_BROWSER: "0"
|
||||||
|
# Build separately with plain progress so BuildKit step output is visible in the
|
||||||
|
# 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"
|
||||||
|
# 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
|
||||||
|
# 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).
|
||||||
|
- 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
|
||||||
@@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
## Change process (mandatory)
|
## 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.
|
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<version> — YYYY-MM-DD HH:MM`, then bullet points of what changed).
|
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. **Version bump** — the version is updated in `package.json`.
|
3. **Changelog entry** — every such change is listed in `CHANGELOG.md` with date, time and version (format: `## v<version> — YYYY-MM-DD HH:MM`, then bullet points of what changed).
|
||||||
4. **Commit + tag** — a commit is created for each change, tagged with the version (e.g. `v0.1.0`), push it to remote repo.
|
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)
|
## 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).
|
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** — deploy current code first: `docker compose up -d --build`.
|
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** — 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`).
|
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
|
## API
|
||||||
1. **Live base URL** — `https://flight.dev.kkhost.pl` (the docker-compose build behind Traefik).
|
1. **Live base URL** — `https://flight.dev.kkhost.pl` (the docker-compose build behind Traefik).
|
||||||
|
|||||||
@@ -3,6 +3,21 @@
|
|||||||
## v0.12.3 — 2026-09-19 07:36
|
## 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)
|
- **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
|
||||||
|
- **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
|
||||||
|
- **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
|
||||||
|
- **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
|
## 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
|
- **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
|
||||||
|
|||||||
+13
-18
@@ -1,21 +1,14 @@
|
|||||||
# Multi-stage build: bun for installs (fast), node for build + runtime.
|
# Two-stage build needing ZERO outbound network at image-build time:
|
||||||
# No BuildKit type=cache mounts: they don't persist across builds on this daemon
|
# (a) deps are installed by the CI job / dev host (`bun install`) and copied in via the
|
||||||
# (Docker 29 + containerd image store), and bun install is fast enough (~3s cold)
|
# build context — the runner's dind daemon has broken outbound DNS for build containers,
|
||||||
# that cross-build tarball caching isn't worth the complexity.
|
# so any RUN that fetches packages fails;
|
||||||
FROM oven/bun:1.4.2-alpine AS deps
|
# (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
|
WORKDIR /app
|
||||||
COPY package.json bun.lock ./
|
COPY package.json bun.lock ./
|
||||||
RUN bun install --frozen-lockfile
|
COPY node_modules ./node_modules
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
FROM node:22-alpine AS build
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@@ -23,10 +16,12 @@ RUN npm run build
|
|||||||
# No `output: 'standalone'`: the runtime stage keeps node_modules because
|
# No `output: 'standalone'`: the runtime stage keeps node_modules because
|
||||||
# /api/mlworker/[file] reads maplibre-gl dist files from disk at runtime, and
|
# /api/mlworker/[file] reads maplibre-gl dist files from disk at runtime, and
|
||||||
# /api/airports reads data/*.csv — both resolved relative to process.cwd().
|
# /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
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
|
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/.next ./.next
|
||||||
COPY --from=build /app/package.json ./package.json
|
COPY --from=build /app/package.json ./package.json
|
||||||
COPY --from=build /app/data ./data
|
COPY --from=build /app/data ./data
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"sql.js": "^1.14.2",
|
"sql.js": "^1.14.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.63.0",
|
||||||
"@types/node": "^22.20.3",
|
"@types/node": "^22.20.3",
|
||||||
"@types/react": "^19.3.0",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^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=="],
|
"@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-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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"potpack": ["potpack@2.1.0", "", {}, "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="],
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# e2e/state.spec.ts).
|
||||||
|
services:
|
||||||
|
flight:
|
||||||
|
build: .
|
||||||
|
image: flight-ops-ci
|
||||||
|
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:
|
||||||
|
aliases:
|
||||||
|
- app
|
||||||
|
ipv4_address: 172.39.0.10
|
||||||
|
|
||||||
|
networks:
|
||||||
|
flight-ci-e2e:
|
||||||
|
name: flight-ci-e2e
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
flight-db:
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -3,8 +3,10 @@ import path from 'node:path';
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
// Repo root — docker-compose.yml lives here; `docker compose restart` runs on
|
// 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 + project (-f docker-compose.ci.yml -p flight-ci) instead of the default one.
|
||||||
const repoRoot = path.join(__dirname, '..');
|
const repoRoot = path.join(__dirname, '..');
|
||||||
|
const composeArgs = process.env.E2E_COMPOSE_ARGS ?? '';
|
||||||
|
|
||||||
interface StateBody {
|
interface StateBody {
|
||||||
account: { cash: number; reputation: number };
|
account: { cash: number; reputation: number };
|
||||||
@@ -46,7 +48,7 @@ test('state survives a container restart (sim clock + account persisted)', async
|
|||||||
expect(resBefore.status()).toBe(200);
|
expect(resBefore.status()).toBe(200);
|
||||||
const before = (await resBefore.json()) as StateBody;
|
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.
|
// Wait for the app to come back, capturing the post-restart snapshot.
|
||||||
let after: StateBody | undefined;
|
let after: StateBody | undefined;
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flight-ops",
|
"name": "flight-ops",
|
||||||
"version": "0.12.3",
|
"version": "0.13.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000",
|
"dev": "next dev -p 3000",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"sql.js": "^1.14.2"
|
"sql.js": "^1.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.63.0",
|
||||||
"@types/node": "^22.20.3",
|
"@types/node": "^22.20.3",
|
||||||
"@types/react": "^19.3.0",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19.3.0",
|
"@types/react-dom": "^19.3.0",
|
||||||
|
|||||||
+17
-12
@@ -1,28 +1,33 @@
|
|||||||
import { defineConfig } from '@playwright/test';
|
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/';
|
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({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
|
globalSetup: './e2e/ci-diagnostics.ts',
|
||||||
// Serial execution: e2e/state.spec.ts restarts the compose container mid-run,
|
// Serial execution: e2e/state.spec.ts restarts the compose container mid-run,
|
||||||
// which would break any map test running concurrently in another worker.
|
// which would break any map test running concurrently in another worker.
|
||||||
workers: 1,
|
workers: 1,
|
||||||
timeout: 300_000,
|
timeout: 300_000,
|
||||||
retries: 1,
|
retries: 1,
|
||||||
use: {
|
use: {
|
||||||
baseURL: process.env.E2E_BASE_URL ?? 'https://flight.dev.kkhost.pl',
|
baseURL: baseUrl,
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
},
|
},
|
||||||
webServer: remoteRunner
|
webServer: deployCmd
|
||||||
? undefined
|
? { command: deployCmd, url: baseUrl, reuseExistingServer: true, timeout: 600_000 }
|
||||||
: {
|
: undefined,
|
||||||
command: 'docker compose up -d --build',
|
});
|
||||||
url: 'https://flight.dev.kkhost.pl',
|
|
||||||
reuseExistingServer: true,
|
|
||||||
timeout: 600_000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
Reference in New Issue
Block a user