python-release.yml 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. name: Release (Python)
  2. # A PR labeled python-release-dry-run or a manual run with publish=false builds
  3. # and validates the complete release without registry credentials. Publication
  4. # is accepted only from a manual run on the matching python-v* tag when the
  5. # private publisher-repository identity and public-PyPI switch are configured.
  6. on:
  7. workflow_dispatch:
  8. inputs:
  9. publish:
  10. description: Publish the validated wheels to public PyPI. Must run from a python-v* tag.
  11. required: true
  12. type: boolean
  13. default: false
  14. pull_request:
  15. types: [labeled]
  16. permissions:
  17. contents: read
  18. concurrency:
  19. # Public runs stay globally serialized across tags. Dry runs remain isolated
  20. # by ref so they do not block an intentional publication.
  21. group: ${{ github.event_name == 'workflow_dispatch' && inputs.publish && 'python-publication' || format('{0}-{1}', github.workflow, github.ref) }}
  22. cancel-in-progress: false
  23. jobs:
  24. build:
  25. name: Build four wheels
  26. if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'python-release-dry-run'
  27. uses: ./.github/workflows/build-exe-for-python-sdk.yml
  28. with:
  29. targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64
  30. release: true
  31. python-compat:
  32. name: Python ${{ matrix.python }} / installed SDK
  33. needs: build
  34. runs-on: ubuntu-latest
  35. timeout-minutes: 10
  36. strategy:
  37. fail-fast: false
  38. matrix:
  39. python: ['3.10', '3.14']
  40. steps:
  41. - uses: actions/checkout@v6
  42. with:
  43. persist-credentials: false
  44. - uses: actions/setup-python@v6.3.0
  45. with:
  46. python-version: ${{ matrix.python }}
  47. - uses: actions/download-artifact@v8
  48. with:
  49. pattern: deepseek_harness_*
  50. path: dist
  51. merge-multiple: true
  52. - name: Resolve installed wheel version
  53. id: compatibility-version
  54. run: |
  55. python - <<'PY' >> "$GITHUB_OUTPUT"
  56. import runpy
  57. release = runpy.run_path("scripts/build-python-release.py")
  58. repository_version = release["repository_version"]()
  59. print(f"version={release['pep440_version'](repository_version)}")
  60. PY
  61. - name: Install local release wheels and run the public entry path
  62. env:
  63. VERSION: ${{ steps.compatibility-version.outputs.version }}
  64. run: |
  65. python -m pip install \
  66. "dist/deepseek_harness_sdk-$VERSION-py3-none-any.whl" \
  67. "dist/deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl"
  68. python scripts/smoke-python-runtime.py --scenario sdk-default
  69. python scripts/smoke-python-runtime.py --scenario sdk-mcp
  70. validate:
  71. name: Validate release candidate
  72. needs: [build, python-compat]
  73. runs-on: ubuntu-latest
  74. timeout-minutes: 10
  75. outputs:
  76. version: ${{ steps.version.outputs.version }}
  77. steps:
  78. - uses: actions/checkout@v6
  79. with:
  80. persist-credentials: false
  81. - uses: actions/setup-python@v6.3.0
  82. with:
  83. python-version: '3.10'
  84. - name: Resolve release version
  85. id: version
  86. run: |
  87. python3 - <<'PY' >> "$GITHUB_OUTPUT"
  88. import runpy
  89. release = runpy.run_path("scripts/build-python-release.py")
  90. repository_version = release["repository_version"]()
  91. wheel_version = release["pep440_version"](repository_version)
  92. print(f"repository-version={repository_version}")
  93. print(f"version={wheel_version}")
  94. PY
  95. - name: Authorize publication request
  96. env:
  97. PUBLISH: ${{ github.event_name == 'workflow_dispatch' && inputs.publish }}
  98. PUBLIC_PYPI_RELEASE_ENABLED: ${{ vars.PUBLIC_PYPI_RELEASE_ENABLED }}
  99. PYPI_PUBLISHER_REPOSITORY: ${{ vars.PYPI_PUBLISHER_REPOSITORY }}
  100. REPOSITORY: ${{ github.repository }}
  101. REF_NAME: ${{ github.ref_name }}
  102. REF_TYPE: ${{ github.ref_type }}
  103. REPOSITORY_VERSION: ${{ steps.version.outputs.repository-version }}
  104. run: |
  105. set -euo pipefail
  106. if [ "$PUBLISH" = true ]; then
  107. [ -n "$PYPI_PUBLISHER_REPOSITORY" ] || {
  108. echo "::error::Set the repository variable PYPI_PUBLISHER_REPOSITORY before publication."
  109. exit 1
  110. }
  111. [ "$REPOSITORY" = "$PYPI_PUBLISHER_REPOSITORY" ] || {
  112. echo "::error::This repository is not the configured PyPI publisher repository."
  113. exit 1
  114. }
  115. [ "$PUBLIC_PYPI_RELEASE_ENABLED" = true ] || {
  116. echo "::error::Set PUBLIC_PYPI_RELEASE_ENABLED=true before public publication."
  117. exit 1
  118. }
  119. [ "$REF_TYPE" = tag ] && [ "$REF_NAME" = "python-v$REPOSITORY_VERSION" ] || {
  120. echo "::error::Publication must run from tag python-v$REPOSITORY_VERSION."
  121. exit 1
  122. }
  123. fi
  124. - uses: actions/download-artifact@v8
  125. with:
  126. pattern: deepseek_harness_*
  127. path: dist
  128. merge-multiple: true
  129. - name: Check release contents
  130. env:
  131. VERSION: ${{ steps.version.outputs.version }}
  132. run: |
  133. set -euo pipefail
  134. expected="$(mktemp)"
  135. actual="$(mktemp)"
  136. printf '%s\n' \
  137. "deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl" \
  138. "deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl" \
  139. "deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl" \
  140. "deepseek_harness_sdk-$VERSION-py3-none-any.whl" > "$expected"
  141. find dist -maxdepth 1 -type f -name '*.whl' -exec basename {} \; | sort > "$actual"
  142. diff -u "$expected" "$actual"
  143. while IFS= read -r wheel; do
  144. size="$(stat -c '%s' "dist/$wheel")"
  145. [ "$size" -lt 100000000 ] || {
  146. echo "::error::$wheel is $size bytes; public PyPI accepts at most 100000000 bytes by default."
  147. exit 1
  148. }
  149. done < "$actual"
  150. - name: Validate package metadata
  151. run: |
  152. python -m pip install twine==6.2.0
  153. python -m twine check dist/*.whl
  154. - name: Record artifact hashes
  155. run: |
  156. cd dist
  157. sha256sum *.whl | sort -k2 > SHA256SUMS
  158. cat SHA256SUMS
  159. - uses: actions/upload-artifact@v7
  160. with:
  161. name: python-release-${{ steps.version.outputs.version }}
  162. path: dist/*
  163. if-no-files-found: error
  164. retention-days: 7
  165. publish-runtime:
  166. name: Publish runtime wheels to public PyPI
  167. if: github.event_name == 'workflow_dispatch' && inputs.publish
  168. needs: validate
  169. runs-on: ubuntu-latest
  170. timeout-minutes: 10
  171. environment: pypi-runtime
  172. permissions:
  173. contents: read
  174. id-token: write
  175. steps:
  176. - uses: actions/download-artifact@v8
  177. with:
  178. name: python-release-${{ needs.validate.outputs.version }}
  179. path: dist
  180. - name: Verify release artifact hashes
  181. run: cd dist && sha256sum -c SHA256SUMS
  182. - name: Select runtime wheels
  183. run: |
  184. mkdir -p dist/runtime
  185. mv dist/deepseek_harness_runtime_bin-*.whl dist/runtime/
  186. - name: Publish runtime wheels
  187. uses: pypa/gh-action-pypi-publish@release/v1
  188. with:
  189. packages-dir: dist/runtime/
  190. # Public attestations reveal the private publisher repository. OIDC
  191. # authentication remains enabled without uploading that provenance.
  192. attestations: false
  193. # Keep the SDK in a dependent job. If its upload fails after the immutable
  194. # runtime files arrive, "re-run failed jobs" resumes here without attempting
  195. # to overwrite the runtime release.
  196. publish-sdk:
  197. name: Publish SDK wheel to public PyPI
  198. if: github.event_name == 'workflow_dispatch' && inputs.publish
  199. needs: [validate, publish-runtime]
  200. runs-on: ubuntu-latest
  201. timeout-minutes: 10
  202. environment: pypi
  203. permissions:
  204. contents: read
  205. id-token: write
  206. steps:
  207. - uses: actions/download-artifact@v8
  208. with:
  209. name: python-release-${{ needs.validate.outputs.version }}
  210. path: dist
  211. - name: Verify release artifact hashes
  212. run: cd dist && sha256sum -c SHA256SUMS
  213. - name: Select SDK wheel
  214. run: |
  215. mkdir -p dist/sdk
  216. mv dist/deepseek_harness_sdk-*.whl dist/sdk/
  217. - name: Publish SDK wheel
  218. uses: pypa/gh-action-pypi-publish@release/v1
  219. with:
  220. packages-dir: dist/sdk/
  221. attestations: false