ci: e2e — static in-network IP for the app (bypass DNS) + post-deploy chromium diagnostics
This commit is contained in:
+18
-17
@@ -17,19 +17,30 @@ jobs:
|
|||||||
|
|
||||||
# The runner provides a working docker CLI out of the box (full-dind) — no endpoint
|
# 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
|
# 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
|
# user-defined network with a pinned subnet; the app gets a STATIC IP (172.39.0.10,
|
||||||
# default route here, so host-IP URLs are unreliable). The alias `app` is declared
|
# declared in docker-compose.ci.yml) because Chromium got ERR_SSL_PROTOCOL_ERROR via
|
||||||
# in docker-compose.ci.yml.
|
# 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
|
- name: Prepare e2e network
|
||||||
run: |
|
run: |
|
||||||
set -u
|
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
|
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
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
docker network inspect flight-ci-e2e --format 'containers on flight-ci-e2e: {{range .Containers}}{{.Name}} {{end}}'
|
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
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -132,18 +143,8 @@ jobs:
|
|||||||
# in-network app host out of any proxy path.
|
# 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
|
||||||
no_proxy: app,localhost,127.0.0.1,.kkhost.pl
|
no_proxy: app,localhost,127.0.0.1,.kkhost.pl
|
||||||
run: |
|
# Connectivity diagnostics now run post-deploy in e2e/ci-diagnostics.ts (globalSetup).
|
||||||
# --- e2e connectivity diagnostics (non-fatal; evidence for ERR_SSL_PROTOCOL_ERROR hunt) ---
|
run: npx playwright test
|
||||||
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.
|
# 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).
|
# -v drops the named DB volume so every run seeds fresh (no schema drift between PRs).
|
||||||
|
|||||||
@@ -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
|
- **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 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 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
|
- 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
|
## v0.12.2 — 2026-09-18 14:06
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# Isolated e2e stack for CI (.gitea/workflows/ci.yml) — runs on the runner's shared dind
|
# 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
|
# 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
|
# 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,
|
# 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
|
# 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
|
# (fresh per run — CI cleanup removes it — survives `docker compose restart flight` for
|
||||||
@@ -18,6 +20,7 @@ services:
|
|||||||
flight-ci-e2e:
|
flight-ci-e2e:
|
||||||
aliases:
|
aliases:
|
||||||
- app
|
- app
|
||||||
|
ipv4_address: 172.39.0.10
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
flight-ci-e2e:
|
flight-ci-e2e:
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user