name: (Runtime) Build and Test

on:
  push:
    branches: [main]
  pull_request:
    paths-ignore:
      - compiler/**

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: Check cache hit
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          lookup-only: true
      - uses: actions/setup-node@v4
        if: steps.node_modules.outputs.cache-hit != 'true'
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Warm with old cache
        if: steps.node_modules.outputs.cache-hit != 'true'
        uses: actions/cache/restore@v4
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Save cache
        if: steps.node_modules.outputs.cache-hit != 'true'
        uses: actions/cache/save@v4
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}

  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 }}
      - name: Check cache hit
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          lookup-only: true
      - uses: actions/setup-node@v4
        if: steps.node_modules.outputs.cache-hit != 'true'
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: |
            yarn.lock
            compiler/yarn.lock
      - name: Warm with old cache
        if: steps.node_modules.outputs.cache-hit != 'true'
        uses: actions/cache/restore@v4
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          restore-keys: |
            runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-and-compiler-node_modules-v6-
      - 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: Save cache
        if: steps.node_modules.outputs.cache-hit != 'true'
        uses: actions/cache/save@v4
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}

  # ----- 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'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - 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'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - 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 --quiet || (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'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ 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'
          cache: yarn
          cache-dependency-path: |
            yarn.lock
            compiler/yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          restore-keys: |
            runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-and-compiler-node_modules-v6-
      - 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: 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'
          cache: yarn
          cache-dependency-path:  |
            yarn.lock
            compiler/yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v6-${{ 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]
    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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: |
            yarn.lock
            compiler/yarn.lock
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 11.0.22
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          restore-keys: |
            runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-and-compiler-node_modules-v6-
      - 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: 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 }}
      - 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,

          # Dev Tools
          --project=devtools -r=experimental,

          # 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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: |
            yarn.lock
            compiler/yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
          restore-keys: |
            runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-and-compiler-node_modules-v6-
      - 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'
      - name: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - name: Display structure of build
        run: ls -R build
      - run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci

  process_artifacts_combined:
    name: Process artifacts combined
    needs: [build_and_lint, runtime_node_modules_cache]
    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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - 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 facebook/react.
        # However, if the PR lands, the run on `main` 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' || 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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - name: Display structure of build
        run: ls -R build
      - name: Search build artifacts for unminified errors
        run: |
          yarn extract-errors
          git diff --quiet || (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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - 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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      - 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') }}
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn --cwd fixtures/dom install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - 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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      # 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') }}
      - 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 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: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - 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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - 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

  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-*-extension

  run_devtools_e2e_tests:
    name: Run DevTools e2e tests
    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 }}
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: yarn
          cache-dependency-path: yarn.lock
      - name: Restore cached node_modules
        uses: actions/cache/restore@v4
        id: node_modules
        with:
          path: |
            **/node_modules
          key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
          restore-keys: |
            runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
            runtime-node_modules-v6-
      - name: Ensure clean build directory
        run: rm -rf build
      - run: yarn install --frozen-lockfile
        if: steps.node_modules.outputs.cache-hit != 'true'
      - name: Restore archived build
        uses: actions/download-artifact@v4
        with:
          pattern: _build_*
          path: build
          merge-multiple: true
      - run: |
          npx playwright install
          sudo npx playwright install-deps
      - run: ./scripts/ci/run_devtools_e2e_tests.js
        env:
          RELEASE_CHANNEL: experimental

  # ----- 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
    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'
          cache: yarn
          cache-dependency-path: yarn.lock
      - 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: Download artifacts for base revision
        # 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: |
          GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=$(git rev-parse ${{ github.event.pull_request.base.sha }}) ${{ (github.event.pull_request.head.repo.full_name != github.repository && '--noVerify') || ''}}
          mv ./build ./base-build
      - 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 from origin/main
        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
      - run: node ./scripts/tasks/danger
      - name: Archive sizebot results
        uses: actions/upload-artifact@v4
        with:
          name: sizebot-message
          path: sizebot-message.md
          if-no-files-found: ignore