build-exe-for-python-sdk.yml 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. name: Build single-exe
  2. # Native builds for the release targets; see
  3. # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
  4. # A full target run retains one SDK wheel and four runtime wheels; subset
  5. # dispatch retains the SDK wheel and selected runtime wheels. Bare executables
  6. # and source closures are test inputs. Run manually, label a PR `build-exe`
  7. # (remove and reapply to rerun), or call it from the Python release workflow.
  8. # Checkout uses the triggering ref, so dispatch needs no separate ref input.
  9. on:
  10. workflow_call:
  11. inputs:
  12. targets:
  13. description: Comma-separated pkg targets to build; empty builds all four.
  14. type: string
  15. required: false
  16. default: ''
  17. release:
  18. description: Run as the native builder for the Python release workflow.
  19. type: boolean
  20. required: false
  21. default: false
  22. ci:
  23. description: Run as the required all-target Python runtime pull-request check.
  24. type: boolean
  25. required: false
  26. default: false
  27. secrets:
  28. DEEPSEEK_API_KEY_EXTERNAL:
  29. description: Real DeepSeek API key for trusted installed-wheel pull-request tests.
  30. required: false
  31. workflow_dispatch:
  32. inputs:
  33. targets:
  34. description: >-
  35. Comma-separated pkg targets to build. Any subset of:
  36. node24-linux-x64, node24-linux-arm64, node24-macos-arm64,
  37. node24-win-x64. Empty builds all four.
  38. type: string
  39. required: false
  40. default: ''
  41. pull_request:
  42. types: [labeled]
  43. concurrency:
  44. # Keep the called workflow distinct from its caller's concurrency group;
  45. # github.workflow identifies the caller inside a reusable workflow and keeps
  46. # an ordinary CI run from cancelling a full release validation on the same ref.
  47. group: build-single-exe-${{ github.workflow }}-${{ github.ref }}
  48. cancel-in-progress: true
  49. permissions:
  50. contents: read
  51. env:
  52. # CI runs must never report to the production telemetry endpoint baked
  53. # into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
  54. DSH_TELEMETRY_DISABLED: '1'
  55. jobs:
  56. # Job-level conditions cannot inspect `matrix`, so validate target names and
  57. # construct the matrix before the dependent jobs.
  58. plan:
  59. name: plan targets
  60. if: inputs.ci || inputs.release || github.event_name == 'workflow_dispatch' || github.event.label.name == 'build-exe'
  61. runs-on: ubuntu-latest
  62. timeout-minutes: 5
  63. outputs:
  64. matrix: ${{ steps.plan.outputs.matrix }}
  65. version: ${{ steps.version.outputs.version }}
  66. repository-version: ${{ steps.version.outputs.repository-version }}
  67. steps:
  68. - uses: actions/checkout@v6
  69. - name: Resolve repository version
  70. id: version
  71. run: |
  72. set -euo pipefail
  73. python3 - <<'PY' >> "$GITHUB_OUTPUT"
  74. import runpy
  75. release = runpy.run_path("scripts/build-python-release.py")
  76. repository_version = release["repository_version"]()
  77. wheel_version = release["pep440_version"](repository_version)
  78. print(f"repository-version={repository_version}")
  79. print(f"version={wheel_version}")
  80. PY
  81. - name: Compute matrix from targets input
  82. id: plan
  83. env:
  84. # Label runs and blank dispatch inputs build all targets.
  85. TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64' }}
  86. run: |
  87. set -euo pipefail
  88. matrix='[]'
  89. IFS=',' read -r -a targets <<< "$TARGETS"
  90. for raw in "${targets[@]}"; do
  91. t="$(echo "$raw" | xargs)" # trim surrounding whitespace
  92. [ -z "$t" ] && continue
  93. # Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while
  94. # macos-latest is Apple Silicon.
  95. case "$t" in
  96. node24-linux-x64) runner=ubuntu-latest ;;
  97. node24-linux-arm64) runner=ubuntu-24.04-arm ;;
  98. node24-macos-arm64) runner=macos-latest ;;
  99. node24-win-x64) runner=windows-2025 ;;
  100. *)
  101. echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64, node24-win-x64."
  102. exit 1
  103. ;;
  104. esac
  105. matrix="$(jq -c --arg target "$t" --arg runner "$runner" '. + [{target: $target, runner: $runner}]' <<< "$matrix")"
  106. done
  107. if [ "$matrix" = '[]' ]; then
  108. echo "::error::The targets input selected nothing to build."
  109. exit 1
  110. fi
  111. echo "Matrix: $matrix"
  112. echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
  113. sdk-wheel:
  114. needs: plan
  115. name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  116. runs-on: ubuntu-latest
  117. timeout-minutes: 5
  118. steps:
  119. - uses: actions/checkout@v6
  120. - uses: actions/setup-python@v6.3.0
  121. with:
  122. python-version: '3.10'
  123. - name: Install Python build tooling
  124. run: python -m pip install uv==0.11.23
  125. - name: Build release-shaped SDK wheel
  126. run: >-
  127. python scripts/build-python-release.py
  128. --package sdk
  129. --output-dir dist-python
  130. - uses: actions/upload-artifact@v7
  131. with:
  132. name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  133. path: dist-python/deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  134. if-no-files-found: error
  135. retention-days: 7
  136. build:
  137. needs: [plan, sdk-wheel]
  138. name: ${{ matrix.target }}
  139. runs-on: ${{ matrix.runner }}
  140. timeout-minutes: 45
  141. strategy:
  142. fail-fast: false
  143. matrix:
  144. include: ${{ fromJSON(needs.plan.outputs.matrix) }}
  145. defaults:
  146. run:
  147. shell: bash
  148. steps:
  149. - uses: actions/checkout@v6
  150. - uses: pnpm/action-setup@v4
  151. with:
  152. dest: ${{ runner.temp }}/setup-pnpm-js
  153. - name: Enable Windows Developer Mode (symlink support)
  154. if: runner.os == 'Windows'
  155. shell: pwsh
  156. run: >-
  157. reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
  158. /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
  159. # setup-node's built-in pnpm store cache keys on platform AND arch, so
  160. # the Linux architectures sharing runner.os stay on separate caches.
  161. - uses: actions/setup-node@v6
  162. with:
  163. node-version: 24
  164. cache: pnpm
  165. - uses: actions/setup-python@v6.3.0
  166. with:
  167. python-version: '3.10'
  168. - name: Install Python build tooling
  169. run: python -m pip install uv==0.11.23
  170. # Cache pkg's target Node binary; lockfile changes roll the
  171. # exact key while the restore prefix can seed its replacement.
  172. - uses: actions/cache@v4
  173. with:
  174. path: ~/.pkg-cache
  175. key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
  176. restore-keys: |
  177. pkg-fetch-${{ matrix.target }}-
  178. - name: Install (immutable)
  179. run: pnpm install --frozen-lockfile
  180. - name: Rebuild Linux node-pty against manylinux 2.28
  181. if: runner.os == 'Linux'
  182. env:
  183. RUNNER_ARCH: ${{ runner.arch }}
  184. run: |
  185. set -euo pipefail
  186. case "$RUNNER_ARCH" in
  187. X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
  188. ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
  189. *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
  190. esac
  191. addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)"
  192. pnpm_setup_root="$(realpath "$(dirname "$(dirname "$PNPM_HOME")")")"
  193. (cd "$addon_dir" && npm_config_build_from_source=true pnpm run install)
  194. addon="$addon_dir/build/Release/pty.node"
  195. [ -f "$addon_dir/build/Makefile" ] || {
  196. echo "::error::node-pty install did not generate $addon_dir/build/Makefile"
  197. exit 1
  198. }
  199. docker run --rm \
  200. --user "$(id -u):$(id -g)" \
  201. -v "$PWD:$PWD" \
  202. -v "$HOME/.cache/node-gyp:$HOME/.cache/node-gyp:ro" \
  203. -v "$pnpm_setup_root:$pnpm_setup_root:ro" \
  204. -w "$addon_dir" \
  205. "$image" \
  206. bash -euxo pipefail -c \
  207. 'rm -rf build/Release && make -C build -j2 BUILDTYPE=Release'
  208. [ -f "$addon" ] || { echo "::error::$addon missing after manylinux rebuild"; exit 1; }
  209. readelf --version-info "$addon" | tee node-pty-glibc-versions.txt
  210. maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' node-pty-glibc-versions.txt | sort -V | tail -1)"
  211. [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found in $addon"; exit 1; }
  212. dpkg --compare-versions "$maximum" le 2.28 || {
  213. echo "::error::node-pty addon requires GLIBC_$maximum but wheel claims manylinux_2_28"
  214. exit 1
  215. }
  216. - name: Build single-exe
  217. env:
  218. DSH_BUILD_CLIENT_PROFILE: official
  219. run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
  220. - name: Resolve platform outputs
  221. id: runtime
  222. env:
  223. TARGET: ${{ matrix.target }}
  224. VERSION: ${{ needs.plan.outputs.version }}
  225. run: |
  226. set -euo pipefail
  227. platform="${TARGET#node24-}"
  228. exe="$PWD/dist-exe/deepseek-harness-sdk-runtime-$platform"
  229. case "$platform" in
  230. linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
  231. linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
  232. macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl ;;
  233. win-x64)
  234. exe="$exe.exe"
  235. wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-win_amd64.whl
  236. ;;
  237. *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
  238. esac
  239. if [ "$RUNNER_OS" = Windows ]; then
  240. [ -f "$exe" ] || { echo "::error::$exe missing"; exit 1; }
  241. else
  242. [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
  243. fi
  244. echo "platform=$platform" >> "$GITHUB_OUTPUT"
  245. echo "exe=$exe" >> "$GITHUB_OUTPUT"
  246. echo "wheel=$wheel" >> "$GITHUB_OUTPUT"
  247. - name: Build release-shaped runtime wheel
  248. run: >-
  249. python scripts/build-python-release.py
  250. --package runtime
  251. --platform "${{ steps.runtime.outputs.platform }}"
  252. --runtime-exe "${{ steps.runtime.outputs.exe }}"
  253. --output-dir dist-python
  254. - uses: actions/download-artifact@v8
  255. with:
  256. name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  257. path: dist-python
  258. - name: Install local SDK and runtime wheels into a clean venv
  259. id: smoke-venv
  260. env:
  261. RUNTIME_WHEEL: ${{ steps.runtime.outputs.wheel }}
  262. SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  263. run: |
  264. set -euo pipefail
  265. venv="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-smoke-"))')"
  266. python -m venv "$venv"
  267. if [ "$RUNNER_OS" = Windows ]; then
  268. smoke_python="$(cygpath -u "$venv")/Scripts/python.exe"
  269. else
  270. smoke_python="$venv/bin/python"
  271. fi
  272. "$smoke_python" -m pip install \
  273. "dist-python/$SDK_WHEEL" \
  274. "dist-python/$RUNTIME_WHEEL"
  275. echo "python=$smoke_python" >> "$GITHUB_OUTPUT"
  276. - name: Run installed-wheel keyless black-box tests
  277. run: |
  278. set -euo pipefail
  279. blackbox_root="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-"))')"
  280. if [ "$RUNNER_OS" = Windows ]; then blackbox_root="$(cygpath -u "$blackbox_root")"; fi
  281. cd "$blackbox_root"
  282. env -u PYTHONPATH -u DSH_RUNTIME_MODE \
  283. "${{ steps.smoke-venv.outputs.python }}" \
  284. "$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
  285. --scenario all \
  286. --installed-wheel
  287. - name: Preflight installed-wheel real API test
  288. if: >-
  289. inputs.ci
  290. && (github.event_name != 'pull_request'
  291. || !(github.event.pull_request.head.repo.fork
  292. || github.event.pull_request.user.login == 'dependabot[bot]'))
  293. env:
  294. DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
  295. run: |
  296. set -euo pipefail
  297. if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
  298. echo "::error::DEEPSEEK_API_KEY_EXTERNAL is empty; the installed-wheel real API test cannot self-skip."
  299. exit 1
  300. fi
  301. - name: Run installed-wheel real API black-box test
  302. if: >-
  303. inputs.ci
  304. && (github.event_name != 'pull_request'
  305. || !(github.event.pull_request.head.repo.fork
  306. || github.event.pull_request.user.login == 'dependabot[bot]'))
  307. env:
  308. DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
  309. DEEPSEEK_BASE_URL: https://api.deepseek.com
  310. run: |
  311. set -euo pipefail
  312. blackbox_root="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-live-"))')"
  313. if [ "$RUNNER_OS" = Windows ]; then blackbox_root="$(cygpath -u "$blackbox_root")"; fi
  314. cd "$blackbox_root"
  315. env -u PYTHONPATH -u DSH_RUNTIME_MODE \
  316. "${{ steps.smoke-venv.outputs.python }}" \
  317. "$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
  318. --scenario sdk-live \
  319. --installed-wheel
  320. - name: Check Linux GLIBC requirements
  321. if: runner.os == 'Linux'
  322. run: |
  323. set -euo pipefail
  324. readelf --version-info "${{ steps.runtime.outputs.exe }}" | tee glibc-versions.txt
  325. maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' glibc-versions.txt | sort -V | tail -1)"
  326. [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found"; exit 1; }
  327. dpkg --compare-versions "$maximum" le 2.28 || {
  328. echo "::error::Executable requires GLIBC_$maximum but wheel claims manylinux_2_28"
  329. exit 1
  330. }
  331. - name: Check macOS deployment target
  332. if: runner.os == 'macOS'
  333. env:
  334. EXE: ${{ steps.runtime.outputs.exe }}
  335. run: >-
  336. python3 scripts/check-macos-deployment-target.py
  337. "$EXE" "$EXE-spawn-helper"
  338. - name: Run wheel in a manylinux 2.28 container
  339. if: runner.os == 'Linux'
  340. env:
  341. RUNNER_ARCH: ${{ runner.arch }}
  342. RUNTIME_WHEEL: ${{ steps.runtime.outputs.wheel }}
  343. SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  344. run: |
  345. set -euo pipefail
  346. case "$RUNNER_ARCH" in
  347. X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
  348. ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
  349. *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
  350. esac
  351. docker run --rm -e RUNTIME_WHEEL -e SDK_WHEEL -e DSH_TELEMETRY_DISABLED -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
  352. /opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
  353. /tmp/dsh-sdk/bin/python -m pip install "/work/dist-python/$SDK_WHEEL" "/work/dist-python/$RUNTIME_WHEEL"
  354. mkdir -p /tmp/dsh-sdk-manylinux-smoke
  355. cd /tmp/dsh-sdk-manylinux-smoke
  356. env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default --installed-wheel
  357. env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp --installed-wheel
  358. '
  359. - uses: actions/upload-artifact@v7
  360. with:
  361. name: ${{ steps.runtime.outputs.wheel }}
  362. path: dist-python/${{ steps.runtime.outputs.wheel }}
  363. if-no-files-found: error
  364. retention-days: 7