# Keep this name in sync with the `workflow_run.workflows` list in
# runtime_sizebot_comment.yml, which matches on this exact string rather than on
# the file name. Renaming it here alone stops sizebot from ever commenting again,
# and nothing fails: the comment workflow simply never triggers.
name: (Runtime) Build and Test

on:
  push:
    branches:
      # release branches (keep in sync with branches that receive artifact attestations)
      - main
      - releases/**
  pull_request:
    paths-ignore:
      - compiler/**
  # Manual re-runs are allowed. The workflow always runs on the SHA it was
  # dispatched from (github.sha); no user-supplied ref is accepted, because
  # downstream workflows (e.g. runtime_release_from_ci) consume this
  # workflow's artifacts under a protected environment.
  workflow_dispatch:

permissions: {}

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
  cancel-in-progress: true

env:
  TZ: /usr/share/zoneinfo/America/Los_Angeles
  # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
  SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1

jobs:
  # ----- NODE_MODULES CACHE -----
  # Centralize the node_modules cache so it is saved once and each subsequent job only needs to
  # restore the cache. Prevents race conditions where multiple workflows try to write to the cache.
  runtime_node_modules_cache:
    name: Cache Runtime node_modules
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      
      - name: Prepare cache
        id: node_modules
        uses: actions/cache@v4
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.

      - uses: actions/setup-node@v4
        if: steps.node_modules.outputs.cache-hit != 'true'
        with:
          node-version-file: '.nvmrc'

      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'

  runtime_compiler_node_modules_cache:
    name: Cache Runtime, Compiler node_modules
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Prepare cache
        id: node_modules
        uses: actions/cache@v4
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn --cwd compiler install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'

  # ----- BUILD WEIGHTS -----
  # Pins one weights snapshot for the whole run. A cache restore with
  # restore-keys returns the newest entry at call time, so 50 workers
  # restoring individually can disagree when another run saves a new entry
  # mid-fan-out, which scrambles the shard assignment and drops bundles from
  # the build.
  resolve_build_weights:
    name: Resolve build shard weights
    runs-on: ubuntu-latest
    outputs:
      available: ${{ steps.available.outputs.available }}
    steps:
      - name: Restore latest build shard weights
        id: weights
        uses: actions/cache/restore@v4
        with:
          # Must match the save step's path in process_artifacts_combined
          # exactly: the cache version is a hash of the path, so a different
          # path never matches the key.
          path: build-weights.json
          key: build-weights-v1-${{ github.run_id }}
          restore-keys: build-weights-v1-
      - id: available
        run: echo "available=${{ steps.weights.outputs.cache-matched-key != '' }}" >> "$GITHUB_OUTPUT"
      - name: Publish build shard weights for this run
        if: steps.available.outputs.available == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: build-weights
          path: build-weights.json
          if-no-files-found: error

  # ----- FLOW -----
  discover_flow_inline_configs:
    name: Discover flow inline configs
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.result }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/github-script@v7
        id: set-matrix
        with:
          script: |
            const inlinedHostConfigs = require('./scripts/shared/inlinedHostConfigs.js');
            return inlinedHostConfigs.map(config => config.shortName);

  flow:
    name: Flow check ${{ matrix.flow_inline_config_shortname }}
    needs: [discover_flow_inline_configs, runtime_node_modules_cache]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        flow_inline_config_shortname: ${{ fromJSON(needs.discover_flow_inline_configs.outputs.matrix) }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: node ./scripts/tasks/flow-ci ${{ matrix.flow_inline_config_shortname }}

  # ----- FIZZ -----
  check_generated_fizz_runtime:
    name: Confirm generated inline Fizz runtime is up to date
    needs: [runtime_node_modules_cache]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: |
          yarn generate-inline-fizz-runtime
          git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)

  # ----- FEATURE FLAGS -----
  flags:
    name: Check flags
    needs: [runtime_node_modules_cache]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn flags

  # ----- TESTS -----
  test:
    name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard }})
    needs: [runtime_compiler_node_modules_cache]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        params:
          - "-r=stable --env=development"
          - "-r=stable --env=production"
          - "-r=experimental --env=development"
          - "-r=experimental --env=production"
          - "-r=www-classic --env=development --variant=false"
          - "-r=www-classic --env=production --variant=false"
          - "-r=www-classic --env=development --variant=true"
          - "-r=www-classic --env=production --variant=true"
          - "-r=www-modern --env=development --variant=false"
          - "-r=www-modern --env=production --variant=false"
          - "-r=www-modern --env=development --variant=true"
          - "-r=www-modern --env=production --variant=true"
          - "-r=xplat --env=development --variant=false"
          - "-r=xplat --env=development --variant=true"
          - "-r=xplat --env=production --variant=false"
          - "-r=xplat --env=production --variant=true"
          # TODO: Test more persistent configurations?
          - "-r=stable --env=development --persistent"
          - "-r=experimental --env=development --persistent"
        shard:
          - 1/5
          - 2/5
          - 3/5
          - 4/5
          - 5/5
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn --cwd compiler install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: node --version
      - run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}

  # Hardcoded to improve parallelism
  test-linter:
    name: Test eslint-plugin-react-hooks
    needs: [runtime_compiler_node_modules_cache]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
      - name: Install runtime dependencies
        run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Install compiler dependencies
        run: yarn install --frozen-lockfile
        working-directory: compiler
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
      - run: yarn workspace eslint-plugin-react-hooks test

  # ----- BUILD -----
  build_and_lint:
    name: yarn build and lint
    needs: [runtime_compiler_node_modules_cache, resolve_build_weights]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        # yml is dumb. update the --total arg to yarn build if you change the number of workers
        worker_id: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]
        release_channel: [stable, experimental]
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      # Java is only needed by `yarn build` (Closure Compiler), so its setup
      # runs in the background while Node and node_modules are set up.
      - name: Set up Java
        id: setup_java
        uses: actions/setup-java@v4
        background: true
        with:
          distribution: temurin
          java-version: 11.0.22
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn --cwd compiler install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      # Pinned once per run by resolve_build_weights so every worker computes
      # the same shard assignment. Absent on a cold start; the build then
      # falls back to round-robin sharding.
      - name: Download build shard weights
        if: needs.resolve_build_weights.outputs.available == 'true'
        uses: actions/download-artifact@v4
        with:
          name: build-weights
      # setup-java's exports (e.g. JAVA_HOME) only apply to steps after this wait.
      - name: Wait for Java setup
        wait: setup_java
      - run: yarn build --index=${{ matrix.worker_id }} --total=25 --r=${{ matrix.release_channel }} --ci
        env:
          CI: github
          RELEASE_CHANNEL: ${{ matrix.release_channel }}
          NODE_INDEX: ${{ matrix.worker_id }}
          BUILD_SHARD_WEIGHTS: build-weights.json
      - name: Lint build
        run: yarn lint-build
      - name: Display structure of build
        run: ls -R build
      - name: Archive build
        uses: actions/upload-artifact@v4
        with:
          name: _build_${{ matrix.worker_id }}_${{ matrix.release_channel }}
          path: build
          if-no-files-found: error

  test_build:
    name: yarn test-build
    needs: [build_and_lint, runtime_compiler_node_modules_cache]
    strategy:
      fail-fast: false
      matrix:
        test_params: [
          # Intentionally passing these as strings instead of creating a
          # separate parameter per CLI argument, since it's easier to
          # control/see which combinations we want to run.
          -r=stable --env=development,
          -r=stable --env=production,
          -r=experimental --env=development,
          -r=experimental --env=production,

          # TODO: Update test config to support www build tests
          # - "-r=www-classic --env=development --variant=false"
          # - "-r=www-classic --env=production --variant=false"
          # - "-r=www-classic --env=development --variant=true"
          # - "-r=www-classic --env=production --variant=true"
          # - "-r=www-modern --env=development --variant=false"
          # - "-r=www-modern --env=production --variant=false"
          # - "-r=www-modern --env=development --variant=true"
          # - "-r=www-modern --env=production --variant=true"

          # TODO: Update test config to support xplat build tests
          # - "-r=xplat --env=development --variant=false"
          # - "-r=xplat --env=development --variant=true"
          # - "-r=xplat --env=production --variant=false"
          # - "-r=xplat --env=production --variant=true"

          # TODO: Test more persistent configurations?
        ]
        shard:
          - 1/10
          - 2/10
          - 3/10
          - 4/10
          - 5/10
          - 6/10
          - 7/10
          - 8/10
          - 9/10
          - 10/10
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      # The build download runs in the background while Node is set up and
      # node_modules is restored. The `wait` step below synchronizes on it.
      # It must start after checkout, whose git clean would wipe `build/`.
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn --cwd compiler install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Wait for archived build
        wait: download_build
      - name: Display structure of build
        run: ls -R build
      - run: node --version
      - run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci

  test_build_devtools:
    name: yarn test-build (devtools)
    needs: [build_and_lint, runtime_node_modules_cache]
    strategy:
      fail-fast: false
      matrix:
        shard:
          - 1/5
          - 2/5
          - 3/5
          - 4/5
          - 5/5
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Wait for archived build
        wait: download_build
      - name: Display structure of build
        run: ls -R build
      - run: node --version
      - run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci

  process_artifacts_combined:
    name: Process artifacts combined
    needs: [build_and_lint, runtime_node_modules_cache, resolve_build_weights]
    permissions:
      # https://github.com/actions/attest-build-provenance
      id-token: write
      attestations: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Wait for archived build
        wait: download_build
      # Only used to log weight variance; the new measurement is what gets
      # saved, so removed bundles drop out instead of accumulating. Downloads
      # the run's pinned snapshot so the diff compares against the weights
      # that actually drove this run's assignment.
      - name: Download previous build shard weights
        if: needs.resolve_build_weights.outputs.available == 'true'
        uses: actions/download-artifact@v4
        with:
          name: build-weights
      - name: Update build shard weights
        run: node scripts/ci/merge-build-weights.js
      - name: Save build shard weights
        # Pull request saves land in the pull request's own merge-ref cache
        # scope, which only that pull request can read; fork pull requests
        # have a read-only cache token, so their save degrades to a warning.
        # A re-run of this same workflow run reuses the cache key, and cache
        # entries are immutable. Weights are an optimization, so a failed
        # save must not break artifact processing.
        continue-on-error: true
        uses: actions/cache/save@v4
        with:
          path: build-weights.json
          key: build-weights-v1-${{ github.run_id }}
      # Keep the shard timings out of the released tarball.
      - run: rm -rf build/__shard_timings__
      - name: Display structure of build
        run: ls -R build
      - run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
      - name: Scrape warning messages
        run: |
          mkdir -p ./build/__test_utils__
          node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
        # Compress build directory into a single tarball for easy download
      - run: tar -zcvf ./build.tgz ./build
        # TODO: Migrate scripts to use `build` directory instead of `build2`
      - run: cp ./build.tgz ./build2.tgz
      - name: Archive build artifacts
        id: upload_artifacts_combined
        uses: actions/upload-artifact@v4
        with:
          name: artifacts_combined
          path: |
            ./build.tgz
            ./build2.tgz
          if-no-files-found: error
      - uses: actions/attest-build-provenance@v2
        # We don't verify builds generated from pull requests not originating from react/react.
        # However, if the PR lands, the run on release branches will generate the attestation which can then
        # be used to download a build via scripts/release/download-experimental-build.js.
        #
        # Note that this means that scripts/release/download-experimental-build.js must be run with
        # --no-verify when downloading a build from a fork.
        if: github.event_name == 'push' && (github.ref_name == 'main' || startsWith(github.ref_name, 'releases/')) || github.event.pull_request.head.repo.full_name == github.repository
        with:
          subject-name: artifacts_combined.zip
          subject-digest: sha256:${{ steps.upload_artifacts_combined.outputs.artifact-digest }}

  check_error_codes:
    name: Search build artifacts for unminified errors
    needs: [build_and_lint, runtime_node_modules_cache]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Wait for archived build
        wait: download_build
      - name: Display structure of build
        run: ls -R build
      - name: Search build artifacts for unminified errors
        run: |
          yarn extract-errors
          git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)

  check_release_dependencies:
    name: Check release dependencies
    needs: [build_and_lint, runtime_node_modules_cache]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Wait for archived build
        wait: download_build
      - name: Display structure of build
        run: ls -R build
      - run: yarn check-release-dependencies

  RELEASE_CHANNEL_stable_yarn_test_dom_fixtures:
    name: Check fixtures DOM (stable)
    needs: build_and_lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
        id: node_modules
        with:
          path: |
            **/node_modules
          key: fixtures_dom-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'fixtures/dom/yarn.lock') }}
      - run: yarn --cwd fixtures/dom install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Wait for archived build
        wait: download_build
      - name: Display structure of build
        run: ls -R build
      - name: Run DOM fixture tests
        run: |
          yarn predev
          yarn test
        working-directory: fixtures/dom
        env:
          RELEASE_CHANNEL: stable

  # ----- FLIGHT -----
  run_fixtures_flight_tests:
    name: Run fixtures Flight tests
    needs: build_and_lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      # Fixture copies some built packages from the workroot after install.
      # That means dependencies of the built packages are not installed.
      # We need to install dependencies of the workroot to fulfill all dependency constraints
      - name: Restore cached node_modules
        uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
        id: node_modules
        with:
          path: |
            **/node_modules
          key: fixtures_flight-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'fixtures/flight/yarn.lock') }}
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn --cwd fixtures/flight install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Check Playwright version
        id: playwright_version
        run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
      - name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
        id: cache_playwright_browsers
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
      - name: Playwright install deps
        if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
        working-directory: fixtures/flight
        run: npx playwright install --with-deps chromium
      - name: Wait for archived build
        wait: download_build
      - name: Display structure of build
        run: ls -R build
      - name: Run tests
        working-directory: fixtures/flight
        run: yarn test
        env:
          # Otherwise the webserver is a blackbox
          DEBUG: pw:webserver
      - name: Archive Flight fixture artifacts
        uses: actions/upload-artifact@v4
        with:
          name: flight-playwright-report
          path: fixtures/flight/playwright-report
          if-no-files-found: warn
      - name: Archive Flight fixture artifacts
        uses: actions/upload-artifact@v4
        with:
          name: flight-test-results
          path: fixtures/flight/test-results
          if-no-files-found: ignore

  # ----- DEVTOOLS -----
  build_devtools_and_process_artifacts:
    name: Build DevTools and process artifacts
    needs: [build_and_lint, runtime_node_modules_cache]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [chrome, firefox, edge]
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Wait for archived build
        wait: download_build
      - run: ./scripts/ci/pack_and_store_devtools_artifacts.sh ${{ matrix.browser }}
        env:
          RELEASE_CHANNEL: experimental
      - name: Display structure of build
        run: ls -R build
      # Simplifies getting the extension for local testing
      - name: Archive ${{ matrix.browser }} extension
        uses: actions/upload-artifact@v4
        with:
          name: react-devtools-${{ matrix.browser }}-extension
          path: build/devtools/${{ matrix.browser }}-extension.zip
          if-no-files-found: error
      - name: Archive ${{ matrix.browser }} metadata
        uses: actions/upload-artifact@v4
        with:
          name: react-devtools-${{ matrix.browser }}-metadata
          path: build/devtools/webpack-stats.*.json

  merge_devtools_artifacts:
    name: Merge DevTools artifacts
    needs: build_devtools_and_process_artifacts
    runs-on: ubuntu-latest
    steps:
      - name: Merge artifacts
        uses: actions/upload-artifact/merge@v4
        with:
          name: react-devtools
          pattern: react-devtools-*

  runtime_playwright_chromium_cache:
    name: Cache Runtime Playwright Chromium
    needs: runtime_node_modules_cache
    runs-on: ubuntu-latest
    outputs:
      playwright_version: ${{ steps.playwright_version.outputs.playwright_version }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Check Playwright version
        id: playwright_version
        run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
      - name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
        id: cache_playwright_browsers
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
      - name: Install Playwright Chromium
        if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
        run: npx playwright install chromium

  run_devtools_e2e_tests:
    name: Run React DevTools browser extension e2e tests
    needs: [build_and_lint, runtime_playwright_chromium_cache]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore Playwright Chromium
        uses: actions/cache/restore@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ needs.runtime_playwright_chromium_cache.outputs.playwright_version }}
          fail-on-cache-miss: true
      - name: Wait for archived build
        wait: download_build
      - run: ./scripts/ci/run_devtools_e2e_tests.js
        env:
          RELEASE_CHANNEL: experimental
      - name: Archive Playwright report
        uses: actions/upload-artifact@v4
        with:
          name: devtools-playwright-artifacts
          path: tmp/playwright-artifacts
          if-no-files-found: warn

  run_react_devtools_cdt_mcp_e2e_tests:
    name: Run react-devtools-cdt-mcp e2e tests
    needs: [build_and_lint, runtime_playwright_chromium_cache]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build
        id: download_build
        uses: actions/download-artifact@v4
        background: true
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v10-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          # Don't use restore-keys here. Otherwise the cache grows indefinitely.
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore Playwright Chromium
        uses: actions/cache/restore@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ needs.runtime_playwright_chromium_cache.outputs.playwright_version }}
          fail-on-cache-miss: true
      - name: Check Playwright Chromium version
        id: playwright_chromium
        run: |
          echo "executable_path=$(node -e 'process.stdout.write(require("playwright").chromium.executablePath())')" >> "$GITHUB_OUTPUT"
          "$(node -e 'process.stdout.write(require("playwright").chromium.executablePath())')" --version
      - name: Wait for archived build
        wait: download_build
      - name: Run React DevTools CDT MCP e2e tests
        run: yarn --cwd packages/react-devtools-cdt-mcp test:e2e:ci
        env:
          CHROME_EXECUTABLE_PATH: ${{ steps.playwright_chromium.outputs.executable_path }}
      - name: Archive React DevTools CDT MCP e2e logs
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: react-devtools-cdt-mcp-e2e-logs
          path: tmp/react-devtools-cdt-mcp-e2e
          if-no-files-found: warn

  # ----- SIZEBOT -----
  sizebot:
    if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' && github.event.pull_request.base.ref == 'main' }}
    name: Run sizebot
    needs: [build_and_lint]
    permissions:
      # We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
      actions: read
      # Used to resolve the merge-base commit through the compare API
      contents: read
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
      - name: Restore cached node_modules
        uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - run: yarn --cwd scripts/release install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Resolve base commit for comparison
        # Compare against the commit this pull request diverged from, not the
        # current tip of the base branch. Otherwise, once main moves ahead,
        # unrelated commits show up as size changes on every pull request.
        id: base
        run: |
          echo "sha=$(gh api repos/${{ github.repository }}/compare/${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} --jq .merge_base_commit.sha)" >> "$GITHUB_OUTPUT"
        env:
          GH_TOKEN: ${{ github.token }}
      - name: Download artifacts for base revision
        id: download_base
        # The build could have been generated from a fork, so we must download the build without
        # any verification. This is safe since we only use this for sizebot calculation and the
        # unverified artifact is not used. Additionally this workflow runs in the pull_request
        # trigger so only restricted permissions are available.
        run: |
          if GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ steps.base.outputs.sha }} ${{ (github.event.pull_request.head.repo.full_name != github.repository && '--noVerify') || ''}}; then
            mv ./build ./base-build
          else
            # Rendered as a warning comment by render-comment.js on the
            # default branch, which also fails the comment workflow's check.
            jq --null-input --arg baseSha "${{ steps.base.outputs.sha }}" \
              '{version: 1, status: "base-build-not-found", baseSha: $baseSha}' \
              > sizebot-results.json
            echo "missing=true" >> "$GITHUB_OUTPUT"
          fi
      - name: Delete extraneous files
        # TODO: The `download-experimental-build` script copies the npm
        # packages into the `node_modules` directory. This is a historical
        # quirk of how the release script works. Let's pretend they
        # don't exist.
        run: rm -rf ./base-build/node_modules
      - name: Display structure of base-build
        if: steps.download_base.outputs.missing != 'true'
        run: ls -R base-build
      - name: Ensure clean build directory
        run: rm -rf build
      - name: Restore archived build for PR
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - name: Scrape warning messages
        run: |
          mkdir -p ./build/__test_utils__
          node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
      - name: Display structure of build for PR
        run: ls -R build
      - run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
      - name: Measure size changes
        # Only measures and records the numbers. The comment is rendered and
        # posted by runtime_sizebot_comment.yml, which runs on the workflow_run
        # trigger because this job's token is read-only for pull requests from
        # forks and so cannot comment.
        # Skipped when the base build is missing: the download step already
        # wrote the not-found results.
        if: steps.download_base.outputs.missing != 'true'
        run: node ./scripts/sizebot/compare-sizes.js
      - name: Archive sizebot results
        uses: actions/upload-artifact@v4
        with:
          name: sizebot-results
          path: sizebot-results.json
          if-no-files-found: error
      # Note: a missing base build does not fail this job on purpose. A failed
      # job would fail the whole run, and render-comment.js only reads the
      # results of successful runs, so the warning would never be rendered.
      # Instead the comment workflow fails on the sizebot-problem.txt marker
      # after posting the warning, same as for build configuration drift.