name: Bazel

# Note this workflow was originally derived from:
# https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml

on:
  pull_request: {}
  push:
    branches:
      - main
  workflow_dispatch:

concurrency:
  # Cancel previous actions from the same PR or branch except 'main' branch.
  # See https://docs.github.com/en/actions/using-jobs/using-concurrency and https://docs.github.com/en/actions/learn-github-actions/contexts for more info.
  group: concurrency-group::${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}${{ github.ref_name == 'main' && format('::{0}', github.run_id) || ''}}
  cancel-in-progress: ${{ github.ref_name != 'main' }}
jobs:
  test:
    # PRs use the sharded Windows cross-compiled test jobs below. Post-merge
    # pushes to main also run the native Windows test job for broader Windows
    # signal without putting PR latency back on the critical path. Cargo CI
    # owns V8/code-mode test coverage for now.
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        include:
          # macOS
          - os: macos-15-xlarge
            target: aarch64-apple-darwin
          - os: macos-15-xlarge
            target: x86_64-apple-darwin

          # Linux
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-musl
          # 2026-02-27 Bazel tests have been flaky on arm in CI.
          # Disable until we can investigate and stabilize them.
          # - os: ubuntu-24.04-arm
          #   target: aarch64-unknown-linux-musl
          # - os: ubuntu-24.04-arm
          #   target: aarch64-unknown-linux-gnu

    runs-on: ${{ matrix.os }}

    # Configure a human readable name for each job
    name: Bazel test on ${{ matrix.os }} for ${{ matrix.target }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
          persist-credentials: false

      - name: Check rusty_v8 MODULE.bazel checksums
        if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
        shell: bash
        run: |
          python3 .github/scripts/rusty_v8_bazel.py check-module-bazel
          python3 -m unittest discover -s .github/scripts -p test_rusty_v8_bazel.py

      - name: Prepare Bazel CI
        id: prepare_bazel
        uses: ./.github/actions/prepare-bazel-ci
        with:
          target: ${{ matrix.target }}
          cache-scope: bazel-${{ github.job }}
          install-test-prereqs: "true"
      - name: Check MODULE.bazel.lock is up to date
        if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu'
        shell: bash
        run: ./scripts/check-module-bazel-lock.sh

      - name: bazel test //...
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          bazel_targets=(
            //...
            # Keep standalone V8 library targets out of the ordinary Bazel CI
            # path. V8 consumers under `//codex-rs/...` still participate
            # transitively through `//...`.
            -//third_party/v8:all
            # V8-backed code-mode tests are covered by Cargo CI. Bazel CI
            # cross-compiles in several legs, and those tests are not stable in
            # that setup yet.
            -//codex-rs/code-mode:code-mode-unit-tests
            -//codex-rs/v8-poc:v8-poc-unit-tests
          )

          bazel_wrapper_args=(
            --print-failed-action-summary
            --print-failed-test-logs
          )
          bazel_test_args=(
            test
            --test_tag_filters=-argument-comment-lint
            --test_verbose_timeout_warnings
            --build_metadata=COMMIT_SHA=${GITHUB_SHA}
          )
          ./.github/scripts/run-bazel-ci.sh \
            "${bazel_wrapper_args[@]}" \
            -- \
            "${bazel_test_args[@]}" \
            -- \
            "${bazel_targets[@]}"

      - name: Upload Bazel execution logs
        if: always() && !cancelled()
        continue-on-error: true
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: bazel-execution-logs-test-${{ matrix.target }}
          path: ${{ runner.temp }}/bazel-execution-logs
          if-no-files-found: ignore

      # Save the job-scoped Bazel repository cache after cache misses. Keep the
      # upload non-fatal so cache service issues never fail the job itself.
      - name: Save bazel repository cache
        if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
          key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}

  test-windows-shard:
    # Split the Windows Bazel test leg across separate Windows
    # hosts. Each shard still uses Linux RBE for build actions, but the test
    # execution itself happens on its own Windows runner.
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        shard:
          - 1
          - 2
          - 3
          - 4
    runs-on: windows-latest
    name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm shard ${{ matrix.shard }}/4

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
          persist-credentials: false

      - name: Prepare Bazel CI
        id: prepare_bazel
        uses: ./.github/actions/prepare-bazel-ci
        with:
          target: x86_64-pc-windows-gnullvm
          # Reuse the former monolithic Windows test cache for restores. Do
          # not save it from every shard below; duplicate uploads would sit on
          # the PR-blocking critical path after the useful test work is done.
          cache-scope: bazel-test
          install-test-prereqs: "true"

      - name: bazel test shard
        env:
          BAZEL_TEST_SHARD: ${{ matrix.shard }}
          BAZEL_TEST_SHARD_COUNT: 4
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          set -euo pipefail

          bazel_test_query='tests(//...) except tests(//third_party/v8:all) except //codex-rs/code-mode:code-mode-unit-tests except //codex-rs/v8-poc:v8-poc-unit-tests except attr(tags, "manual", tests(//...))'
          mapfile -t bazel_targets < <(
            MSYS2_ARG_CONV_EXCL='*' bazel query --output=label "${bazel_test_query}" \
              | LC_ALL=C sort
          )

          selected_targets=()
          for bazel_target in "${bazel_targets[@]}"; do
            target_bucket="$(
              printf '%s\n' "${bazel_target}" \
                | cksum \
                | awk -v shard_count="${BAZEL_TEST_SHARD_COUNT}" '{ print ($1 % shard_count) + 1 }'
            )"
            if [[ "${target_bucket}" == "${BAZEL_TEST_SHARD}" ]]; then
              selected_targets+=("${bazel_target}")
            fi
          done

          if [[ ${#selected_targets[@]} -eq 0 ]]; then
            echo "No Bazel test targets selected for Windows shard ${BAZEL_TEST_SHARD}/${BAZEL_TEST_SHARD_COUNT}." >&2
            exit 1
          fi

          echo "Selected ${#selected_targets[@]} of ${#bazel_targets[@]} Bazel test targets for Windows shard ${BAZEL_TEST_SHARD}/${BAZEL_TEST_SHARD_COUNT}."

          bazel_test_args=(
            test
            --skip_incompatible_explicit_targets
            --test_tag_filters=-argument-comment-lint
            --test_verbose_timeout_warnings
            --build_metadata=COMMIT_SHA=${GITHUB_SHA}
            --build_metadata=TAG_windows_test_shard=${BAZEL_TEST_SHARD}
          )

          ./.github/scripts/run-bazel-ci.sh \
            --print-failed-action-summary \
            --print-failed-test-logs \
            --windows-cross-compile \
            --remote-download-toplevel \
            -- \
            "${bazel_test_args[@]}" \
            -- \
            "${selected_targets[@]}"

      - name: Upload Bazel execution logs
        if: always() && !cancelled()
        continue-on-error: true
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: bazel-execution-logs-test-x86_64-pc-windows-gnullvm-shard-${{ matrix.shard }}
          path: ${{ runner.temp }}/bazel-execution-logs
          if-no-files-found: ignore

  test-windows:
    # Preserve the existing required-check surface while the real work happens
    # in the sharded Windows jobs above.
    if: always()
    needs: test-windows-shard
    runs-on: ubuntu-24.04
    name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm

    steps:
      - name: Confirm Windows Bazel test shards passed
        shell: bash
        run: |
          if [[ "${{ needs.test-windows-shard.result }}" != "success" ]]; then
            echo "Windows Bazel test shards finished with result: ${{ needs.test-windows-shard.result }}" >&2
            exit 1
          fi

  test-windows-native-main:
    # Native Windows Bazel tests are slower and frequently approach the
    # 30-minute PR budget. Run this only for post-merge commits to main and give
    # it a larger timeout.
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    timeout-minutes: 40
    runs-on: windows-latest
    name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
          persist-credentials: false

      - name: Prepare Bazel CI
        id: prepare_bazel
        uses: ./.github/actions/prepare-bazel-ci
        with:
          target: x86_64-pc-windows-gnullvm
          cache-scope: bazel-${{ github.job }}
          install-test-prereqs: "true"

      - name: bazel test //...
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          bazel_targets=(
            //...
            # Keep standalone V8 library targets out of the ordinary Bazel CI
            # path. V8 consumers under `//codex-rs/...` still participate
            # transitively through `//...`.
            -//third_party/v8:all
            # Keep this aligned with the main Bazel job. The native Windows
            # job preserves broad post-merge coverage, but code-mode/V8 tests
            # are covered by Cargo CI rather than Bazel for now.
            -//codex-rs/code-mode:code-mode-unit-tests
            -//codex-rs/v8-poc:v8-poc-unit-tests
          )

          bazel_test_args=(
            test
            --test_tag_filters=-argument-comment-lint
            --test_verbose_timeout_warnings
            --build_metadata=COMMIT_SHA=${GITHUB_SHA}
            --build_metadata=TAG_windows_native_main=true
          )

          ./.github/scripts/run-bazel-ci.sh \
            --print-failed-action-summary \
            --print-failed-test-logs \
            -- \
            "${bazel_test_args[@]}" \
            -- \
            "${bazel_targets[@]}"

      - name: Upload Bazel execution logs
        if: always() && !cancelled()
        continue-on-error: true
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: bazel-execution-logs-test-windows-native-x86_64-pc-windows-gnullvm
          path: ${{ runner.temp }}/bazel-execution-logs
          if-no-files-found: ignore

      # Save the job-scoped Bazel repository cache after cache misses. Keep the
      # upload non-fatal so cache service issues never fail the job itself.
      - name: Save bazel repository cache
        if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
          key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}

  clippy:
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        include:
          # Keep Linux lint coverage on x64 and add the arm64 macOS path that
          # the Bazel test job already exercises. Add Windows gnullvm as well
          # so PRs get Bazel-native lint signal on the same Windows toolchain
          # that the Bazel test job uses.
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
          - os: macos-15-xlarge
            target: aarch64-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-gnullvm
    runs-on: ${{ matrix.os }}
    name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
          persist-credentials: false

      - name: Prepare Bazel CI
        id: prepare_bazel
        uses: ./.github/actions/prepare-bazel-ci
        with:
          target: ${{ matrix.target }}
          cache-scope: bazel-${{ github.job }}

      - name: bazel build --config=clippy lint targets
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          bazel_clippy_args=(
            --config=clippy
            --build_metadata=COMMIT_SHA=${GITHUB_SHA}
            --build_metadata=TAG_job=clippy
          )
          bazel_wrapper_args=()
          bazel_target_list_args=()
          if [[ "${RUNNER_OS}" == "Windows" ]]; then
            # Keep this aligned with the fast Windows Bazel test job: use
            # Linux RBE for clippy build actions while targeting Windows
            # gnullvm. Fork/community PRs without the BuildBuddy secret fall
            # back inside `run-bazel-ci.sh` to the previous local Windows MSVC
            # host-platform shape.
            bazel_wrapper_args+=(--windows-cross-compile)
            bazel_target_list_args+=(--windows-cross-compile)
            if [[ -z "${BUILDBUDDY_API_KEY:-}" ]]; then
              # The fork fallback can see incompatible explicit Windows-cross
              # internal test binaries in the generated target list. Preserve
              # the old local-fallback behavior there.
              bazel_clippy_args+=(--skip_incompatible_explicit_targets)
            fi
          fi

          bazel_target_lines="$(./scripts/list-bazel-clippy-targets.sh "${bazel_target_list_args[@]}")"
          bazel_targets=()
          while IFS= read -r target; do
            bazel_targets+=("${target}")
          done <<< "${bazel_target_lines}"

          ./.github/scripts/run-bazel-ci.sh \
            --print-failed-action-summary \
            "${bazel_wrapper_args[@]}" \
            -- \
            build \
            "${bazel_clippy_args[@]}" \
            -- \
            "${bazel_targets[@]}"

      - name: Upload Bazel execution logs
        if: always() && !cancelled()
        continue-on-error: true
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: bazel-execution-logs-clippy-${{ matrix.target }}
          path: ${{ runner.temp }}/bazel-execution-logs
          if-no-files-found: ignore

      # Save the job-scoped Bazel repository cache after cache misses. Keep the
      # upload non-fatal so cache service issues never fail the job itself.
      - name: Save bazel repository cache
        if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
          key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}

  verify-release-build:
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
          - os: macos-15-xlarge
            target: aarch64-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-gnullvm
    runs-on: ${{ matrix.os }}
    name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
          persist-credentials: false

      - name: Prepare Bazel CI
        id: prepare_bazel
        uses: ./.github/actions/prepare-bazel-ci
        with:
          target: ${{ matrix.target }}
          cache-scope: bazel-${{ github.job }}

      - name: bazel build verify-release-build targets
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          # This job exists to compile Rust code behind
          # `cfg(not(debug_assertions))` so PR CI catches failures that would
          # otherwise show up only in a release build. We do not need the full
          # optimizer and debug-info work that normally comes with a release
          # build to get that signal, so keep Bazel in `fastbuild` and disable
          # Rust debug assertions explicitly.
          bazel_wrapper_args=()
          if [[ "${RUNNER_OS}" == "Windows" ]]; then
            # This is build-only signal, so use the same Linux-RBE
            # cross-compile path as the fast Windows test and clippy jobs.
            # Fork/community PRs without the BuildBuddy secret fall back
            # inside `run-bazel-ci.sh` to the previous local Windows MSVC
            # host-platform shape.
            bazel_wrapper_args+=(--windows-cross-compile)
          fi

          bazel_build_args=(
            --compilation_mode=fastbuild
            --@rules_rust//rust/settings:extra_rustc_flag=-Cdebug-assertions=no
            --@rules_rust//rust/settings:extra_exec_rustc_flag=-Cdebug-assertions=no
            --build_metadata=COMMIT_SHA=${GITHUB_SHA}
            --build_metadata=TAG_job=verify-release-build
            --build_metadata=TAG_rust_debug_assertions=off
          )

          bazel_target_lines="$(bash ./scripts/list-bazel-release-targets.sh)"
          bazel_targets=()
          while IFS= read -r target; do
            bazel_targets+=("${target}")
          done <<< "${bazel_target_lines}"

          ./.github/scripts/run-bazel-ci.sh \
            "${bazel_wrapper_args[@]}" \
            -- \
            build \
            "${bazel_build_args[@]}" \
            -- \
            "${bazel_targets[@]}"

      - name: Verify Bazel builds bwrap
        if: runner.os == 'Linux'
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          ./.github/scripts/run-bazel-ci.sh \
            --remote-download-toplevel \
            --print-failed-action-summary \
            -- \
            build \
            --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
            --build_metadata=TAG_job=verify-bwrap \
            -- \
            //codex-rs/bwrap:bwrap

      - name: Upload Bazel execution logs
        if: always() && !cancelled()
        continue-on-error: true
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: bazel-execution-logs-verify-release-build-${{ matrix.target }}
          path: ${{ runner.temp }}/bazel-execution-logs
          if-no-files-found: ignore

      # Save the job-scoped Bazel repository cache after cache misses. Keep the
      # upload non-fatal so cache service issues never fail the job itself.
      - name: Save bazel repository cache
        if: always() && !cancelled() && steps.prepare_bazel.outputs.repository-cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: ${{ steps.prepare_bazel.outputs.repository-cache-path }}
          key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
