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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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 or label a PR
  7. # `build-exe` (remove and reapply to rerun). Checkout uses the triggering ref,
  8. # so dispatch needs no separate ref input.
  9. on:
  10. workflow_dispatch:
  11. inputs:
  12. targets:
  13. description: >-
  14. Comma-separated pkg targets to build. Any subset of:
  15. node24-linux-x64, node24-linux-arm64, node24-macos-arm64.
  16. Empty builds all three.
  17. type: string
  18. required: false
  19. default: ''
  20. pull_request:
  21. types: [labeled]
  22. concurrency:
  23. group: ${{ github.workflow }}-${{ github.ref }}
  24. cancel-in-progress: true
  25. permissions:
  26. contents: read
  27. jobs:
  28. # Job-level conditions cannot inspect `matrix`, so validate target names and
  29. # construct the matrix before the dependent jobs.
  30. plan:
  31. name: plan targets
  32. if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'build-exe'
  33. runs-on: ubuntu-latest
  34. timeout-minutes: 5
  35. outputs:
  36. matrix: ${{ steps.plan.outputs.matrix }}
  37. version: ${{ steps.version.outputs.version }}
  38. steps:
  39. - uses: actions/checkout@v6
  40. - name: Resolve repository version
  41. id: version
  42. run: |
  43. set -euo pipefail
  44. version="$(jq -r '.version // empty' package.json)"
  45. [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
  46. echo "::error::package.json version must be stable X.Y.Z, got '$version'"
  47. exit 1
  48. }
  49. echo "version=$version" >> "$GITHUB_OUTPUT"
  50. - name: Compute matrix from targets input
  51. id: plan
  52. env:
  53. # Label runs and blank dispatch inputs build all targets.
  54. TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64' }}
  55. run: |
  56. set -euo pipefail
  57. matrix='[]'
  58. IFS=',' read -r -a targets <<< "$TARGETS"
  59. for raw in "${targets[@]}"; do
  60. t="$(echo "$raw" | xargs)" # trim surrounding whitespace
  61. [ -z "$t" ] && continue
  62. # Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while
  63. # macos-latest is Apple Silicon.
  64. case "$t" in
  65. node24-linux-x64) runner=ubuntu-latest ;;
  66. node24-linux-arm64) runner=ubuntu-24.04-arm ;;
  67. node24-macos-arm64) runner=macos-latest ;;
  68. *)
  69. echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64."
  70. exit 1
  71. ;;
  72. esac
  73. matrix="$(jq -c --arg target "$t" --arg runner "$runner" '. + [{target: $target, runner: $runner}]' <<< "$matrix")"
  74. done
  75. if [ "$matrix" = '[]' ]; then
  76. echo "::error::The targets input selected nothing to build."
  77. exit 1
  78. fi
  79. echo "Matrix: $matrix"
  80. echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
  81. sdk-wheel:
  82. needs: plan
  83. name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
  84. runs-on: ubuntu-latest
  85. timeout-minutes: 5
  86. steps:
  87. - uses: actions/checkout@v6
  88. - uses: actions/setup-python@v6
  89. with:
  90. python-version: '3.10'
  91. - name: Install Python build tooling
  92. run: python -m pip install uv==0.11.23
  93. - name: Build release-shaped SDK wheel
  94. run: >-
  95. python scripts/build-python-release.py
  96. --package sdk
  97. --output-dir dist-python
  98. - uses: actions/upload-artifact@v6
  99. with:
  100. name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
  101. path: dist-python/deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
  102. if-no-files-found: error
  103. build:
  104. needs: [plan, sdk-wheel]
  105. name: ${{ matrix.target }}
  106. runs-on: ${{ matrix.runner }}
  107. timeout-minutes: 45
  108. strategy:
  109. fail-fast: false
  110. matrix:
  111. include: ${{ fromJSON(needs.plan.outputs.matrix) }}
  112. steps:
  113. - uses: actions/checkout@v6
  114. - uses: actions/setup-node@v6
  115. with:
  116. node-version: 24
  117. - uses: actions/setup-python@v6
  118. with:
  119. python-version: '3.10'
  120. - name: Install Python build tooling
  121. run: python -m pip install uv==0.11.23
  122. - name: Enable corepack (pnpm)
  123. run: corepack enable
  124. - name: Resolve pnpm store path
  125. id: pnpm-store
  126. run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
  127. # Linux architectures share runner.os, so the cache key includes arch.
  128. - uses: actions/cache@v4
  129. with:
  130. path: ${{ steps.pnpm-store.outputs.path }}
  131. key: ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
  132. restore-keys: |
  133. ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-
  134. # Cache pkg's target Node binary; lockfile changes roll the
  135. # exact key while the restore prefix can seed its replacement.
  136. - uses: actions/cache@v4
  137. with:
  138. path: ~/.pkg-cache
  139. key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
  140. restore-keys: |
  141. pkg-fetch-${{ matrix.target }}-
  142. - name: Install (immutable)
  143. run: pnpm install --frozen-lockfile
  144. - name: Build single-exe
  145. run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
  146. - name: Resolve platform outputs
  147. id: runtime
  148. env:
  149. TARGET: ${{ matrix.target }}
  150. VERSION: ${{ needs.plan.outputs.version }}
  151. run: |
  152. set -euo pipefail
  153. platform="${TARGET#node24-}"
  154. exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform"
  155. [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
  156. case "$platform" in
  157. linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
  158. linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
  159. macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_11_0_arm64.whl ;;
  160. *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
  161. esac
  162. echo "platform=$platform" >> "$GITHUB_OUTPUT"
  163. echo "exe=$exe" >> "$GITHUB_OUTPUT"
  164. echo "wheel=$wheel" >> "$GITHUB_OUTPUT"
  165. - name: Full-turn SDK, executable snapshot, and direct-binary smoke
  166. run: >-
  167. uv run --python 3.10 --group test --project python/sdk
  168. python scripts/smoke-python-runtime.py
  169. --scenario all
  170. --exe "${{ steps.runtime.outputs.exe }}"
  171. - name: Build release-shaped runtime wheel
  172. run: >-
  173. python scripts/build-python-release.py
  174. --package runtime
  175. --platform "${{ steps.runtime.outputs.platform }}"
  176. --runtime-exe "${{ steps.runtime.outputs.exe }}"
  177. --output-dir dist-python
  178. - uses: actions/download-artifact@v8
  179. with:
  180. name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl
  181. path: dist-python
  182. - name: Install only the SDK into a clean venv and run zero-config
  183. env:
  184. VERSION: ${{ needs.plan.outputs.version }}
  185. run: |
  186. set -euo pipefail
  187. python -m venv "$RUNNER_TEMP/dsh-sdk-smoke"
  188. "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \
  189. --find-links dist-python \
  190. deepseek-harness=="$VERSION"
  191. "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \
  192. --scenario sdk-default
  193. - name: Check Linux GLIBC requirements
  194. if: runner.os == 'Linux'
  195. run: |
  196. set -euo pipefail
  197. readelf --version-info "${{ steps.runtime.outputs.exe }}" | tee glibc-versions.txt
  198. maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' glibc-versions.txt | sort -V | tail -1)"
  199. [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found"; exit 1; }
  200. dpkg --compare-versions "$maximum" le 2.28 || {
  201. echo "::error::Executable requires GLIBC_$maximum but wheel claims manylinux_2_28"
  202. exit 1
  203. }
  204. - name: Run wheel in a manylinux 2.28 container
  205. if: runner.os == 'Linux'
  206. env:
  207. RUNNER_ARCH: ${{ runner.arch }}
  208. VERSION: ${{ needs.plan.outputs.version }}
  209. run: |
  210. set -euo pipefail
  211. case "$RUNNER_ARCH" in
  212. X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
  213. ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
  214. *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
  215. esac
  216. docker run --rm -e VERSION -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
  217. /opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
  218. /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness=="$VERSION"
  219. /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default
  220. '
  221. - uses: actions/upload-artifact@v6
  222. with:
  223. name: ${{ steps.runtime.outputs.wheel }}
  224. path: dist-python/${{ steps.runtime.outputs.wheel }}
  225. if-no-files-found: error