python-release.yml 8.5 KB

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