Skip to content

fix(offline): short-circuit corpus pointer lookup in Capacitor apps (… #171

fix(offline): short-circuit corpus pointer lookup in Capacitor apps (…

fix(offline): short-circuit corpus pointer lookup in Capacitor apps (… #171

Workflow file for this run

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