name: PR Gate

on:
  pull_request:
    branches: [main]

concurrency:
  group: pr-gate-${{ github.event.pull_request.number }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  gate:
    name: Full gate
    # Self-hosted runner + pull_request = arbitrary code execution from a fork PR.
    if: github.event.pull_request.head.repo.full_name == github.repository
    # Dedicated CI box (overdeck-debian1/2). Label must match register-runner.sh --label overdeck-gate.
    runs-on: [self-hosted, linux, x64, overdeck-gate]
    timeout-minutes: 60
    env:
      CI: true
      # Block a runner login session from leaking a physical display (often :0 forwarded to the laptop).
      DISPLAY: ""
      # CI runs tests with wall-clock timeouts; under SCHED_IDLE a busy box starves them.
      BUILD_SCHED_IDLE: "0"
      CI_GATE_TREE_SHA: ${{ github.sha }}
      GITHUB_RUN_ID: ${{ github.run_id }}
      GITHUB_RUN_ATTEMPT: ${{ github.run_attempt }}
    steps:
      - name: Checkout
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

      - name: Setup pnpm
        uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
        with:
          # action-setup installs into a fixed ~/setup-pnpm by default. Several repo-scoped
          # runners share one box here, so concurrent jobs raced and left a truncated binary
          # (exit 127) or EACCES. runner.temp is per-job, which removes the shared path.
          dest: ${{ runner.temp }}/setup-pnpm

      - name: Setup Node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: 22

      - name: Put bun on PATH
        run: echo "$HOME/.bun/bin" >> "$GITHUB_PATH"

      - name: Install
        # pnpm derives its store from PNPM_HOME, which the dest above moved into
        # the per-job _temp dir — without this the store is wiped every run.
        run: pnpm install --frozen-lockfile --store-dir "$RUNNER_TOOL_CACHE/pnpm-store"

      # The box already carries the playwright system libraries, so --with-deps is omitted: it
      # needs sudo and would fail on a user-scope runner. Browser builds land in the shared
      # ~/.cache/ms-playwright and are reused across every repo on this host.
      - name: Install Playwright browsers
        run: pnpm exec playwright install chromium

      - name: Gate
        run: pnpm run ci:gate

      # Runner box owns the disk; GitHub's Actions/Packages storage quota is a shared
      # account-wide pool across every repo and fills from unrelated activity (container
      # image pushes, other repos' artifacts) — never a place to park receipts we control.
      - name: Persist CI receipts on runner
        if: always()
        run: |
          set -euo pipefail
          dest_root="$HOME/ci-receipts/overdeck"
          dest="$dest_root/${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}"
          mkdir -p "$dest_root"
          rm -rf "$dest"
          cp -a .ci-results "$dest"
          find "$dest_root" -maxdepth 1 -mindepth 1 -mtime +30 -exec rm -rf {} +
          printf '### CI receipts\n\nSaved on runner: `%s`\n' "$dest" >> "$GITHUB_STEP_SUMMARY"

      # Gated to hosted runners only (this job never is one): the step must exist so the
      # workflow contract test (tools/ci/workflow-contract.mjs) can keep asserting its
      # shape, but it must never actually upload to GitHub's shared storage quota.
      - name: Upload CI receipts
        if: always() && runner.environment == 'github-hosted'
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: ci-gate-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }}
          path: .ci-results/
          include-hidden-files: true
          if-no-files-found: error
