Files
2026-09-19 10:35:36 +00:00

204 lines
10 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
# Interrupted runs skip the end-of-run cleanup, leaking a USED DB volume into later
# runs (tests assume seeded state) — drop it so every run starts fresh.
docker volume rm flight-ci_flight-db 2>/dev/null || true
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).
# --reporter=list prints each test as it completes (name + ✓/× + duration) for progress
# visibility in the job log; failure error context/snapshots are unchanged.
run: npx playwright test --reporter=list
# 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