# Continuous integration. # # Two jobs, split by language, because they prove different things and the # Python one is the slow half. # # What this actually gates, in order of how likely each is to catch something: # # 1. The two implementations of the game agree. `engine.py` and `engine.ts` # score every ordered pair of words in the answer list — 4,603^2 = 21.2M # feedback patterns — and their SHA-256 digests must match each other and # the committed value. The page claims it VERIFIES a recorded run in your # browser; that claim is only worth something if the browser's scorer and # the environment's scorer cannot drift. # 2. The reward measures something. `probe.py` plays seven deliberately # crafted policies and asserts the reward orders them the way a person # would, PER WEIGHTED COMPONENT — a component that is flat across every # policy is measuring nothing and still moves the total. It also asserts # neither good policy dominates the other, because the reward editor on # the site is a claim about a real trade-off. # 3. Every committed fixture replays through the engine and reproduces its # own recorded rewards. A fixture that cannot be regenerated is a claim # with no receipt behind it. # 4. The word lists rebuild byte-identically from their committed sources. # If they do not, the conformance digest is describing a different game. # 5. The demo contract holds — see scripts/check-demos.mjs. # 6. Four things that fail SILENTLY in production and nowhere else: # prerendered routes carrying their own , sitemap.xml existing, # no blob-backed worker, and the bundle budget. # # NO MODEL IS EVER CALLED. Rollouts are captured by hand and committed; a gate # that cost a paid inference call would be switched off within a month. # # This runner is aarch64 and configured with `container.network: host`. Nothing # here needs a service container, so the traps that cost PIG three failed runs # do not apply — but do not add `services:` here without reading that repo's # workflow first. name: ci on: push: branches: [main] pull_request: jobs: web: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' # Corepack ships with Node and installs the exact pnpm pinned by # `packageManager`, so CI and a laptop run the same version. The download # prompt is disabled because a non-interactive runner cannot answer it and # would otherwise hang until the job times out. - name: Enable pnpm env: COREPACK_ENABLE_DOWNLOAD_PROMPT: '0' run: | corepack enable corepack prepare --activate test "$(pnpm -v)" = "11.21.0" || { echo "pnpm $(pnpm -v) != 11.21.0"; exit 1; } - name: Install env: CI: 'true' run: pnpm install --frozen-lockfile # `pnpm build` ends in the prerender pass, which drives a real browser. # Without this the build fails on its very last action, after everything # else has gone green — the most expensive place to find a missing # dependency. - name: Install chromium run: pnpm exec playwright install --with-deps chromium - run: pnpm typecheck - run: pnpm check - run: pnpm test - run: pnpm build - run: node scripts/bundle-budget.mjs # Crawlers do not run JavaScript. Without a real per-route HTML file every # shared link previews as the homepage, and the failure is invisible from # inside the app. - name: The prerendered head is real run: | test -f dist/demos/wordle/index.html || { echo "no prerendered demo route"; exit 1; } grep -q 'og:title' dist/demos/wordle/index.html || { echo "og tags missing"; exit 1; } grep -q 'Word Five' dist/demos/wordle/index.html \ || { echo "the demo route kept the homepage title"; exit 1; } test -f dist/404.html || { echo "no 404.html"; exit 1; } test -f dist/sitemap.xml || { echo "no sitemap.xml"; exit 1; } # The site's CSP has no `worker-src`, so it falls back to # `default-src 'self'` and a blob-backed worker is blocked with no console # error — in production only. Grep for construction FROM a blob, not for # the string anywhere: React's bundle contains it in a scheme check, and a # check that fails on a risk which is not present teaches everyone to # ignore it. - name: No blob-backed workers run: | if grep -rEo "new (Shared)?Worker\([^)]{0,80}" dist/assets/*.js \ | grep -E "blob:|createObjectURL"; then echo "a worker is constructed from a blob URL; production CSP blocks it silently" exit 1 fi echo "ok — no blob-backed worker construction in the bundle" python: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - run: uv sync --all-packages # If these do not reproduce, every downstream number — the conformance # digest included — is describing a different game. - name: The word lists rebuild identically run: | uv run python envs/wordle_five/words/build_words.py git diff --exit-code envs/wordle_five/words/*.json - run: uv run pytest envs/wordle_five/tests -q - run: uv run python envs/probe.py - run: uv run python envs/verify_fixtures.py # The other half of the cross-language gate. Node scores the same 21.2M # pairs and must produce the digest Python committed. - uses: actions/setup-node@v4 with: node-version: '22' - name: Enable pnpm env: COREPACK_ENABLE_DOWNLOAD_PROMPT: '0' run: | corepack enable corepack prepare --activate - name: Install env: CI: 'true' run: pnpm install --frozen-lockfile - run: pnpm conformance