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 three runtime wheels; subset
# dispatch retains the SDK wheel and selected runtime wheels. Bare executables
# and source closures are test inputs. Run manually or label a PR
# `build-exe` (remove and reapply to rerun). Checkout uses the triggering ref,
# so dispatch needs no separate ref input.
on:
  workflow_dispatch:
    inputs:
      targets:
        description: >-
          Comma-separated pkg targets to build. Any subset of:
          node24-linux-x64, node24-linux-arm64, node24-macos-arm64.
          Empty builds all three.
        type: string
        required: false
        default: ''
  pull_request:
    types: [labeled]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

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

      - name: Resolve repository version
        id: version
        run: |
          set -euo pipefail
          version="$(jq -r '.version // empty' package.json)"
          [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
            echo "::error::package.json version must be stable X.Y.Z, got '$version'"
            exit 1
          }
          echo "version=$version" >> "$GITHUB_OUTPUT"

      - name: Compute matrix from targets input
        id: plan
        env:
          # Label runs and blank dispatch inputs build all targets.
          TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64' }}
        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 ;;
              *)
                echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64."
                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-${{ 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
        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@v6
        with:
          name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
          path: dist-python/deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
          if-no-files-found: error

  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: actions/setup-node@v6
        with:
          node-version: 24

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

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

      - name: Enable corepack (pnpm)
        run: corepack enable

      - name: Resolve pnpm store path
        id: pnpm-store
        run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

      # Linux architectures share runner.os, so the cache key includes arch.
      - uses: actions/cache@v4
        with:
          path: ${{ steps.pnpm-store.outputs.path }}
          key: ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-

      # 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: Build single-exe
        run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}

      - name: Resolve platform outputs
        id: runtime
        env:
          TARGET: ${{ matrix.target }}
          VERSION: ${{ needs.plan.outputs.version }}
        run: |
          set -euo pipefail
          platform="${TARGET#node24-}"
          exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform"
          [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
          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_11_0_arm64.whl ;;
            *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
          esac
          echo "platform=$platform" >> "$GITHUB_OUTPUT"
          echo "exe=$exe" >> "$GITHUB_OUTPUT"
          echo "wheel=$wheel" >> "$GITHUB_OUTPUT"

      - name: Full-turn SDK, executable snapshot, and direct-binary smoke
        run: >-
          uv run --python 3.10 --group test --project python/sdk
          python scripts/smoke-python-runtime.py
          --scenario all
          --exe "${{ steps.runtime.outputs.exe }}"

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

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

      - name: Install only the SDK into a clean venv and run zero-config
        env:
          VERSION: ${{ needs.plan.outputs.version }}
        run: |
          set -euo pipefail
          python -m venv "$RUNNER_TEMP/dsh-sdk-smoke"
          "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \
            --find-links dist-python \
            deepseek-harness=="$VERSION"
          "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \
            --scenario sdk-default

      - name: Check Linux GLIBC requirements
        if: runner.os == 'Linux'
        run: |
          set -euo pipefail
          readelf --version-info "${{ steps.runtime.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: Run wheel in a manylinux 2.28 container
        if: runner.os == 'Linux'
        env:
          RUNNER_ARCH: ${{ runner.arch }}
          VERSION: ${{ needs.plan.outputs.version }}
        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 VERSION -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 --find-links /work/dist-python deepseek-harness=="$VERSION"
            /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default
          '

      - uses: actions/upload-artifact@v6
        with:
          name: ${{ steps.runtime.outputs.wheel }}
          path: dist-python/${{ steps.runtime.outputs.wheel }}
          if-no-files-found: error
