name: rust-ci-full nextest platform

on:
  workflow_call:
    inputs:
      runner:
        required: true
        type: string
      runner_group:
        required: false
        default: ""
        type: string
      runner_labels:
        required: false
        default: ""
        type: string
      archive_runner:
        required: false
        default: ""
        type: string
      archive_runner_group:
        required: false
        default: ""
        type: string
      archive_runner_labels:
        required: false
        default: ""
        type: string
      target:
        required: true
        type: string
      profile:
        required: true
        type: string
      artifact_id:
        required: true
        type: string
      remote_env:
        required: false
        default: false
        type: boolean
      test_threads:
        required: false
        default: 0
        type: number
      use_sccache:
        required: false
        default: false
        type: boolean

# Caller workflow-level env does not flow through workflow_call, so keep the
# Cargo git transport hardening on the archive and shard jobs directly here.
env:
  CARGO_NET_GIT_FETCH_WITH_CLI: "true"

jobs:
  archive:
    name: Build nextest archive
    runs-on: ${{ inputs.archive_runner_group != '' && fromJSON(format('{{"group":"{0}","labels":"{1}"}}', inputs.archive_runner_group, inputs.archive_runner_labels)) || inputs.archive_runner != '' && inputs.archive_runner || inputs.runner_group != '' && fromJSON(format('{{"group":"{0}","labels":"{1}"}}', inputs.runner_group, inputs.runner_labels)) || inputs.runner }}
    timeout-minutes: 60
    defaults:
      run:
        working-directory: codex-rs
    env:
      # Windows ARM64 archives are built on Windows x64, while their shards run
      # on native Windows ARM64. Key producer-side caches by the archive runner
      # so the cross-compile build reuses the Windows x64 cache lineage.
      ARCHIVE_CACHE_RUNNER: ${{ inputs.archive_runner != '' && inputs.archive_runner || inputs.runner }}
      USE_SCCACHE: ${{ inputs.use_sccache && 'true' || 'false' }}
      CARGO_INCREMENTAL: "0"
      SCCACHE_CACHE_SIZE: 10G
      NEXTEST_ARCHIVE_FILE: nextest-${{ inputs.artifact_id }}.tar.zst
      TEST_HELPERS_ARTIFACT: nextest-test-helpers-${{ inputs.artifact_id }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Configure Dev Drive (Windows)
        if: ${{ runner.os == 'Windows' }}
        shell: pwsh
        run: ../.github/scripts/setup-dev-drive.ps1

      - name: Install Linux build dependencies
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          set -euo pipefail
          if command -v apt-get >/dev/null 2>&1; then
            sudo apt-get update -y
            sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev bubblewrap
          fi

      - name: Install DotSlash
        uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2

      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          targets: ${{ inputs.target }}

      - name: Expose MSVC SDK environment (Windows)
        if: ${{ runner.os == 'Windows' && inputs.target == 'aarch64-pc-windows-msvc' }}
        uses: ./.github/actions/setup-msvc-env
        with:
          target: ${{ inputs.target }}

      - name: Compute lockfile hash
        id: lockhash
        shell: bash
        run: |
          set -euo pipefail
          echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
          echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"

      - name: Restore cargo home cache
        id: cache_cargo_home_restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
          key: cargo-home-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
          restore-keys: |
            cargo-home-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-

      - name: Install sccache
        if: ${{ env.USE_SCCACHE == 'true' }}
        uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
        with:
          tool: sccache
          version: 0.7.5

      - name: Configure sccache backend
        if: ${{ env.USE_SCCACHE == 'true' }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
            echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
            echo "Using sccache GitHub backend"
          else
            echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
            if [[ -n "${DEV_DRIVE:-}" ]]; then
              echo "SCCACHE_DIR=${DEV_DRIVE}\\.sccache" >> "$GITHUB_ENV"
            else
              echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
            fi
            echo "Using sccache local disk + actions/cache fallback"
          fi

      - name: Enable sccache wrapper
        if: ${{ env.USE_SCCACHE == 'true' }}
        shell: bash
        run: |
          set -euo pipefail
          wrapper="$(command -v sccache)"
          if [[ "${RUNNER_OS}" == "Windows" ]] && command -v cygpath >/dev/null 2>&1; then
            wrapper="$(cygpath -w "${wrapper}")"
          fi
          echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
          echo "CARGO_BUILD_RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"

      - name: Restore sccache cache (fallback)
        if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
        id: cache_sccache_restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: ${{ env.SCCACHE_DIR }}
          key: sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
          restore-keys: |
            sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-
            sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-

      - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
        with:
          tool: nextest
          version: 0.9.103

      - name: Enable unprivileged user namespaces (Linux)
        if: runner.os == 'Linux'
        run: |
          sudo sysctl -w kernel.unprivileged_userns_clone=1
          if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
            sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
          fi

      - name: Build nextest archive
        shell: bash
        run: |
          set -euo pipefail
          archive_dir="${RUNNER_TEMP}/nextest-archive"
          mkdir -p "${archive_dir}"
          cargo nextest archive \
            --target ${{ inputs.target }} \
            --cargo-profile ${{ inputs.profile }} \
            --timings \
            --archive-file "${archive_dir}/${NEXTEST_ARCHIVE_FILE}"

      - name: Build runtime test helpers
        if: ${{ runner.os == 'Linux' || runner.os == 'Windows' }}
        shell: bash
        run: |
          set -euo pipefail
          helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
          mkdir -p "${helper_dir}"

          if [[ "${RUNNER_OS}" == "Linux" ]]; then
            cargo build \
              --target ${{ inputs.target }} \
              --profile ${{ inputs.profile }} \
              -p codex-linux-sandbox \
              --bin codex-linux-sandbox
            cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-linux-sandbox" "${helper_dir}/"
          else
            cargo build \
              --target ${{ inputs.target }} \
              --profile ${{ inputs.profile }} \
              -p codex-windows-sandbox \
              --bin codex-windows-sandbox-setup \
              --bin codex-command-runner
            cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-windows-sandbox-setup.exe" "${helper_dir}/"
            cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-command-runner.exe" "${helper_dir}/"
          fi

      - name: Upload Cargo timings (nextest)
        if: always()
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: cargo-timings-rust-ci-nextest-${{ inputs.target }}-${{ inputs.profile }}
          path: codex-rs/target/**/cargo-timings/cargo-timing.html
          if-no-files-found: warn

      - name: Upload nextest archive
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: nextest-archive-${{ inputs.artifact_id }}
          path: ${{ runner.temp }}/nextest-archive/${{ env.NEXTEST_ARCHIVE_FILE }}
          if-no-files-found: error
          retention-days: 1

      - name: Upload runtime test helpers
        if: ${{ runner.os == 'Linux' || runner.os == 'Windows' }}
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: ${{ env.TEST_HELPERS_ARTIFACT }}
          path: ${{ runner.temp }}/${{ env.TEST_HELPERS_ARTIFACT }}/*
          if-no-files-found: error
          retention-days: 1

      - name: Save cargo home cache
        if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
          key: cargo-home-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}

      - name: Save sccache cache (fallback)
        if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
        with:
          path: ${{ env.SCCACHE_DIR }}
          key: sccache-${{ env.ARCHIVE_CACHE_RUNNER }}-${{ inputs.target }}-${{ inputs.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}

      - name: sccache stats
        if: always() && env.USE_SCCACHE == 'true'
        continue-on-error: true
        run: sccache --show-stats || true

      - name: sccache summary
        if: always() && env.USE_SCCACHE == 'true'
        shell: bash
        run: |
          {
            echo "### sccache stats — ${{ inputs.target }} (tests)";
            echo;
            echo '```';
            sccache --show-stats || true;
            echo '```';
          } >> "$GITHUB_STEP_SUMMARY"

  shard:
    name: Tests shard ${{ matrix.shard }}/4
    needs: archive
    runs-on: ${{ inputs.runner_group != '' && fromJSON(format('{{"group":"{0}","labels":"{1}"}}', inputs.runner_group, inputs.runner_labels)) || inputs.runner }}
    timeout-minutes: 60
    defaults:
      run:
        working-directory: codex-rs
    env:
      NEXTEST_ARCHIVE_FILE: nextest-${{ inputs.artifact_id }}.tar.zst
      TEST_HELPERS_ARTIFACT: nextest-test-helpers-${{ inputs.artifact_id }}
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          persist-credentials: false

      - name: Install Linux build dependencies
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          set -euo pipefail
          if command -v apt-get >/dev/null 2>&1; then
            sudo apt-get update -y
            sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev bubblewrap
          fi

      - name: Install DotSlash
        uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2

      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          targets: ${{ inputs.target }}

      - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
        with:
          tool: nextest
          version: 0.9.103

      - name: Enable unprivileged user namespaces (Linux)
        if: runner.os == 'Linux'
        run: |
          sudo sysctl -w kernel.unprivileged_userns_clone=1
          if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
            sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
          fi

      - name: Set up remote test env (Docker)
        if: ${{ runner.os == 'Linux' && inputs.remote_env }}
        shell: bash
        run: |
          set -euo pipefail
          export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME="codex-remote-test-env-${{ github.run_id }}-${{ matrix.shard }}"
          source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh"
          echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV"
          echo "CODEX_TEST_REMOTE_EXEC_SERVER_URL=${CODEX_TEST_REMOTE_EXEC_SERVER_URL}" >> "$GITHUB_ENV"

      - name: Download nextest archive
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: nextest-archive-${{ inputs.artifact_id }}
          path: ${{ runner.temp }}/nextest-archive

      - name: Download runtime test helpers
        if: ${{ runner.os == 'Linux' || runner.os == 'Windows' }}
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: ${{ env.TEST_HELPERS_ARTIFACT }}
          path: ${{ runner.temp }}/${{ env.TEST_HELPERS_ARTIFACT }}

      - name: tests
        id: test
        shell: bash
        run: |
          set -euo pipefail
          archive_file="${RUNNER_TEMP}/nextest-archive/${NEXTEST_ARCHIVE_FILE}"
          workspace_root="$(pwd)"

          if [[ "${RUNNER_OS}" == "Windows" ]]; then
            archive_file="$(cygpath -w "${archive_file}")"
            workspace_root="$(cygpath -w "${workspace_root}")"
          fi

          if [[ "${RUNNER_OS}" == "Linux" ]]; then
            helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
            helper_target_dir="$(pwd)/target/${{ inputs.target }}/${{ inputs.profile }}"
            mkdir -p "${helper_target_dir}"
            cp "${helper_dir}/codex-linux-sandbox" "${helper_target_dir}/"
            chmod +x "${helper_target_dir}/codex-linux-sandbox"
          elif [[ "${RUNNER_OS}" == "Windows" ]]; then
            helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
            helper_target_dir="$(pwd)/target/${{ inputs.target }}/${{ inputs.profile }}"
            mkdir -p "${helper_target_dir}"
            cp "${helper_dir}/codex-windows-sandbox-setup.exe" "${helper_target_dir}/"
            cp "${helper_dir}/codex-command-runner.exe" "${helper_target_dir}/"
          fi

          nextest_args=(
            run
            --no-fail-fast
            --archive-file "${archive_file}"
            --workspace-remap "${workspace_root}"
            --partition "hash:${{ matrix.shard }}/4"
          )
          if [[ "${{ inputs.test_threads }}" != "0" ]]; then
            nextest_args+=(--test-threads "${{ inputs.test_threads }}")
          fi

          test_command=(cargo nextest "${nextest_args[@]}")
          if [[ "${RUNNER_OS}" == "Linux" ]]; then
            sandbox_helper="${helper_target_dir}/codex-linux-sandbox"
            test_command=(
              env
              "CARGO_BIN_EXE_codex-linux-sandbox=${sandbox_helper}"
              "CARGO_BIN_EXE_codex_linux_sandbox=${sandbox_helper}"
              cargo nextest "${nextest_args[@]}"
            )
          elif [[ "${RUNNER_OS}" == "Windows" ]]; then
            setup_helper="$(cygpath -w "${helper_target_dir}/codex-windows-sandbox-setup.exe")"
            command_runner="$(cygpath -w "${helper_target_dir}/codex-command-runner.exe")"
            test_command=(
              env
              "CARGO_BIN_EXE_codex_windows_sandbox_setup=${setup_helper}"
              "CARGO_BIN_EXE_codex_command_runner=${command_runner}"
              cargo nextest "${nextest_args[@]}"
            )
          fi

          "${test_command[@]}"
        env:
          RUST_BACKTRACE: 1
          RUST_MIN_STACK: "8388608" # 8 MiB
          NEXTEST_STATUS_LEVEL: leak

      - name: Upload nextest JUnit report
        if: always()
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: nextest-junit-rust-ci-${{ inputs.artifact_id }}-shard-${{ matrix.shard }}
          path: codex-rs/target/nextest/default/junit.xml
          if-no-files-found: warn

      - name: Tear down remote test env
        if: ${{ always() && runner.os == 'Linux' && inputs.remote_env }}
        shell: bash
        run: |
          set +e
          if [[ "${STEPS_TEST_OUTCOME}" != "success" ]]; then
            docker logs "${CODEX_TEST_REMOTE_ENV}" || true
          fi
          docker rm -f "${CODEX_TEST_REMOTE_ENV}" >/dev/null 2>&1 || true
        env:
          STEPS_TEST_OUTCOME: ${{ steps.test.outcome }}

      - name: verify tests passed
        if: steps.test.outcome == 'failure'
        run: |
          echo "Tests failed. See logs for details."
          exit 1

  result:
    name: Platform result
    needs: shard
    if: always()
    runs-on: ubuntu-24.04
    steps:
      - name: Confirm test shards passed
        shell: bash
        run: |
          if [[ "${{ needs.shard.result }}" != "success" ]]; then
            echo "Nextest shards finished with result: ${{ needs.shard.result }}" >&2
            exit 1
          fi
