name: Build single-exe

# Native builds for the release targets; see
# .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
# A full target run retains one SDK wheel and four runtime wheels; subset
# dispatch retains the SDK wheel and selected runtime wheels. Bare executables
# and source closures are test inputs. Run manually or call it from the Python
# release workflow. There is no `pull_request` trigger: a label trigger would
# list gray skipped checks on every unrelated PR label event. Checkout uses the
# triggering ref, so dispatch needs no separate ref input.
on:
  workflow_call:
    inputs:
      targets:
        description: Comma-separated pkg targets to build; empty builds all four.
        type: string
        required: false
        default: ''
      release:
        description: Run as the native builder for the Python release workflow.
        type: boolean
        required: false
        default: false
      ci:
        description: Run as the required all-target Python runtime pull-request check.
        type: boolean
        required: false
        default: false
    secrets:
      DEEPSEEK_API_KEY_EXTERNAL:
        description: Real DeepSeek API key for trusted installed-wheel pull-request tests.
        required: false
  workflow_dispatch:
    inputs:
      targets:
        description: >-
          Comma-separated pkg targets to build. Any subset of:
          node24-linux-x64, node24-linux-arm64, node24-macos-arm64,
          node24-win-x64. Empty builds all four.
        type: string
        required: false
        default: ''

concurrency:
  # Keep the called workflow distinct from its caller's concurrency group;
  # github.workflow identifies the caller inside a reusable workflow and keeps
  # an ordinary CI run from cancelling a full release validation on the same ref.
  group: build-single-exe-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  # CI runs must never report to the production telemetry endpoint baked
  # into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
  DSH_TELEMETRY_DISABLED: '1'

jobs:
  # Job-level conditions cannot inspect `matrix`, so validate target names and
  # construct the matrix before the dependent jobs.
  plan:
    name: plan targets
    if: inputs.ci || inputs.release || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      matrix: ${{ steps.plan.outputs.matrix }}
      version: ${{ steps.version.outputs.version }}
      repository-version: ${{ steps.version.outputs.repository-version }}
    steps:
      - uses: actions/checkout@v6

      - name: Resolve repository version
        id: version
        run: |
          set -euo pipefail
          python3 - <<'PY' >> "$GITHUB_OUTPUT"
          import runpy

          release = runpy.run_path("scripts/build-python-release.py")
          repository_version = release["repository_version"]()
          wheel_version = release["pep440_version"](repository_version)
          print(f"repository-version={repository_version}")
          print(f"version={wheel_version}")
          PY

      - name: Compute matrix from targets input
        id: plan
        env:
          # Blank dispatch inputs build all targets.
          TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64' }}
        run: |
          set -euo pipefail
          matrix='[]'
          IFS=',' read -r -a targets <<< "$TARGETS"
          for raw in "${targets[@]}"; do
            t="$(echo "$raw" | xargs)" # trim surrounding whitespace
            [ -z "$t" ] && continue
            # Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while
            # macos-latest is Apple Silicon.
            case "$t" in
              node24-linux-x64)   runner=ubuntu-latest ;;
              node24-linux-arm64) runner=ubuntu-24.04-arm ;;
              node24-macos-arm64) runner=macos-latest ;;
              node24-win-x64)     runner=windows-2025 ;;
              *)
                echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64, node24-win-x64."
                exit 1
                ;;
            esac
            matrix="$(jq -c --arg target "$t" --arg runner "$runner" '. + [{target: $target, runner: $runner}]' <<< "$matrix")"
          done
          if [ "$matrix" = '[]' ]; then
            echo "::error::The targets input selected nothing to build."
            exit 1
          fi
          echo "Matrix: $matrix"
          echo "matrix=$matrix" >> "$GITHUB_OUTPUT"

  sdk-wheel:
    needs: plan
    name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-python@v6.3.0
        with:
          python-version: '3.10'

      - name: Install Python build tooling
        run: python -m pip install uv==0.11.23

      - name: Build release-shaped SDK wheel
        run: >-
          python scripts/build-python-release.py
          --package sdk
          --output-dir dist-python

      - uses: actions/upload-artifact@v7
        with:
          name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
          path: dist-python/deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
          if-no-files-found: error
          retention-days: 7

  build:
    needs: [plan, sdk-wheel]
    name: ${{ matrix.target }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 45
    strategy:
      fail-fast: false
      matrix:
        include: ${{ fromJSON(needs.plan.outputs.matrix) }}
    steps:
      - uses: actions/checkout@v6

      - uses: pnpm/action-setup@v4
        with:
          dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}

      - name: Enable Windows Developer Mode (symlink support)
        if: runner.os == 'Windows'
        shell: pwsh
        run: >-
          reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
          /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"

      # setup-node's built-in pnpm store cache keys on platform AND arch, so
      # the Linux architectures sharing runner.os stay on separate caches.
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - uses: actions/setup-python@v6.3.0
        with:
          python-version: '3.10'

      - name: Install Python build tooling
        run: python -m pip install uv==0.11.23

      # Cache pkg's target Node binary; lockfile changes roll the
      # exact key while the restore prefix can seed its replacement.
      - uses: actions/cache@v4
        with:
          path: ~/.pkg-cache
          key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            pkg-fetch-${{ matrix.target }}-

      - name: Install (immutable)
        run: pnpm install --frozen-lockfile

      - name: Rebuild Linux node-pty against manylinux 2.28
        if: runner.os == 'Linux'
        env:
          RUNNER_ARCH: ${{ runner.arch }}
        run: |
          set -euo pipefail
          case "$RUNNER_ARCH" in
            X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
            ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
            *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
          esac
          addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)"
          pnpm_setup_root="$(realpath "$(dirname "$(dirname "$PNPM_HOME")")")"
          (cd "$addon_dir" && npm_config_build_from_source=true pnpm run install)
          addon="$addon_dir/build/Release/pty.node"
          [ -f "$addon_dir/build/Makefile" ] || {
            echo "::error::node-pty install did not generate $addon_dir/build/Makefile"
            exit 1
          }
          docker run --rm \
            --user "$(id -u):$(id -g)" \
            -v "$PWD:$PWD" \
            -v "$HOME/.cache/node-gyp:$HOME/.cache/node-gyp:ro" \
            -v "$pnpm_setup_root:$pnpm_setup_root:ro" \
            -w "$addon_dir" \
            "$image" \
            bash -euxo pipefail -c \
              'rm -rf build/Release && make -C build -j2 BUILDTYPE=Release'
          [ -f "$addon" ] || { echo "::error::$addon missing after manylinux rebuild"; exit 1; }
          readelf --version-info "$addon" | tee node-pty-glibc-versions.txt
          maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' node-pty-glibc-versions.txt | sort -V | tail -1)"
          [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found in $addon"; exit 1; }
          dpkg --compare-versions "$maximum" le 2.28 || {
            echo "::error::node-pty addon requires GLIBC_$maximum but wheel claims manylinux_2_28"
            exit 1
          }

      - name: Build single-exe
        env:
          DSH_BUILD_CLIENT_PROFILE: official
        run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}

      - name: Resolve platform outputs (POSIX)
        id: runtime-posix
        if: runner.os != 'Windows'
        env:
          TARGET: ${{ matrix.target }}
          VERSION: ${{ needs.plan.outputs.version }}
        run: |
          set -euo pipefail
          platform="${TARGET#node24-}"
          exe="$PWD/dist-exe/deepseek-harness-sdk-runtime-$platform"
          case "$platform" in
            linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
            linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
            macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl ;;
            *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
          esac
          [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
          echo "platform=$platform" >> "$GITHUB_OUTPUT"
          echo "exe=$exe" >> "$GITHUB_OUTPUT"
          echo "wheel=$wheel" >> "$GITHUB_OUTPUT"

      - name: Resolve platform outputs (Windows)
        id: runtime-windows
        if: runner.os == 'Windows'
        shell: pwsh
        env:
          TARGET: ${{ matrix.target }}
          VERSION: ${{ needs.plan.outputs.version }}
        run: |
          if ($env:TARGET -ne 'node24-win-x64') { throw "Unsupported runtime target $env:TARGET" }
          $platform = 'win-x64'
          $exe = Join-Path $PWD 'dist-exe\deepseek-harness-sdk-runtime-win-x64.exe'
          $wheel = "deepseek_harness_runtime_bin-$env:VERSION-py3-none-win_amd64.whl"
          if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw "Runtime executable is missing at $exe" }
          "platform=$platform" >> $env:GITHUB_OUTPUT
          "exe=$exe" >> $env:GITHUB_OUTPUT
          "wheel=$wheel" >> $env:GITHUB_OUTPUT

      - name: Build release-shaped runtime wheel
        run: >-
          python scripts/build-python-release.py
          --package runtime
          --platform "${{ steps.runtime-posix.outputs.platform || steps.runtime-windows.outputs.platform }}"
          --runtime-exe "${{ steps.runtime-posix.outputs.exe || steps.runtime-windows.outputs.exe }}"
          --output-dir dist-python

      - uses: actions/download-artifact@v8
        with:
          name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
          path: dist-python

      - name: Install local SDK and runtime wheels into a clean venv (POSIX)
        id: smoke-venv-posix
        if: runner.os != 'Windows'
        env:
          RUNTIME_WHEEL: ${{ steps.runtime-posix.outputs.wheel }}
          SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
        run: |
          set -euo pipefail
          venv="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-smoke-"))')"
          python -m venv "$venv"
          smoke_python="$venv/bin/python"
          "$smoke_python" -m pip install \
            "dist-python/$SDK_WHEEL" \
            "dist-python/$RUNTIME_WHEEL"
          echo "python=$smoke_python" >> "$GITHUB_OUTPUT"

      - name: Install local SDK and runtime wheels into a clean venv (Windows)
        id: smoke-venv-windows
        if: runner.os == 'Windows'
        shell: pwsh
        env:
          RUNTIME_WHEEL: ${{ steps.runtime-windows.outputs.wheel }}
          SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
        run: |
          $venv = (& python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-smoke-"))').Trim()
          python -m venv $venv
          $smokePython = Join-Path $venv 'Scripts\python.exe'
          & $smokePython -m pip install "dist-python/$env:SDK_WHEEL" "dist-python/$env:RUNTIME_WHEEL"
          if ($LASTEXITCODE -ne 0) { throw "Wheel installation failed with exit code $LASTEXITCODE" }
          "python=$smokePython" >> $env:GITHUB_OUTPUT

      - name: Run installed-wheel keyless black-box tests (POSIX)
        if: runner.os != 'Windows'
        run: |
          set -euo pipefail
          blackbox_root="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-"))')"
          cd "$blackbox_root"
          env -u PYTHONPATH -u DSH_RUNTIME_MODE \
            "${{ steps.smoke-venv-posix.outputs.python }}" \
            "$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
            --scenario all \
            --installed-wheel

      - name: Run installed-wheel keyless black-box tests (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $blackboxRoot = (& python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-"))').Trim()
          Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
          Remove-Item Env:DSH_RUNTIME_MODE -ErrorAction SilentlyContinue
          Push-Location $blackboxRoot
          try {
            & "${{ steps.smoke-venv-windows.outputs.python }}" "$env:GITHUB_WORKSPACE\scripts\smoke-python-runtime.py" --scenario all --installed-wheel
            if ($LASTEXITCODE -ne 0) { throw "Installed-wheel black-box failed with exit code $LASTEXITCODE" }
          } finally {
            Pop-Location
          }

      - name: Preflight installed-wheel real API test (POSIX)
        if: >-
          inputs.ci
          && runner.os != 'Windows'
          && (github.event_name != 'pull_request'
              || !(github.event.pull_request.head.repo.fork
                   || github.event.pull_request.user.login == 'dependabot[bot]'))
        env:
          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
        run: |
          set -euo pipefail
          if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
            echo "::error::DEEPSEEK_API_KEY_EXTERNAL is empty; the installed-wheel real API test cannot self-skip."
            exit 1
          fi

      - name: Preflight installed-wheel real API test (Windows)
        if: >-
          inputs.ci
          && runner.os == 'Windows'
          && (github.event_name != 'pull_request'
              || !(github.event.pull_request.head.repo.fork
                   || github.event.pull_request.user.login == 'dependabot[bot]'))
        shell: pwsh
        env:
          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
        run: |
          if ([string]::IsNullOrWhiteSpace($env:DEEPSEEK_API_KEY)) {
            throw 'DEEPSEEK_API_KEY_EXTERNAL is empty; the installed-wheel real API test cannot self-skip.'
          }

      - name: Run installed-wheel real API black-box test (POSIX)
        if: >-
          inputs.ci
          && runner.os != 'Windows'
          && (github.event_name != 'pull_request'
              || !(github.event.pull_request.head.repo.fork
                   || github.event.pull_request.user.login == 'dependabot[bot]'))
        env:
          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
          DEEPSEEK_BASE_URL: https://api.deepseek.com
        run: |
          set -euo pipefail
          blackbox_root="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-live-"))')"
          cd "$blackbox_root"
          env -u PYTHONPATH -u DSH_RUNTIME_MODE \
            "${{ steps.smoke-venv-posix.outputs.python }}" \
            "$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
            --scenario sdk-live \
            --installed-wheel

      - name: Run installed-wheel real API black-box test (Windows)
        if: >-
          inputs.ci
          && runner.os == 'Windows'
          && (github.event_name != 'pull_request'
              || !(github.event.pull_request.head.repo.fork
                   || github.event.pull_request.user.login == 'dependabot[bot]'))
        shell: pwsh
        env:
          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
          DEEPSEEK_BASE_URL: https://api.deepseek.com
        run: |
          $blackboxRoot = (& python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-live-"))').Trim()
          Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
          Remove-Item Env:DSH_RUNTIME_MODE -ErrorAction SilentlyContinue
          Push-Location $blackboxRoot
          try {
            & "${{ steps.smoke-venv-windows.outputs.python }}" "$env:GITHUB_WORKSPACE\scripts\smoke-python-runtime.py" --scenario sdk-live --installed-wheel
            if ($LASTEXITCODE -ne 0) { throw "Installed-wheel live API smoke failed with exit code $LASTEXITCODE" }
          } finally {
            Pop-Location
          }

      - name: Check Linux GLIBC requirements
        if: runner.os == 'Linux'
        run: |
          set -euo pipefail
          readelf --version-info "${{ steps.runtime-posix.outputs.exe }}" | tee glibc-versions.txt
          maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' glibc-versions.txt | sort -V | tail -1)"
          [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found"; exit 1; }
          dpkg --compare-versions "$maximum" le 2.28 || {
            echo "::error::Executable requires GLIBC_$maximum but wheel claims manylinux_2_28"
            exit 1
          }

      - name: Check macOS deployment target
        if: runner.os == 'macOS'
        env:
          EXE: ${{ steps.runtime-posix.outputs.exe }}
        run: >-
          python3 scripts/check-macos-deployment-target.py
          "$EXE" "$EXE-spawn-helper"

      - name: Run wheel in a manylinux 2.28 container
        if: runner.os == 'Linux'
        env:
          RUNNER_ARCH: ${{ runner.arch }}
          RUNTIME_WHEEL: ${{ steps.runtime-posix.outputs.wheel }}
          SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
        run: |
          set -euo pipefail
          case "$RUNNER_ARCH" in
            X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
            ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
            *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
          esac
          docker run --rm -e RUNTIME_WHEEL -e SDK_WHEEL -e DSH_TELEMETRY_DISABLED -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
            /opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
            /tmp/dsh-sdk/bin/python -m pip install "/work/dist-python/$SDK_WHEEL" "/work/dist-python/$RUNTIME_WHEEL"
            mkdir -p /tmp/dsh-sdk-manylinux-smoke
            cd /tmp/dsh-sdk-manylinux-smoke
            env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default --installed-wheel
            env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp --installed-wheel
          '

      - uses: actions/upload-artifact@v7
        with:
          name: ${{ steps.runtime-posix.outputs.wheel || steps.runtime-windows.outputs.wheel }}
          path: dist-python/${{ steps.runtime-posix.outputs.wheel || steps.runtime-windows.outputs.wheel }}
          if-no-files-found: error
          retention-days: 7
