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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. name: Build single-exe
  2. # Single-file executable (single-exe) builds of the DeepSeek Harness SDK
  3. # runtime. The build pipeline and target platforms are specified in
  4. # docs/rfc/implemented/architecture/2026-07-10-single-exe-sdk-runtime.md: each target is
  5. # built natively on a runner of its own platform (no cross-compilation) by
  6. # scripts/build-exe-for-python-sdk.ts, which deploys the dsh-jsonrpc-agent-pkg closure manifest
  7. # closure with @yao-pkg/pkg into dist-exe/.
  8. #
  9. # Each build leg uploads two artifacts:
  10. # - dsh-jsonrpc-agent-pkg-<target> — the bare single-file exe, for
  11. # consumers that want just the binary.
  12. # - deepseek-harness-python-<target> — the whole python/ directory as a
  13. # tar.gz with that exe already embedded (the build script syncs it into
  14. # the Python runtime package): unpack and both packages pip install
  15. # as-is, the checked-in default runtime/cordis.yml is editable in
  16. # place, and the embedded exe also runs directly.
  17. #
  18. # workflow_dispatch ONLY — deliberately not triggered by push/pull_request:
  19. # the exe is a release-style deliverable, and the build (full pnpm build +
  20. # pnpm deploy + pkg across a 3-platform matrix, ~100MB per artifact) is far
  21. # too expensive to run as a per-commit CI signal. Dispatch it from the
  22. # Actions tab when artifacts are needed. There is no `ref` input on purpose:
  23. # actions/checkout already checks out the branch/tag the run was dispatched
  24. # on.
  25. on:
  26. workflow_dispatch:
  27. inputs:
  28. targets:
  29. description: >-
  30. Comma-separated pkg targets to build. Any subset of:
  31. node24-linux-x64, node24-linux-arm64, node24-macos-arm64.
  32. type: string
  33. required: false
  34. default: node24-linux-x64,node24-linux-arm64,node24-macos-arm64
  35. # Manual runs on the same ref supersede each other.
  36. concurrency:
  37. group: ${{ github.workflow }}-${{ github.ref }}
  38. cancel-in-progress: true
  39. # Least privilege: the jobs only read the repo; artifact upload needs no
  40. # extra scope.
  41. permissions:
  42. contents: read
  43. jobs:
  44. # Turn the `targets` input into the build matrix. The `matrix` context is
  45. # not available in a job-level `if:` (jobs.<job_id>.if only sees
  46. # github/needs/vars/inputs), so target selection happens here instead of
  47. # skipping matrix legs; an unknown target name fails the whole run loudly
  48. # instead of being silently ignored.
  49. plan:
  50. name: plan targets
  51. runs-on: ubuntu-latest
  52. timeout-minutes: 5
  53. outputs:
  54. matrix: ${{ steps.plan.outputs.matrix }}
  55. steps:
  56. - name: Compute matrix from targets input
  57. id: plan
  58. env:
  59. TARGETS: ${{ inputs.targets }}
  60. run: |
  61. set -euo pipefail
  62. matrix='[]'
  63. IFS=',' read -r -a targets <<< "$TARGETS"
  64. for raw in "${targets[@]}"; do
  65. t="$(echo "$raw" | xargs)" # trim surrounding whitespace
  66. [ -z "$t" ] && continue
  67. # Native builds only — each target maps to a runner of its own
  68. # platform: linux-arm64 uses GitHub's hosted arm64 label
  69. # ubuntu-24.04-arm (there is no ubuntu-latest-arm), macos-arm64
  70. # uses macos-latest (Apple Silicon since macos-14).
  71. case "$t" in
  72. node24-linux-x64) runner=ubuntu-latest ;;
  73. node24-linux-arm64) runner=ubuntu-24.04-arm ;;
  74. node24-macos-arm64) runner=macos-latest ;;
  75. *)
  76. echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64."
  77. exit 1
  78. ;;
  79. esac
  80. matrix="$(jq -c --arg target "$t" --arg runner "$runner" '. + [{target: $target, runner: $runner}]' <<< "$matrix")"
  81. done
  82. if [ "$matrix" = '[]' ]; then
  83. echo "::error::The targets input selected nothing to build."
  84. exit 1
  85. fi
  86. echo "Matrix: $matrix"
  87. echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
  88. build:
  89. needs: plan
  90. name: ${{ matrix.target }}
  91. runs-on: ${{ matrix.runner }}
  92. timeout-minutes: 45
  93. strategy:
  94. fail-fast: false
  95. matrix:
  96. include: ${{ fromJSON(needs.plan.outputs.matrix) }}
  97. steps:
  98. - uses: actions/checkout@v6
  99. - uses: actions/setup-node@v6
  100. with:
  101. node-version: 24
  102. - name: Enable corepack (pnpm)
  103. run: corepack enable
  104. - name: Resolve pnpm store path
  105. id: pnpm-store
  106. run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
  107. # Unlike ci.yml (x64-only), this matrix spans two Linux architectures
  108. # that share runner.os, so runner.arch is part of the key.
  109. - uses: actions/cache@v4
  110. with:
  111. path: ${{ steps.pnpm-store.outputs.path }}
  112. key: ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
  113. restore-keys: |
  114. ${{ runner.os }}-${{ runner.arch }}-node-24-pnpm-
  115. # The first run per target has pkg-fetch download yao-pkg's patched
  116. # Node binary into ~/.pkg-cache; cache it so later runs skip the
  117. # download. The target string pins Node major + platform + arch;
  118. # pnpm-lock.yaml rolls the key when @yao-pkg/pkg (and with it the
  119. # pinned patched-binary version) is bumped, with restore-keys still
  120. # seeding from the previous cache.
  121. - uses: actions/cache@v4
  122. with:
  123. path: ~/.pkg-cache
  124. key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
  125. restore-keys: |
  126. pkg-fetch-${{ matrix.target }}-
  127. - name: Install (immutable)
  128. run: pnpm install --frozen-lockfile
  129. # The script runs the whole pipeline itself (pnpm run build → pnpm
  130. # deploy --prod → pkg) and writes its output to dist-exe/ by default.
  131. - name: Build single-exe
  132. run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
  133. - uses: actions/upload-artifact@v6
  134. with:
  135. name: dsh-jsonrpc-agent-pkg-${{ matrix.target }}
  136. path: dist-exe/
  137. if-no-files-found: error
  138. # After the build step, python/ is already in its complete
  139. # distributable shape — the script synced this leg's exe into
  140. # python/sdk-runtime/src/deepseek_harness_runtime/runtime/ next to the
  141. # checked-in default cordis.yml — so packing is all that is left.
  142. # Everything goes under one top-level deepseek-harness-python/
  143. # directory so unpacking never scatters files. Shipping a tar (rather
  144. # than uploading the tree bare) preserves the exe's executable bit —
  145. # tar keeps file modes; upload-artifact's zip does not.
  146. #
  147. # Excluded: runtime/node/ (dev-only node-mode carrier, ~140MB) plus
  148. # __pycache__ / .pytest_cache / .venv / node_modules anywhere (install
  149. # or test leftovers); uv.lock stays in.
  150. #
  151. # Portability: GNU tar (ubuntu) and bsdtar (macos) both accept
  152. # `tar -czf out.tar.gz --exclude=… -C <parent> <dir>` and both treat
  153. # an excluded directory as pruned (no descent). The top-level rename
  154. # is done by copying into a temp dir first — GNU --transform / BSD -s
  155. # are single-implementation flags.
  156. - name: Pack Python SDK bundle
  157. id: pack
  158. env:
  159. TARGET: ${{ matrix.target }}
  160. run: |
  161. set -euo pipefail
  162. platform_arch="${TARGET#*-}" # node24-macos-arm64 -> macos-arm64
  163. exe="python/sdk-runtime/src/deepseek_harness_runtime/runtime/dsh-jsonrpc-agent-pkg-${platform_arch}"
  164. if [ ! -x "$exe" ]; then
  165. echo "::error::$exe missing or not executable — the build step did not sync this leg's exe into the Python runtime package; refusing to pack a half-empty bundle."
  166. exit 1
  167. fi
  168. staging="$(mktemp -d)"
  169. cp -R python "$staging/deepseek-harness-python"
  170. bundle="deepseek-harness-python-${platform_arch}.tar.gz"
  171. tar -czf "$bundle" \
  172. --exclude='deepseek-harness-python/sdk-runtime/src/deepseek_harness_runtime/runtime/node' \
  173. --exclude='__pycache__' \
  174. --exclude='.pytest_cache' \
  175. --exclude='.venv' \
  176. --exclude='node_modules' \
  177. -C "$staging" deepseek-harness-python
  178. rm -rf "$staging"
  179. ls -lh "$bundle"
  180. echo "bundle=$bundle" >> "$GITHUB_OUTPUT"
  181. - uses: actions/upload-artifact@v6
  182. with:
  183. name: deepseek-harness-python-${{ matrix.target }}
  184. path: ${{ steps.pack.outputs.bundle }}
  185. if-no-files-found: error