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, label a PR `build-exe` # (remove and reapply to rerun), or call it from the Python release workflow. # 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 three. type: string required: false default: '' release: description: Run as the native builder for the Python release workflow. type: boolean required: false default: false 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: # Keep the called workflow distinct from its caller's concurrency group; # github.workflow identifies the caller inside a reusable workflow. group: build-single-exe-${{ 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.release || 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 }} 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: # 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_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 # 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)" 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 "$HOME/setup-pnpm:$HOME/setup-pnpm: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 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_14_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_sdk-${{ 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-sdk=="$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: Check macOS deployment target if: runner.os == 'macOS' env: EXE: ${{ steps.runtime.outputs.exe }} run: | set -euo pipefail minos="$(otool -l "$EXE" | awk '/LC_BUILD_VERSION/{seen=1; next} seen && /minos/{print $2; exit}')" [ -n "$minos" ] || { echo "::error::No macOS deployment target found in $EXE"; exit 1; } python3 - "$minos" <<'PY' import sys actual = tuple(int(part) for part in sys.argv[1].split(".")) claimed = (14, 0) width = max(len(actual), len(claimed)) actual += (0,) * (width - len(actual)) claimed += (0,) * (width - len(claimed)) if actual > claimed: raise SystemExit( f"::error::Executable requires macOS {sys.argv[1]} but the wheel claims macosx_14_0" ) PY - 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 -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 --find-links /work/dist-python deepseek-harness-sdk=="$VERSION" /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default ' - uses: actions/upload-artifact@v7 with: name: ${{ steps.runtime.outputs.wheel }} path: dist-python/${{ steps.runtime.outputs.wheel }} if-no-files-found: error retention-days: 7