From 0c921556fb60650b1f1bedd431b1ad31abecefdb Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Fri, 18 Sep 2026 15:35:06 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20v0.13.0=20=E2=80=94=20CI=20pipeline=20?= =?UTF-8?q?(build=20=E2=86=92=20unit=20=E2=86=92=20e2e)=20on=20PRs=20to=20?= =?UTF-8?q?master=20+=20push,=20dind-isolated=20e2e=20stack=20(docker-comp?= =?UTF-8?q?ose.ci.yml),=20playwright=20config=20env-var=20overrides=20(PW?= =?UTF-8?q?=5FREMOTE=5FBROWSER/E2E=5FDEPLOY=5FCMD/E2E=5FBASE=5FURL/E2E=5FC?= =?UTF-8?q?OMPOSE=5FARGS,=20backward-compatible),=20@playwright/test=20pin?= =?UTF-8?q?ned=20as=20devDependency;=20AGENTS.md=20switched=20to=20pull-re?= =?UTF-8?q?quest=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, +});