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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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 five 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 call it from the Python
  7. # release workflow. There is no `pull_request` trigger: a label trigger would
  8. # list gray skipped checks on every unrelated PR label event. Checkout uses the
  9. # triggering ref, so dispatch needs no separate ref input.
  10. on:
  11. workflow_call:
  12. inputs:
  13. targets:
  14. description: Comma-separated pkg targets to build; empty builds all five.
  15. type: string
  16. required: false
  17. default: ''
  18. release:
  19. description: Run as the native builder for the Python release workflow.
  20. type: boolean
  21. required: false
  22. default: false
  23. ci:
  24. description: Run as the required all-target Python runtime pull-request check.
  25. type: boolean
  26. required: false
  27. default: false
  28. secrets:
  29. DEEPSEEK_API_KEY_EXTERNAL:
  30. description: Real DeepSeek API key for trusted installed-wheel pull-request tests.
  31. required: false
  32. workflow_dispatch:
  33. inputs:
  34. targets:
  35. description: >-
  36. Comma-separated pkg targets to build. Any subset of:
  37. node24-linux-x64, node24-linux-arm64, node24-macos-arm64,
  38. node24-macos-x64, node24-win-x64. Empty builds all five.
  39. type: string
  40. required: false
  41. default: ''
  42. concurrency:
  43. # Keep the called workflow distinct from its caller's concurrency group;
  44. # github.workflow identifies the caller inside a reusable workflow and keeps
  45. # an ordinary CI run from cancelling a full release validation on the same ref.
  46. group: build-single-exe-${{ github.workflow }}-${{ github.ref }}
  47. cancel-in-progress: true
  48. permissions:
  49. contents: read
  50. env:
  51. # CI runs must never report to the production telemetry endpoint baked
  52. # into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
  53. DSH_TELEMETRY_DISABLED: '1'
  54. jobs:
  55. # Job-level conditions cannot inspect `matrix`, so validate target names and
  56. # construct the matrix before the dependent jobs.
  57. plan:
  58. name: plan targets
  59. if: inputs.ci || inputs.release || github.event_name == 'workflow_dispatch'
  60. runs-on: ubuntu-latest
  61. timeout-minutes: 5
  62. outputs:
  63. matrix: ${{ steps.plan.outputs.matrix }}
  64. version: ${{ steps.version.outputs.version }}
  65. repository-version: ${{ steps.version.outputs.repository-version }}
  66. steps:
  67. - uses: actions/checkout@v6
  68. - name: Resolve repository version
  69. id: version
  70. run: |
  71. set -euo pipefail
  72. python3 - <<'PY' >> "$GITHUB_OUTPUT"
  73. import runpy
  74. release = runpy.run_path("scripts/build-python-release.py")
  75. repository_version = release["repository_version"]()
  76. wheel_version = release["pep440_version"](repository_version)
  77. print(f"repository-version={repository_version}")
  78. print(f"version={wheel_version}")
  79. PY
  80. - name: Compute matrix from targets input
  81. id: plan
  82. env:
  83. # Blank dispatch inputs build all targets.
  84. TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64' }}
  85. run: |
  86. set -euo pipefail
  87. matrix='[]'
  88. IFS=',' read -r -a targets <<< "$TARGETS"
  89. for raw in "${targets[@]}"; do
  90. t="$(echo "$raw" | xargs)" # trim surrounding whitespace
  91. [ -z "$t" ] && continue
  92. # Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while
  93. # macos-latest is Apple Silicon; macos-15-intel is native x64.
  94. case "$t" in
  95. node24-linux-x64) runner=ubuntu-latest ;;
  96. node24-linux-arm64) runner=ubuntu-24.04-arm ;;
  97. node24-macos-arm64) runner=macos-latest ;;
  98. node24-macos-x64) runner=macos-15-intel ;;
  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-macos-x64, 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. # Release and manual builds retain disposable hosted images. Only trusted CI
  140. # may use the persistent Windows host; Linux requires an unavailable Docker daemon.
  141. runs-on: >-
  142. ${{ inputs.ci && !inputs.release
  143. && github.repository == 'deepseek-harness/deepseek-harness'
  144. && github.event_name == 'pull_request'
  145. && github.event.pull_request.head.repo.full_name == github.repository
  146. && !github.event.pull_request.head.repo.fork
  147. && github.event.pull_request.user.login != 'dependabot[bot]'
  148. && matrix.target == 'node24-win-x64'
  149. && vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
  150. && fromJSON('["self-hosted", "dsh-win-ci", "windows", "x64"]')
  151. || matrix.runner }}
  152. timeout-minutes: 45
  153. strategy:
  154. fail-fast: false
  155. matrix:
  156. include: ${{ fromJSON(needs.plan.outputs.matrix) }}
  157. steps:
  158. - uses: actions/checkout@v6
  159. with:
  160. persist-credentials: false
  161. - name: Prepare private Windows Python toolchain
  162. id: private-windows
  163. if: runner.os == 'Windows' && runner.environment == 'self-hosted'
  164. shell: pwsh
  165. run: ./scripts/setup-python-runtime-windows.ps1
  166. - uses: pnpm/action-setup@v4
  167. with:
  168. dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
  169. - name: Enable Windows Developer Mode (symlink support)
  170. if: runner.os == 'Windows' && runner.environment != 'self-hosted'
  171. shell: pwsh
  172. run: >-
  173. reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"
  174. /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1"
  175. # setup-node's built-in pnpm store cache keys on platform AND arch, so
  176. # the Linux architectures sharing runner.os stay on separate caches.
  177. - uses: actions/setup-node@v6
  178. with:
  179. node-version: 24
  180. cache: ${{ runner.environment != 'self-hosted' && 'pnpm' || '' }}
  181. package-manager-cache: false
  182. - uses: actions/setup-python@v6.3.0
  183. if: runner.environment != 'self-hosted'
  184. with:
  185. python-version: '3.10'
  186. - name: Install Python build tooling
  187. if: runner.environment != 'self-hosted'
  188. run: python -m pip install uv==0.11.23
  189. # Cache pkg's target Node binary; lockfile changes roll the
  190. # exact key while the restore prefix can seed its replacement.
  191. - uses: actions/cache@v4
  192. if: runner.environment != 'self-hosted'
  193. with:
  194. path: ~/.pkg-cache
  195. key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
  196. restore-keys: |
  197. pkg-fetch-${{ matrix.target }}-
  198. - name: Install (immutable)
  199. if: runner.environment != 'self-hosted'
  200. run: pnpm install --frozen-lockfile
  201. - name: Install private Windows dependencies (immutable)
  202. if: runner.os == 'Windows' && runner.environment == 'self-hosted'
  203. shell: pwsh
  204. run: |
  205. pnpm install --frozen-lockfile --package-import-method=copy
  206. if ($LASTEXITCODE -ne 0) { throw 'Private Windows dependency installation failed.' }
  207. - name: Rebuild Linux node-pty against manylinux 2.28
  208. if: runner.os == 'Linux'
  209. env:
  210. RUNNER_ARCH: ${{ runner.arch }}
  211. run: |
  212. set -euo pipefail
  213. case "$RUNNER_ARCH" in
  214. X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
  215. ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
  216. *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
  217. esac
  218. addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)"
  219. pnpm_setup_root="$(realpath "$(dirname "$(dirname "$PNPM_HOME")")")"
  220. (cd "$addon_dir" && npm_config_build_from_source=true pnpm run install)
  221. addon="$addon_dir/build/Release/pty.node"
  222. [ -f "$addon_dir/build/Makefile" ] || {
  223. echo "::error::node-pty install did not generate $addon_dir/build/Makefile"
  224. exit 1
  225. }
  226. docker run --rm \
  227. --user "$(id -u):$(id -g)" \
  228. -v "$PWD:$PWD" \
  229. -v "$HOME/.cache/node-gyp:$HOME/.cache/node-gyp:ro" \
  230. -v "$pnpm_setup_root:$pnpm_setup_root:ro" \
  231. -w "$addon_dir" \
  232. "$image" \
  233. bash -euxo pipefail -c \
  234. 'rm -rf build/Release && make -C build -j2 BUILDTYPE=Release'
  235. [ -f "$addon" ] || { echo "::error::$addon missing after manylinux rebuild"; exit 1; }
  236. readelf --version-info "$addon" | tee node-pty-glibc-versions.txt
  237. maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' node-pty-glibc-versions.txt | sort -V | tail -1)"
  238. [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found in $addon"; exit 1; }
  239. dpkg --compare-versions "$maximum" le 2.28 || {
  240. echo "::error::node-pty addon requires GLIBC_$maximum but wheel claims manylinux_2_28"
  241. exit 1
  242. }
  243. - name: Build single-exe
  244. env:
  245. DSH_BUILD_CLIENT_PROFILE: official
  246. run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
  247. - name: Resolve platform outputs (POSIX)
  248. id: runtime-posix
  249. if: runner.os != 'Windows'
  250. env:
  251. TARGET: ${{ matrix.target }}
  252. VERSION: ${{ needs.plan.outputs.version }}
  253. run: |
  254. set -euo pipefail
  255. platform="${TARGET#node24-}"
  256. exe="$PWD/dist-exe/deepseek-harness-sdk-runtime-$platform"
  257. case "$platform" in
  258. linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;;
  259. linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;;
  260. macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl ;;
  261. macos-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_x86_64.whl ;;
  262. *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
  263. esac
  264. [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; }
  265. echo "platform=$platform" >> "$GITHUB_OUTPUT"
  266. echo "exe=$exe" >> "$GITHUB_OUTPUT"
  267. echo "wheel=$wheel" >> "$GITHUB_OUTPUT"
  268. - name: Resolve platform outputs (Windows)
  269. id: runtime-windows
  270. if: runner.os == 'Windows'
  271. shell: pwsh
  272. env:
  273. TARGET: ${{ matrix.target }}
  274. VERSION: ${{ needs.plan.outputs.version }}
  275. run: |
  276. if ($env:TARGET -ne 'node24-win-x64') { throw "Unsupported runtime target $env:TARGET" }
  277. $platform = 'win-x64'
  278. $exe = Join-Path $PWD 'dist-exe\deepseek-harness-sdk-runtime-win-x64.exe'
  279. $wheel = "deepseek_harness_runtime_bin-$env:VERSION-py3-none-win_amd64.whl"
  280. if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw "Runtime executable is missing at $exe" }
  281. "platform=$platform" >> $env:GITHUB_OUTPUT
  282. "exe=$exe" >> $env:GITHUB_OUTPUT
  283. "wheel=$wheel" >> $env:GITHUB_OUTPUT
  284. - name: Build release-shaped runtime wheel
  285. run: >-
  286. python scripts/build-python-release.py
  287. --package runtime
  288. --platform "${{ steps.runtime-posix.outputs.platform || steps.runtime-windows.outputs.platform }}"
  289. --runtime-exe "${{ steps.runtime-posix.outputs.exe || steps.runtime-windows.outputs.exe }}"
  290. --output-dir dist-python
  291. - uses: actions/download-artifact@v8
  292. with:
  293. name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  294. path: dist-python
  295. - name: Install local SDK and runtime wheels into a clean venv (POSIX)
  296. id: smoke-venv-posix
  297. if: runner.os != 'Windows'
  298. env:
  299. RUNTIME_WHEEL: ${{ steps.runtime-posix.outputs.wheel }}
  300. SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  301. run: |
  302. set -euo pipefail
  303. venv="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-smoke-"))')"
  304. python -m venv "$venv"
  305. smoke_python="$venv/bin/python"
  306. "$smoke_python" -m pip install \
  307. "dist-python/$SDK_WHEEL" \
  308. "dist-python/$RUNTIME_WHEEL"
  309. echo "python=$smoke_python" >> "$GITHUB_OUTPUT"
  310. - name: Install local SDK and runtime wheels into a clean venv (Windows)
  311. id: smoke-venv-windows
  312. if: runner.os == 'Windows'
  313. shell: pwsh
  314. env:
  315. RUNTIME_WHEEL: ${{ steps.runtime-windows.outputs.wheel }}
  316. SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  317. run: |
  318. $venv = (& python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-smoke-"))').Trim()
  319. python -m venv $venv
  320. $smokePython = Join-Path $venv 'Scripts\python.exe'
  321. & $smokePython -m pip install "dist-python/$env:SDK_WHEEL" "dist-python/$env:RUNTIME_WHEEL"
  322. if ($LASTEXITCODE -ne 0) { throw "Wheel installation failed with exit code $LASTEXITCODE" }
  323. "python=$smokePython" >> $env:GITHUB_OUTPUT
  324. - name: Run installed-wheel keyless black-box tests (POSIX)
  325. if: runner.os != 'Windows'
  326. run: |
  327. set -euo pipefail
  328. blackbox_root="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-"))')"
  329. cd "$blackbox_root"
  330. env -u PYTHONPATH -u DSH_RUNTIME_MODE \
  331. "${{ steps.smoke-venv-posix.outputs.python }}" \
  332. "$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
  333. --scenario all \
  334. --installed-wheel
  335. - name: Run installed-wheel keyless black-box tests (Windows)
  336. if: runner.os == 'Windows'
  337. shell: pwsh
  338. run: |
  339. $blackboxRoot = (& python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-"))').Trim()
  340. Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
  341. Remove-Item Env:DSH_RUNTIME_MODE -ErrorAction SilentlyContinue
  342. Push-Location $blackboxRoot
  343. try {
  344. & "${{ steps.smoke-venv-windows.outputs.python }}" "$env:GITHUB_WORKSPACE\scripts\smoke-python-runtime.py" --scenario all --installed-wheel
  345. if ($LASTEXITCODE -ne 0) { throw "Installed-wheel black-box failed with exit code $LASTEXITCODE" }
  346. } finally {
  347. Pop-Location
  348. }
  349. - name: Preflight installed-wheel real API test (POSIX)
  350. if: >-
  351. inputs.ci
  352. && runner.os != 'Windows'
  353. && (github.event_name != 'pull_request'
  354. || !(github.event.pull_request.head.repo.fork
  355. || github.event.pull_request.user.login == 'dependabot[bot]'))
  356. env:
  357. DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
  358. run: |
  359. set -euo pipefail
  360. if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
  361. echo "::error::DEEPSEEK_API_KEY_EXTERNAL is empty; the installed-wheel real API test cannot self-skip."
  362. exit 1
  363. fi
  364. - name: Preflight installed-wheel real API test (Windows)
  365. if: >-
  366. inputs.ci
  367. && runner.os == 'Windows'
  368. && (github.event_name != 'pull_request'
  369. || !(github.event.pull_request.head.repo.fork
  370. || github.event.pull_request.user.login == 'dependabot[bot]'))
  371. shell: pwsh
  372. env:
  373. DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
  374. run: |
  375. if ([string]::IsNullOrWhiteSpace($env:DEEPSEEK_API_KEY)) {
  376. throw 'DEEPSEEK_API_KEY_EXTERNAL is empty; the installed-wheel real API test cannot self-skip.'
  377. }
  378. - name: Run installed-wheel real API black-box test (POSIX)
  379. if: >-
  380. inputs.ci
  381. && runner.os != 'Windows'
  382. && (github.event_name != 'pull_request'
  383. || !(github.event.pull_request.head.repo.fork
  384. || github.event.pull_request.user.login == 'dependabot[bot]'))
  385. env:
  386. DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
  387. DEEPSEEK_BASE_URL: https://api.deepseek.com
  388. run: |
  389. set -euo pipefail
  390. blackbox_root="$(python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-live-"))')"
  391. cd "$blackbox_root"
  392. env -u PYTHONPATH -u DSH_RUNTIME_MODE \
  393. "${{ steps.smoke-venv-posix.outputs.python }}" \
  394. "$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
  395. --scenario sdk-live \
  396. --installed-wheel
  397. - name: Run installed-wheel real API black-box test (Windows)
  398. if: >-
  399. inputs.ci
  400. && runner.os == 'Windows'
  401. && (github.event_name != 'pull_request'
  402. || !(github.event.pull_request.head.repo.fork
  403. || github.event.pull_request.user.login == 'dependabot[bot]'))
  404. shell: pwsh
  405. env:
  406. DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
  407. DEEPSEEK_BASE_URL: https://api.deepseek.com
  408. run: |
  409. $blackboxRoot = (& python -c 'import tempfile; print(tempfile.mkdtemp(prefix="dsh-sdk-blackbox-live-"))').Trim()
  410. Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue
  411. Remove-Item Env:DSH_RUNTIME_MODE -ErrorAction SilentlyContinue
  412. Push-Location $blackboxRoot
  413. try {
  414. & "${{ steps.smoke-venv-windows.outputs.python }}" "$env:GITHUB_WORKSPACE\scripts\smoke-python-runtime.py" --scenario sdk-live --installed-wheel
  415. if ($LASTEXITCODE -ne 0) { throw "Installed-wheel live API smoke failed with exit code $LASTEXITCODE" }
  416. } finally {
  417. Pop-Location
  418. }
  419. - name: Check Linux GLIBC requirements
  420. if: runner.os == 'Linux'
  421. run: |
  422. set -euo pipefail
  423. readelf --version-info "${{ steps.runtime-posix.outputs.exe }}" | tee glibc-versions.txt
  424. maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' glibc-versions.txt | sort -V | tail -1)"
  425. [ -n "$maximum" ] || { echo "::error::No GLIBC requirements found"; exit 1; }
  426. dpkg --compare-versions "$maximum" le 2.28 || {
  427. echo "::error::Executable requires GLIBC_$maximum but wheel claims manylinux_2_28"
  428. exit 1
  429. }
  430. - name: Check macOS payload architecture and deployment target
  431. if: runner.os == 'macOS'
  432. env:
  433. EXE: ${{ steps.runtime-posix.outputs.exe }}
  434. PLATFORM: ${{ steps.runtime-posix.outputs.platform }}
  435. run: |
  436. set -euo pipefail
  437. case "$PLATFORM" in
  438. macos-arm64) macho_arch=arm64 ;;
  439. macos-x64) macho_arch=x86_64 ;;
  440. *) echo "::error::Unsupported macOS platform $PLATFORM"; exit 1 ;;
  441. esac
  442. for payload in "$EXE" "$EXE-rg" "$EXE-spawn-helper"; do
  443. lipo "$payload" -verify_arch "$macho_arch"
  444. done
  445. python3 scripts/check-macos-deployment-target.py \
  446. --platform "$PLATFORM" "$EXE" "$EXE-rg" "$EXE-spawn-helper"
  447. - name: Run wheel in a manylinux 2.28 container
  448. if: runner.os == 'Linux'
  449. env:
  450. RUNNER_ARCH: ${{ runner.arch }}
  451. RUNTIME_WHEEL: ${{ steps.runtime-posix.outputs.wheel }}
  452. SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
  453. run: |
  454. set -euo pipefail
  455. case "$RUNNER_ARCH" in
  456. X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
  457. ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
  458. *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
  459. esac
  460. docker run --rm -e RUNTIME_WHEEL -e SDK_WHEEL -e DSH_TELEMETRY_DISABLED -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c '
  461. /opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk
  462. /tmp/dsh-sdk/bin/python -m pip install "/work/dist-python/$SDK_WHEEL" "/work/dist-python/$RUNTIME_WHEEL"
  463. mkdir -p /tmp/dsh-sdk-manylinux-smoke
  464. cd /tmp/dsh-sdk-manylinux-smoke
  465. env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default --installed-wheel
  466. env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp --installed-wheel
  467. '
  468. - uses: actions/upload-artifact@v7
  469. with:
  470. name: ${{ steps.runtime-posix.outputs.wheel || steps.runtime-windows.outputs.wheel }}
  471. path: dist-python/${{ steps.runtime-posix.outputs.wheel || steps.runtime-windows.outputs.wheel }}
  472. if-no-files-found: error
  473. retention-days: 7
  474. # Node action posts consume temp/compile-cache paths. pnpm post skips
  475. # pruning without run_install; no Python/pkg/npm subprocess runs after cleanup.
  476. - name: Remove private Windows toolchain and test directories
  477. if: always() && steps.private-windows.outputs.root != ''
  478. shell: pwsh
  479. env:
  480. PRIVATE_ROOT: ${{ steps.private-windows.outputs.root }}
  481. run: |
  482. Set-Location $env:GITHUB_WORKSPACE
  483. $env:TMP = $env:RUNNER_TEMP
  484. $env:TEMP = $env:RUNNER_TEMP
  485. Remove-Item Env:NODE_COMPILE_CACHE -ErrorAction SilentlyContinue
  486. "NODE_COMPILE_CACHE=" >> $env:GITHUB_ENV
  487. "TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
  488. "TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
  489. node -e "const fs = require('node:fs'); const root = process.env.PRIVATE_ROOT; if (fs.lstatSync(root, { throwIfNoEntry: false })?.isSymbolicLink()) fs.unlinkSync(root); else fs.rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })"
  490. if ($LASTEXITCODE -ne 0) { throw 'Private Windows job directory removal failed.' }
  491. if (Test-Path -LiteralPath $env:PRIVATE_ROOT) { throw 'Private Windows job directory survived cleanup.' }