name: CI

# Remove default permissions of GITHUB_TOKEN for security
# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
permissions: {}

on:
  push:
    branches:
      - main

  pull_request:

  workflow_dispatch:

concurrency:
  group: ci-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright
  # Trust the committed lockfile and skip pnpm's per-install supply-chain
  # verification (trustPolicy: no-downgrade, ~15-20s on every job). The lint job
  # overrides this to false so the lockfile is still verified once per run.
  PNPM_CONFIG_TRUST_LOCKFILE: true

jobs:
  lint:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    name: 'Lint: node-latest, ubuntu-latest'
    # verify the lockfile against supply-chain policies once per run
    env:
      PNPM_CONFIG_TRUST_LOCKFILE: false
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - uses: ./.github/actions/setup-and-cache

      - name: Install
        run: pnpm i --filter '!docs'

      - name: Build
        run: pnpm run build

      - name: Generate CLI docs
        run: pnpm -C docs run cli-table

      # check uncommited LICENSE.md, auto-imports.d.ts, etc...
      - name: Check stale build artifacts
        run: git diff --exit-code

      - name: Lint
        run: pnpm run lint

      - name: Typecheck
        run: pnpm run typecheck

      - name: Typecheck UI
        run: pnpm run -C packages/ui typecheck:client

      # From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
      - name: Check workflow files
        run: |
          bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
          ./actionlint -color -shellcheck=""

  changed:
    runs-on: ubuntu-latest
    name: 'Diff: node-latest, ubuntu-latest'
    outputs:
      should_skip: ${{ steps.changed-files.outputs.only_changed == 'true' }}

    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
        with:
          files: |
            docs/**
            .github/**
            !.github/workflows/ci.yml
            **.md

  test:
    needs: changed
    name: 'Test: ${{ matrix.suite }}, node-${{ matrix.node_version }}, ${{ matrix.os }}'
    if: needs.changed.outputs.should_skip != 'true'
    runs-on: ${{ matrix.os }}

    timeout-minutes: 30

    strategy:
      # Split the heavy e2e and coverage suites onto their own runners so they run
      # in parallel with the unit suite instead of sequentially on a single machine.
      # Linux runs the full matrix on node 24/26; node 22 (the minimum supported
      # version, currently in maintenance LTS) gets a single e2e leg as a floor
      # smoke. macOS runs one e2e leg as an Apple-hardware smoke; unit/coverage are
      # redundant on both.
      matrix:
        suite: [unit, e2e, coverage]
        os: [ubuntu-latest]
        node_version: [24, 26]
        include:
          - suite: e2e
            os: ubuntu-latest
            node_version: 22
          - suite: e2e
            os: macos-latest
            node_version: 24
          - suite: unit
            os: windows-latest
            node_version: 24
          - suite: e2e
            os: windows-latest
            node_version: 24
          - suite: coverage
            os: windows-latest
            node_version: 24
      fail-fast: false

    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - uses: ./.github/actions/setup-and-cache
        with:
          node-version: ${{ matrix.node_version }}

      - name: Install
        run: pnpm i --filter '!docs'

      - uses: ./.github/actions/setup-playwright
        with:
          browsers: chromium

      - name: Build
        run: pnpm run build

      - name: Test
        # matrix.suite is a fixed, workflow-defined value (unit/e2e/coverage)
        run: pnpm run test:ci:${{ matrix.suite }} # zizmor: ignore[template-injection]
        env:
          VITEST_CI_BLOB_LABEL: ${{ matrix.os }}-node-${{ matrix.node_version }}

      - name: Test Examples
        if: matrix.suite == 'unit' && matrix.os == 'ubuntu-latest'
        run: pnpm run test:examples

      - name: Unit Test UI
        if: matrix.suite == 'unit'
        run: pnpm -C packages/ui test:ui
        env:
          VITEST_CI_BLOB_LABEL: ${{ matrix.os }}-node-${{ matrix.node_version }}

      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: vitest-results-${{ matrix.suite }}-${{ matrix.os }}-node-${{ matrix.node_version }}
          path: |
            README.md
            test/unit/.vitest
            test/e2e/.vitest
            packages/ui/.vitest
          retention-days: 1
          include-hidden-files: true

  test-browser:
    needs: changed
    name: 'Browsers: ${{ matrix.name }}, node-24, ${{ matrix.os }}'
    if: needs.changed.outputs.should_skip != 'true'

    runs-on: ${{ matrix.os }}
    strategy:
      # runner.test.ts is the heaviest spec (~28% of the suite, ~70% of whichever
      # shard it lands in). Windows isolates it in its own job and shards the other
      # 32 specs; Ubuntu shards the whole suite 2 ways (runner rides in one shard).
      # Every spec, runner included, runs on both OSes.
      matrix:
        include:
          - os: windows-latest
            name: runner
            command: pnpm run test:browser:playwright runner.test.ts
          - os: windows-latest
            name: rest 1/2
            command: "pnpm run test:browser:playwright --exclude 'specs/runner.test.ts' --shard=1/2"
          - os: windows-latest
            name: rest 2/2
            command: "pnpm run test:browser:playwright --exclude 'specs/runner.test.ts' --shard=2/2"
          - os: ubuntu-latest
            name: shard 1/2
            command: pnpm run test:browser:playwright --shard=1/2
          - os: ubuntu-latest
            name: shard 2/2
            command: pnpm run test:browser:playwright --shard=2/2
      fail-fast: false

    timeout-minutes: 30

    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - uses: ./.github/actions/setup-and-cache
        with:
          node-version: 24

      - name: Install
        run: pnpm i --filter '!docs'

      - uses: ./.github/actions/setup-playwright
        with:
          browsers: all

      - name: Build
        run: pnpm run build

      - name: Test Browser (playwright)
        # matrix.command is a fixed, workflow-defined string
        run: ${{ matrix.command }} # zizmor: ignore[template-injection]

  test-vite7:
    needs: changed
    # Runs on ubuntu to keep macOS under its ~5 concurrent-runner limit.
    name: 'Test: vite@7, ${{ matrix.suite }}, node-24, ubuntu-latest'
    if: needs.changed.outputs.should_skip != 'true'
    runs-on: ubuntu-latest

    timeout-minutes: 30

    strategy:
      # vite7 is a compatibility smoke, not a coverage gate. Run the e2e suite
      # (which exercises real config resolution, transform and runs) and the
      # browser suite on separate runners in parallel.
      matrix:
        suite: [e2e, browser]
      fail-fast: false

    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - uses: ./.github/actions/setup-and-cache
        with:
          node-version: 24

      - name: Install
        run: |
          pnpm override-vite7
          # ubuntu runners have no default git identity; set one for this commit
          git add . && git -c user.email=ci@vitest.dev -c user.name=vitest-ci commit -m "ci" && pnpm i --prefer-offline --no-frozen-lockfile --filter '!docs'

      - uses: ./.github/actions/setup-playwright
        with:
          browsers: all

      - name: Build
        run: pnpm run build

      - name: Test E2E
        if: ${{ matrix.suite == 'e2e' }}
        run: pnpm run test:ci:e2e

      - name: Test Browser (playwright)
        if: ${{ matrix.suite == 'browser' && !cancelled() }}
        # smoke: runner.test.ts exercises the core browser-runner behaviour across
        # every instance, enough to catch vite7 browser-mode regressions
        run: pnpm run test:browser:playwright runner.test.ts

  merge-reports:
    needs:
      - test
      - changed
    if: needs.changed.outputs.should_skip != 'true' && !cancelled()
    runs-on: ubuntu-latest
    name: Merge Reports
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - uses: ./.github/actions/setup-and-cache

      - name: Install
        run: pnpm i --filter '!docs'

      - name: Build
        run: pnpm run build

      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          pattern: vitest-results-*
          merge-multiple: true

      - name: Merge reports
        continue-on-error: true
        run: |
          set +e
          pnpm --filter=./packages/ui --no-bail test:ui --merge-reports
          cp ./packages/ui/.vitest/index.html ./packages/ui/.vitest/test-ui.html
          pnpm --filter=./test/unit --filter=./test/e2e --no-bail --sequential test --merge-reports
          cp -rf ./test/unit/.vitest/index.html ./test/unit/.vitest/test-unit.html
          cp -rf ./test/e2e/.vitest/index.html ./test/e2e/.vitest/test-e2e.html
        env:
          VITEST_CI_MERGE_REPORTS: 'true'

      - name: Upload Test UI Report
        id: upload-ui-report
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          path: ./packages/ui/.vitest/test-ui.html
          archive: false
          retention-days: 7

      - name: Upload Test Unit Report
        id: upload-unit-report
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          path: ./test/unit/.vitest/test-unit.html
          archive: false
          retention-days: 7

      - name: Upload Test E2E Report
        id: upload-e2e-report
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          path: ./test/e2e/.vitest/test-e2e.html
          archive: false
          retention-days: 7

      - name: Link CI reports
        run: |
          echo "::notice title=CI reports::test/unit: ${STEPS_UPLOAD_UNIT_REPORT_OUTPUTS_ARTIFACT_URL}"
          echo "::notice title=CI reports::test/e2e: ${STEPS_UPLOAD_E2E_REPORT_OUTPUTS_ARTIFACT_URL}"
          echo "::notice title=CI reports::packages/ui: ${STEPS_UPLOAD_UI_REPORT_OUTPUTS_ARTIFACT_URL}"
        env:
          STEPS_UPLOAD_UNIT_REPORT_OUTPUTS_ARTIFACT_URL: ${{ steps.upload-unit-report.outputs.artifact-url }}
          STEPS_UPLOAD_E2E_REPORT_OUTPUTS_ARTIFACT_URL: ${{ steps.upload-e2e-report.outputs.artifact-url }}
          STEPS_UPLOAD_UI_REPORT_OUTPUTS_ARTIFACT_URL: ${{ steps.upload-ui-report.outputs.artifact-url }}
