Fix layout to keep title controls on the radical's line #181
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| static: | |
| name: Lint / typecheck / build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version: "24" | |
| cache: true | |
| - run: vp install --frozen-lockfile | |
| - run: vp check | |
| - run: vp run typecheck | |
| - run: vp node scripts/check-v8-ignore-count.mjs | |
| - run: vp node scripts/check-deploy-scripts-safety.mjs | |
| - run: vp run check:data | |
| - run: vp run check:trs | |
| - run: vp run check:css-ids | |
| # The legacy vendor bundle has 16 errors + 2 warnings that must remain | |
| # byte-for-byte intact. The baseline gate accepts exactly those findings | |
| # and fails on any addition, removal, severity change, or rule change. | |
| - run: vp run check:css-lint | |
| - run: vp run build | |
| unit: | |
| name: Unit tests (vitest) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version: "24" | |
| cache: true | |
| - run: vp install --frozen-lockfile | |
| # --coverage applies the 100% ratchet from vite.config.ts. | |
| - run: vp run test:unit --coverage --reporter=default --reporter=junit --outputFile=unit-report.xml | |
| # Focused release-orchestrator coverage gate (scripts/release-*.mjs are | |
| # outside the global src/** ratchet). Broke silently once (#144) when it | |
| # was manual-only — keep it in CI. | |
| - run: vp run test:coverage:release | |
| - name: Upload unit report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: unit-report | |
| path: unit-report.xml | |
| if-no-files-found: ignore | |
| integration: | |
| name: Worker integration tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version: "24" | |
| cache: true | |
| - run: vp install --frozen-lockfile | |
| - run: vp run test:integration | |
| e2e: | |
| name: E2E (Chromium + focused WebKit) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| env: | |
| E2E_SKIP_BUILD: "1" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version: "24" | |
| cache: true | |
| - run: vp install --frozen-lockfile | |
| - name: Get Playwright version | |
| id: pw | |
| run: echo "version=$(vp node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT" | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v6 | |
| id: pw-cache | |
| with: | |
| path: ~/.cache/ms-playwright | |
| # Cache key explicitly includes both browsers so a cache that only | |
| # has chromium does not false-hit and skip the webkit install. | |
| key: pw-chromium-webkit-${{ runner.os }}-${{ steps.pw.outputs.version }} | |
| - name: Install Playwright browsers | |
| if: steps.pw-cache.outputs.cache-hit != 'true' | |
| run: vp exec playwright install --with-deps chromium webkit | |
| - name: Install Playwright OS deps (for cached browsers) | |
| if: steps.pw-cache.outputs.cache-hit == 'true' | |
| run: vp exec playwright install-deps chromium webkit | |
| - run: vp run build | |
| - run: vp exec playwright test --project=chromium | |
| - run: vp exec playwright test --project=webkit-romanization | |
| - name: Upload Playwright report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-report | |
| path: | | |
| playwright-report/ | |
| test-results/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| contrast: | |
| name: Dark/light contrast audit | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version: "24" | |
| cache: true | |
| - run: vp install --frozen-lockfile | |
| - run: vp exec playwright install --with-deps chromium | |
| - run: vp run build | |
| - name: Run contrast audit against seeded Miniflare fixture server | |
| env: | |
| BASE_URL: http://127.0.0.1:8877 | |
| CONTRAST_AUDIT_OUTPUT: contrast-audit.json | |
| E2E_PORT: "8877" | |
| run: | | |
| set -euo pipefail | |
| # Real audit target: the same seeded Miniflare fixture server the | |
| # Chromium e2e suite uses (tests/e2e/serve.ts), NOT `vp preview` | |
| # (wrangler dev over the static dist/ build only — /api/* 404s | |
| # there, which made every contrast-audit route 404 in CI). This | |
| # server has R2 fixtures (dictionary entries, fonts, assets) | |
| # seeded in-process, so every audited route hydrates for real. | |
| # | |
| # Launch `node` directly (not `vp run serve:e2e`) so `$!` is the | |
| # actual server process, not an intermediate `vp` wrapper: `vp run | |
| # <script> &` backgrounds the `vp` CLI itself, which does NOT | |
| # forward SIGTERM to its own child process tree on exit -- killing | |
| # that PID alone left tsx/node and the workerd child running and | |
| # the port still accepting connections (reproduced locally: after | |
| # `kill "$server_pid"` on a `vp run serve:e2e &` launch, `pgrep -f | |
| # workerd` and `pgrep -f tests/e2e/serve.ts` both still matched, | |
| # and curl still got a live response). `node --import tsx | |
| # tests/e2e/serve.ts` runs the script directly under `node` with | |
| # no wrapper layer, so `$!` is the exact PID whose SIGTERM handler | |
| # (Miniflare registers its own SIGTERM listener during | |
| # startTestServer() and disposes the workerd child + calls | |
| # process.exit() from it before serve.ts's own handler even runs) | |
| # actually tears down the workerd child. Verified locally: this | |
| # exact invocation, SIGTERM'd after ready, leaves zero matches for | |
| # `pgrep -f workerd` / `pgrep -f tests/e2e/serve.ts` and the port | |
| # immediately refuses connections. | |
| E2E_SKIP_BUILD=1 node --import tsx tests/e2e/serve.ts > serve.log 2>&1 & | |
| server_pid=$! | |
| # Bounded escalation, scoped to only this server's own process | |
| # tree -- never a name-based `pkill -f`. This runner may be | |
| # shared/concurrent (self-hosted or otherwise), and a broad `pkill | |
| # -f tests/e2e/serve.ts` / `pkill -f "workerd serve"` would match | |
| # (and kill) an unrelated job's identically-named process, not | |
| # just this job's own. Enumerate the exact descendant PIDs (via | |
| # recursive `pgrep -P`) up front, while the process is still | |
| # alive, since a process's children can't be discovered anymore | |
| # once it's gone. Send TERM to the server PID only (lets | |
| # Miniflare/serve.ts dispose workerd and close the socket | |
| # cleanly -- see the launch comment above), wait for it to | |
| # actually exit, then KILL only the specific PIDs captured | |
| # earlier that are still alive -- never a fresh pgrep/pkill scan | |
| # at cleanup time, which could by then match a different, | |
| # unrelated process that happens to reuse a freed PID or matches | |
| # by name. | |
| descendant_pids() { | |
| local pid="$1" children child | |
| children=$(pgrep -P "$pid" 2>/dev/null || true) | |
| for child in $children; do | |
| echo "$child" | |
| descendant_pids "$child" | |
| done | |
| } | |
| cleanup() { | |
| local pid | |
| if ! kill -0 "$server_pid" 2>/dev/null; then | |
| return | |
| fi | |
| local tree_pids | |
| tree_pids="$server_pid $(descendant_pids "$server_pid")" | |
| kill -TERM "$server_pid" 2>/dev/null || true | |
| for _ in $(seq 1 10); do | |
| kill -0 "$server_pid" 2>/dev/null || break | |
| sleep 1 | |
| done | |
| for pid in $tree_pids; do | |
| kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true | |
| done | |
| } | |
| trap cleanup EXIT | |
| ready=false | |
| # Poll a seeded ASSETS-bucket font, not /api/<word>.json: the | |
| # dictionary API path is read through readR2JsonCached's | |
| # per-isolate memo (src/api/r2-json-cache.ts), which caches a | |
| # MISS as null for 10 minutes -- a probe hit landing before the | |
| # DICTIONARY bucket finishes seeding would poison that key for | |
| # the rest of the job, no matter how long seeding then takes. | |
| # serveAssetWithFallback reads ASSETS directly with no such memo, | |
| # so this only turns 200 once the fixture is genuinely seeded. | |
| for i in $(seq 1 60); do | |
| if curl -fsS "$BASE_URL/assets/fonts/MOEDICT.woff2" >/dev/null; then ready=true; break; fi | |
| sleep 1 | |
| done | |
| test "$ready" = true | |
| vp run audit:contrast -- --ci | |
| - name: Upload contrast audit report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: contrast-audit | |
| path: | | |
| contrast-audit.json | |
| serve.log | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| visual: | |
| name: Visual regression (playwright) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # Visual tests are opt-in: they require committed linux baselines in | |
| # tests/e2e/visual-snapshots.spec.ts-snapshots/*-chromium-linux.png. If the | |
| # baseline is missing, the job is a no-op (exit 0). See README for how | |
| # to regenerate (needs Linux: use `act`, Codespaces, or a Linux runner). | |
| continue-on-error: true | |
| env: | |
| E2E_SKIP_BUILD: "1" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version: "24" | |
| cache: true | |
| - run: vp install --frozen-lockfile | |
| - name: Check if linux baselines exist | |
| id: baselines | |
| run: | | |
| if ls tests/e2e/visual-snapshots.spec.ts-snapshots/*-chromium-linux.png 2>/dev/null; then | |
| echo "exist=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exist=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Install Playwright | |
| if: steps.baselines.outputs.exist == 'true' | |
| run: vp exec playwright install --with-deps chromium | |
| - run: vp run build | |
| if: steps.baselines.outputs.exist == 'true' | |
| - run: vp exec playwright test --project=visual | |
| if: steps.baselines.outputs.exist == 'true' | |
| - name: Upload visual diff report | |
| if: always() && steps.baselines.outputs.exist == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: visual-report | |
| path: | | |
| playwright-report/ | |
| test-results/ | |
| if-no-files-found: ignore | |
| retention-days: 14 |