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
    # k3s runner scale set (modules/ci/arc/runner-values.yaml). The label IS the
    # scale-set name; anything else queues forever with nothing in the UI to say so.
    # Each job gets a fresh pod from a side-loaded image, so the toolchain is the
    # image's problem, not a step's -- see .github/workflows/arc-k3s-smoke.yml,
    # which asserts bun/node/pnpm and the chromium system libs this job used to
    # install itself.
    runs-on: arc-k3s
    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: 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. On
        # the k3s runner the pod is ephemeral and RUNNER_TOOL_CACHE dies with it,
        # so prefer PNPM_STORE_DIR: the scale set points it at the local-path PVC
        # mounted on /cache, which is the only path that survives a job.
        run: pnpm install --frozen-lockfile --store-dir "${PNPM_STORE_DIR:-$RUNNER_TOOL_CACHE/pnpm-store}"

      # The runner image bakes the playwright system libraries (arc-k3s-smoke.yml asserts
      # them), so --with-deps is omitted: it needs sudo and would fail on a non-root runner.
      # The browser binaries themselves are NOT baked and the pod's home is ephemeral, so
      # this genuinely re-downloads chromium each run — a per-job cost, not a leftover step.
      - 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
          # $HOME is a fresh ephemeral pod filesystem on the k3s runner, so a receipt
          # written there is gone before anyone reads it — the step would keep passing
          # while persisting nothing. /cache is the local-path PVC the scale set mounts;
          # prefer it whenever it is actually writable, and keep $HOME for any runner
          # that owns its own disk.
          if [ -w /cache ]; then dest_root="/cache/ci-receipts/overdeck"
          else dest_root="$HOME/ci-receipts/overdeck"; fi
          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
