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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  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 three 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 three.
  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. workflow_dispatch:
  23. inputs:
  24. targets:
  25. description: >-
  26. Comma-separated pkg targets to build. Any subset of:
  27. node24-linux-x64, node24-linux-arm64, node24-macos-arm64.
  28. Empty builds all three.
  29. type: string
  30. required: false
  31. default: ''
  32. pull_request:
  33. types: [labeled]
  34. concurrency:
  35. # Keep the called workflow distinct from its caller's concurrency group;
  36. # github.workflow identifies the caller inside a reusable workflow.
  37. group: build-single-exe-${{ github.ref }}
  38. cancel-in-progress: true
  39. permissions:
  40. contents: read
  41. env:
  42. # CI runs must never report to the production telemetry endpoint baked
  43. # into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
  44. DSH_TELEMETRY_DISABLED: '1'
  45. jobs:
  46. # Job-level conditions cannot inspect `matrix`, so validate target names and
  47. # construct the matrix before the dependent jobs.
  48. plan:
  49. name: plan targets
  50. if: inputs.release || github.event_name == 'workflow_dispatch' || github.event.label.name == 'build-exe'
  51. runs-on: ubuntu-latest
  52. timeout-minutes: 5
  53. outputs:
  54. matrix: ${{ steps.plan.outputs.matrix }}
  55. version: ${{ steps.version.outputs.version }}
  56. repository-version: ${{ steps.version.outputs.repository-version }}
  57. steps:
  58. - uses: actions/checkout@v6
  59. - name: Resolve repository version
  60. id: version
  61. run: |
  62. set -euo pipefail
  63. python3 - <<'PY' >> "$GITHUB_OUTPUT"
  64. import runpy
  65. release = runpy.run_path("scripts/build-python-release.py")
  66. repository_version = release["repository_version"]()
  67. wheel_version = release["pep440_version"](repository_version)
  68. print(f"repository-version={repository_version}")
  69. print(f"version={wheel_version}")
  70. PY
  71. - name: Compute matrix from targets input
  72. id: plan
  73. env:
  74. # Label runs and blank dispatch inputs build all targets.
  75. TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64' }}
  76. run: |
  77. set -euo pipefail
  78. matrix='[]'
  79. IFS=',' read -r -a targets <<< "$TARGETS"
  80. for raw in "${targets[@]}"; do
  81. t="$(echo "$raw" | xargs)" # trim surrounding whitespace
  82. [ -z "$t" ] && continue
  83. # Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while
  84. # macos-latest is Apple Silicon.
  85. case "$t" in
  86. node24-linux-x64) runner=ubuntu-latest ;;
  87. node24-linux-arm64) runner=ubuntu-24.04-arm ;;
  88. node24-macos-arm64) runner=macos-latest ;;
  89. *)
  90. echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64."
  91. exit 1
  92. ;;
  93. esac
  94. matrix="$(jq -c --arg target "$t" --arg runner "$runner" '. + [{target: $target, runner: $runner}]' <<< "$matrix")"
  95. done
  96. if [ "$matrix" = '[]' ]; then
  97. echo "::error::The targets input selected nothing to build."
  98. exit 1
  99. fi
  100. echo "Matrix: $matrix"
  101. echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
  102. sdk-wheel:
  103. needs: plan
  104. name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  105. runs-on: ubuntu-latest
  106. timeout-minutes: 5
  107. steps:
  108. - uses: actions/checkout@v6
  109. - uses: actions/setup-python@v6.3.0
  110. with:
  111. python-version: '3.10'
  112. - name: Install Python build tooling
  113. run: python -m pip install uv==0.11.23
  114. - name: Build release-shaped SDK wheel
  115. run: >-
  116. python scripts/build-python-release.py
  117. --package sdk
  118. --output-dir dist-python
  119. - uses: actions/upload-artifact@v7
  120. with:
  121. name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  122. path: dist-python/deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  123. if-no-files-found: error
  124. retention-days: 7
  125. build:
  126. needs: [plan, sdk-wheel]
  127. name: ${{ matrix.target }}
  128. runs-on: ${{ matrix.runner }}
  129. timeout-minutes: 45
  130. strategy:
  131. fail-fast: false
  132. matrix:
  133. include: ${{ fromJSON(needs.plan.outputs.matrix) }}
  134. steps:
  135. - uses: actions/checkout@v6
  136. - uses: pnpm/action-setup@v4
  137. # setup-node's built-in pnpm store cache keys on platform AND arch, so
  138. # the Linux architectures sharing runner.os stay on separate caches.
  139. - uses: actions/setup-node@v6
  140. with:
  141. node-version: 24
  142. cache: pnpm
  143. - uses: actions/setup-python@v6.3.0
  144. with:
  145. python-version: '3.10'
  146. - name: Install Python build tooling
  147. run: python -m pip install uv==0.11.23
  148. # Cache pkg's target Node binary; lockfile changes roll the
  149. # exact key while the restore prefix can seed its replacement.
  150. - uses: actions/cache@v4
  151. with:
  152. path: ~/.pkg-cache
  153. key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
  154. restore-keys: |
  155. pkg-fetch-${{ matrix.target }}-
  156. - name: Install (immutable)
  157. run: pnpm install --frozen-lockfile
  158. - name: Rebuild Linux node-pty against manylinux 2.28
  159. if: runner.os == 'Linux'
  160. env:
  161. RUNNER_ARCH: ${{ runner.arch }}
  162. run: |
  163. set -euo pipefail
  164. case "$RUNNER_ARCH" in
  165. X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
  166. ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
  167. *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
  168. esac
  169. addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)"
  170. addon="$addon_dir/build/Release/pty.node"
  171. [ -f "$addon_dir/build/Makefile" ] || {
  172. echo "::error::node-pty install did not generate $addon_dir/build/Makefile"
  173. exit 1
  174. }
  175. docker run --rm \
  176. --user "$(id -u):$(id -g)" \
  177. -v "$PWD:$PWD" \
  178. -v "$HOME/.cache/node-gyp:$HOME/.cache/node-gyp:ro" \
  179. -v "$HOME/setup-pnpm:$HOME/setup-pnpm:ro" \
  180. -w "$addon_dir" \
  181. "$image" \
  182. bash -euxo pipefail -c \
  183. 'rm -rf build/Release && make -C build -j2 BUILDTYPE=Release'
  184. [ -f "$addon" ] || { echo "::error::$addon missing after manylinux rebuild"; exit 1; }
  185. readelf --version-info "$addon" | tee node-pty-glibc-versions.txt
  186. maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' node-pty-glibc-versions.txt | sort -V | tail -1)"
  187. [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found in $addon"; exit 1; }
  188. dpkg --compare-versions "$maximum" le 2.28 || {
  189. echo "::error::node-pty addon requires GLIBC_$maximum but wheel claims manylinux_2_28"
  190. exit 1
  191. }
  192. - name: Build single-exe
  193. run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
  194. - name: Resolve platform outputs
  195. id: runtime
  196. env:
  197. TARGET: ${{ matrix.target }}
  198. VERSION: ${{ needs.plan.outputs.version }}
  199. run: |
  200. set -euo pipefail
  201. platform="${TARGET#node24-}"
  202. exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform"
  203. [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
  204. case "$platform" in
  205. linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
  206. linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
  207. macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl ;;
  208. *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
  209. esac
  210. echo "platform=$platform" >> "$GITHUB_OUTPUT"
  211. echo "exe=$exe" >> "$GITHUB_OUTPUT"
  212. echo "wheel=$wheel" >> "$GITHUB_OUTPUT"
  213. - name: Full-turn SDK, executable snapshot, and direct-binary smoke
  214. run: >-
  215. uv run --python 3.10 --group test --project python/sdk
  216. python scripts/smoke-python-runtime.py
  217. --scenario all
  218. --exe "${{ steps.runtime.outputs.exe }}"
  219. - name: Build release-shaped runtime wheel
  220. run: >-
  221. python scripts/build-python-release.py
  222. --package runtime
  223. --platform "${{ steps.runtime.outputs.platform }}"
  224. --runtime-exe "${{ steps.runtime.outputs.exe }}"
  225. --output-dir dist-python
  226. - uses: actions/download-artifact@v8
  227. with:
  228. name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  229. path: dist-python
  230. - name: Install only the SDK into a clean venv and run zero-config
  231. env:
  232. VERSION: ${{ needs.plan.outputs.version }}
  233. run: |
  234. set -euo pipefail
  235. python -m venv "$RUNNER_TEMP/dsh-sdk-smoke"
  236. "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \
  237. --find-links dist-python \
  238. deepseek-harness-sdk=="$VERSION"
  239. "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \
  240. --scenario sdk-default
  241. - name: Check Linux GLIBC requirements
  242. if: runner.os == 'Linux'
  243. run: |
  244. set -euo pipefail
  245. readelf --version-info "${{ steps.runtime.outputs.exe }}" | tee glibc-versions.txt
  246. maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' glibc-versions.txt | sort -V | tail -1)"
  247. [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found"; exit 1; }
  248. dpkg --compare-versions "$maximum" le 2.28 || {
  249. echo "::error::Executable requires GLIBC_$maximum but wheel claims manylinux_2_28"
  250. exit 1
  251. }
  252. - name: Check macOS deployment target
  253. if: runner.os == 'macOS'
  254. env:
  255. EXE: ${{ steps.runtime.outputs.exe }}
  256. run: >-
  257. python3 scripts/check-macos-deployment-target.py
  258. "$EXE" "$EXE-spawn-helper"
  259. - name: Run wheel in a manylinux 2.28 container
  260. if: runner.os == 'Linux'
  261. env:
  262. RUNNER_ARCH: ${{ runner.arch }}
  263. VERSION: ${{ needs.plan.outputs.version }}
  264. run: |
  265. set -euo pipefail
  266. case "$RUNNER_ARCH" in
  267. X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
  268. ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
  269. *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
  270. esac
  271. docker run --rm -e VERSION -e DSH_TELEMETRY_DISABLED -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
  272. /opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
  273. /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness-sdk=="$VERSION"
  274. /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default
  275. '
  276. - uses: actions/upload-artifact@v7
  277. with:
  278. name: ${{ steps.runtime.outputs.wheel }}
  279. path: dist-python/${{ steps.runtime.outputs.wheel }}
  280. if-no-files-found: error
  281. retention-days: 7