name: ci

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: pnpm/action-setup@v4
        with: { version: 11.21.0 }
      - uses: actions/setup-node@v5
        with: { node-version: 22, cache: pnpm }

      # Asserts the toolchain matches what package.json pins, rather than
      # discovering a mismatch three steps later as an unrelated build error.
      - name: preflight
        run: |
          test "$(pnpm -v)" = "11.21.0" || { echo "pnpm $(pnpm -v) != 11.21.0"; exit 1; }

      - run: CI=true pnpm install --frozen-lockfile

      # `pnpm build` ends in the prerender pass, which drives a real browser.
      # Without this the build fails at the very last step, after everything
      # else has passed, with a Playwright message about installing browsers.
      - 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

      # The prerender pass writes a real HTML file per route. Crawlers do not
      # run JavaScript, so without these every shared link previews as the
      # homepage — assert the baked tags actually landed.
      - name: 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 -qv 'PIG Demo — RL environments you can play</title>' dist/demos/wordle/index.html \
            || { echo "demo route kept the homepage title"; exit 1; }
          test -f dist/404.html || { echo "no 404.html"; exit 1; }

      # A blob-backed worker is blocked in production and nowhere else: the
      # site's CSP has no worker-src, so it falls back to default-src 'self'.
      # The failure is silent — the worker simply never boots.
      #
      # Grep for worker construction FROM a blob, not for the string `blob:`
      # anywhere. React's own bundle contains that string in a URL-scheme check,
      # so the broad version fails on a risk that is not present, which 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@v5
      - uses: astral-sh/setup-uv@v6
        with: { enable-cache: true }

      - run: uv sync --all-packages

      # Rebuild the word lists from their committed sources and assert the
      # output is byte-identical. If it is not, every downstream number —
      # the conformance digest included — is describing a different game.
      - name: 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

      # The cross-language gate. Both halves score all 21.2M (guess, answer)
      # pairs; the digests must match each other and the committed value.
      - uses: pnpm/action-setup@v4
        with: { version: 11.21.0 }
      - uses: actions/setup-node@v5
        with: { node-version: 22, cache: pnpm }
      - run: CI=true pnpm install --frozen-lockfile
      - run: pnpm conformance

      # Every committed fixture must replay through the Python engine and
      # reproduce its own recorded rewards. A fixture that cannot be
      # regenerated is a claim with no receipt behind it.
      - run: uv run python envs/verify_fixtures.py
