From fe3777cf27ecc0bfe2ec7cad89e06d53bfaa2d5c Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 13 Jul 2026 17:49:01 +0800 Subject: [PATCH] python: derive release version from repository --- .../workflows/build-exe-for-python-sdk.yml | 40 +++++++++++++------ .gitlab-ci.yml | 17 ++++---- docs/cookbook/adding-a-package.md | 2 +- ...cutable-sdk-runtime-distribution.i18n.yaml | 4 +- ...ile-executable-sdk-runtime-distribution.md | 4 +- ...-executable-sdk-runtime-distribution.zh.md | 4 +- .../process/2026-06-16-pnpm-over-yarn.md | 2 +- python/README.i18n.yaml | 4 +- python/README.md | 11 ++--- python/README.zh.md | 11 ++--- python/sdk-runtime/README.i18n.yaml | 4 +- python/sdk-runtime/README.md | 2 +- python/sdk-runtime/README.zh.md | 2 +- python/sdk/tests/test_release_version.py | 39 ++++++++++++++++++ scripts/build-python-release.py | 38 ++++++++++++++---- scripts/check-workspace-constraints.ts | 20 ++++++++-- 16 files changed, 149 insertions(+), 55 deletions(-) create mode 100644 python/sdk/tests/test_release_version.py diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index 957c11d963..56099cb137 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -61,7 +61,21 @@ jobs: timeout-minutes: 5 outputs: matrix: ${{ steps.plan.outputs.matrix }} + version: ${{ steps.version.outputs.version }} steps: + - uses: actions/checkout@v6 + + - name: Resolve repository version + id: version + run: | + set -euo pipefail + version="$(jq -r '.version // empty' package.json)" + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "::error::package.json version must be stable X.Y.Z, got '$version'" + exit 1 + } + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Compute matrix from targets input id: plan env: @@ -99,7 +113,7 @@ jobs: sdk-wheel: needs: plan - name: deepseek_harness-0.0.0-py3-none-any.whl + name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -116,13 +130,12 @@ jobs: run: >- python scripts/build-python-release.py --package sdk - --tag python-v0.0.0 --output-dir dist-python - uses: actions/upload-artifact@v6 with: - name: deepseek_harness-0.0.0-py3-none-any.whl - path: dist-python/deepseek_harness-0.0.0-py3-none-any.whl + name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl + path: dist-python/deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl if-no-files-found: error build: @@ -189,15 +202,16 @@ jobs: id: runtime env: TARGET: ${{ matrix.target }} + VERSION: ${{ needs.plan.outputs.version }} run: | set -euo pipefail platform="${TARGET#node24-}" exe="$PWD/dist-exe/dsh-jsonrpc-agent-pkg-$platform" [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; } case "$platform" in - linux-x64) wheel=deepseek_harness_runtime_bin-0.0.0-py3-none-manylinux_2_28_x86_64.whl ;; - linux-arm64) wheel=deepseek_harness_runtime_bin-0.0.0-py3-none-manylinux_2_28_aarch64.whl ;; - macos-arm64) wheel=deepseek_harness_runtime_bin-0.0.0-py3-none-macosx_11_0_arm64.whl ;; + linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;; + linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;; + macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_11_0_arm64.whl ;; *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;; esac echo "platform=$platform" >> "$GITHUB_OUTPUT" @@ -215,23 +229,24 @@ jobs: run: >- python scripts/build-python-release.py --package runtime - --tag python-v0.0.0 --platform "${{ steps.runtime.outputs.platform }}" --runtime-exe "${{ steps.runtime.outputs.exe }}" --output-dir dist-python - uses: actions/download-artifact@v8 with: - name: deepseek_harness-0.0.0-py3-none-any.whl + name: deepseek_harness-${{ needs.plan.outputs.version }}-py3-none-any.whl path: dist-python - name: Install only the SDK into a clean venv and run zero-config + env: + VERSION: ${{ needs.plan.outputs.version }} run: | set -euo pipefail python -m venv "$RUNNER_TEMP/dsh-sdk-smoke" "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \ --find-links dist-python \ - deepseek-harness==0.0.0 + deepseek-harness=="$VERSION" "$RUNNER_TEMP/dsh-sdk-smoke/bin/python" scripts/smoke-python-runtime.py \ --scenario sdk-default @@ -251,6 +266,7 @@ jobs: if: runner.os == 'Linux' env: RUNNER_ARCH: ${{ runner.arch }} + VERSION: ${{ needs.plan.outputs.version }} run: | set -euo pipefail case "$RUNNER_ARCH" in @@ -258,9 +274,9 @@ jobs: ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;; *) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;; esac - docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c ' + docker run --rm -e VERSION -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c ' /opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk - /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness==0.0.0 + /tmp/dsh-sdk/bin/python -m pip install --find-links /work/dist-python deepseek-harness=="$VERSION" /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default ' diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 784de70e9c..b4af9e3ecb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,8 @@ variables: before_script: - python3 -m venv .ci-python - . .ci-python/bin/activate + - export DSH_VERSION="$(python -c 'import json; print(json.load(open("package.json"))["version"])')" + - test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; } - python -m pip install uv==0.11.23 sdk-wheel: @@ -40,7 +42,7 @@ sdk-wheel: - uv run --python 3.10 --group test --project python/sdk python scripts/smoke-python-runtime.py --scenario all --exe "$EXE" - python scripts/build-python-release.py --package runtime --tag "$CI_COMMIT_TAG" --platform "$PLATFORM" --runtime-exe "$EXE" --output-dir "release/$PLATFORM" - python -m venv .wheel-smoke - - .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness=="${CI_COMMIT_TAG#python-v}" + - .wheel-smoke/bin/python -m pip install --find-links "release/$PLATFORM" --find-links release/sdk deepseek-harness=="$DSH_VERSION" - .wheel-smoke/bin/python scripts/smoke-python-runtime.py --scenario sdk-default - | if [ "${PLATFORM#linux-}" != "$PLATFORM" ]; then @@ -53,7 +55,7 @@ sdk-wheel: linux-arm64) image=quay.io/pypa/manylinux_2_28_aarch64 ;; *) echo "Unsupported Linux platform $PLATFORM"; exit 1 ;; esac - docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness==${CI_COMMIT_TAG#python-v} && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default" + docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness==$DSH_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default" fi artifacts: paths: [release/$PLATFORM/*.whl] @@ -105,14 +107,15 @@ publish-python: before_script: - python3 -m venv .ci-python - . .ci-python/bin/activate + - export DSH_VERSION="$(python -c 'import json; print(json.load(open("package.json"))["version"])')" + - test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; } - python -m pip install twine==6.2.0 script: - - version="${CI_COMMIT_TAG#python-v}" - test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 4 - - test -f "release/sdk/deepseek_harness-${version}-py3-none-any.whl" - - test -f "release/linux-x64/deepseek_harness_runtime_bin-${version}-py3-none-manylinux_2_28_x86_64.whl" - - test -f "release/linux-arm64/deepseek_harness_runtime_bin-${version}-py3-none-manylinux_2_28_aarch64.whl" - - test -f "release/macos-arm64/deepseek_harness_runtime_bin-${version}-py3-none-macosx_11_0_arm64.whl" + - test -f "release/sdk/deepseek_harness-${DSH_VERSION}-py3-none-any.whl" + - test -f "release/linux-x64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_x86_64.whl" + - test -f "release/linux-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-manylinux_2_28_aarch64.whl" + - test -f "release/macos-arm64/deepseek_harness_runtime_bin-${DSH_VERSION}-py3-none-macosx_11_0_arm64.whl" - python -m twine check release/*/*.whl - export TWINE_USERNAME=gitlab-ci-token - export TWINE_PASSWORD="$CI_JOB_TOKEN" diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 1ab6931696..852d403e2b 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -18,7 +18,7 @@ packages/// Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it. -package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, `version: 0.0.1`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`. +package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`. In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files. diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index 891792c7dc..f53d8a5d3b 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-10-single-file-executable-sdk-runtime-distribution.md: 474a7af7f2cfe9177dec779e3eb50fd6034e806d -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 1f436d63fc91420c6da6d92e32bd16d5936ced5d +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 08fd6f530d1f564ca169a168c0f7d77bff6bf29f +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 050e4f9a6e8bb03e59835f2c06dd3c701aa34eed diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index 474a7af7f2..08fd6f530d 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -42,13 +42,13 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru [`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg--` land in `dist-exe/` and are copied back into the runtime directory. CI treats them as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources. -CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. The run retains only four artifacts, each containing one release file: the platform-independent SDK wheel and the three native runtime wheels; bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal. +CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. The run retains only four artifacts, each containing one release file: the platform-independent SDK wheel and the three native runtime wheels; bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal. ### Python SDK distribution: two carriers, exe for production, node for development The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds three kinds of content: the checked-in default `runtime/cordis.yml`, the build-injected platform exe, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions. -[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) accepts only stable `python-vX.Y.Z` tags and stages both packages at `X.Y.Z`, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. The SDK is a `py3-none-any` wheel; the wheel-only runtime package contains exactly one exe and uses one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`. Its Hatch hook rejects sdists, universal tags, mixed executable payloads, and unsupported platforms. +[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; the wheel-only runtime package contains exactly one exe and uses one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`. Its Hatch hook rejects sdists, universal tags, mixed executable payloads, and unsupported platforms. The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-jsonrpc` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`. diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index 1f436d63fc..050e4f9a6e 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -42,13 +42,13 @@ deploy root 是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runti [`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):runtime 闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直落** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`→ 注入 pkg 配置(`bin` 指闭包内 `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`,`assets` 全量 glob——动态 import 对 pkg 静态分析不可见,必须显式全量打入)→ 每 target 一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg--` 落 `dist-exe/` 并拷回 runtime 目录。CI 把它们作为测试中间输入,只保留对应的平台 wheel。deploy 四 flag 均有实测依据:`--legacy` 是未开 inject-workspace-packages 时的必选路径;hoisted 产出零符号链接文件树(pkg VFS 最稳、物理保证 cordis 单实例);关 peer 自动安装避免未发布包名触发 registry 解析;link-workspace-packages 让闭包指向 workspace/vendor 源。 -CI:[`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),仅显式触发——`workflow_dispatch` 手动派发,或给 PR 打 `build-exe` 标签;linux-x64 / linux-arm64(`ubuntu-24.04-arm`)/ macos-arm64 三平台原生构建,并缓存 `~/.pkg-cache`;macOS ad-hoc 签名由 pkg 处理。每个平台都以 mock SSE 模型分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再以 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应,最后把 release 形态的 wheel 安装到干净 venv 中并在不传 `runtime_bin` 的情况下运行;Linux 还检查 GLIBC 依赖并在 manylinux 2.28 容器中运行。整次运行只保留 4 个产物,每个只含一个发布文件:平台无关的 SDK wheel 与 3 个原生 runtime wheel;裸 exe 和源码 bundle 只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受 `python-vX.Y.Z` tag 流水线,构建一个 SDK wheel 与 3 个原生 runtime wheel,再由单个串行 job 校验并发布这 4 个文件到项目 PyPI 注册表。Windows 是非目标。 +CI:[`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),仅显式触发——`workflow_dispatch` 手动派发,或给 PR 打 `build-exe` 标签;linux-x64 / linux-arm64(`ubuntu-24.04-arm`)/ macos-arm64 三平台原生构建,并缓存 `~/.pkg-cache`;macOS ad-hoc 签名由 pkg 处理。每个平台都以 mock SSE 模型分别通过默认配置和自定义 `cordis.yml` 驱动 SDK,再以 NDJSON JSON-RPC 直接驱动 exe,校验 JSONL 与最终响应,最后把 release 形态的 wheel 安装到干净 venv 中并在不传 `runtime_bin` 的情况下运行;Linux 还检查 GLIBC 依赖并在 manylinux 2.28 容器中运行。整次运行只保留 4 个产物,每个只含一个发布文件:平台无关的 SDK wheel 与 3 个原生 runtime wheel;裸 exe 和源码 bundle 只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` tag 流水线,构建一个 SDK wheel 与 3 个原生 runtime wheel,再由单个串行 job 校验并发布这 4 个文件到项目 PyPI 注册表。Windows 是非目标。 ### Python SDK 分发:双载体,exe 为生产、node 为开发 Python SDK 位于 [`python/`](../../../../python/README.md):`python/sdk`(客户端)+ `python/sdk-runtime`(运行时载体包)。runtime 包数据目录三类内容:检入的默认 `runtime/cordis.yml`、构建注入的平台 exe、构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 自动解析**只找 exe**;node 载体仅显式 `DSH_RUNTIME_MODE=node` 启用(跑 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`,需系统 node ≥22.19),定位是本仓库成员的开发验证通道,不进 wheel 分发物。 -[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 只接受稳定的 `python-vX.Y.Z` tag,并以 `X.Y.Z` 暂存两个包,同时让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。SDK 是 `py3-none-any` wheel;只提供 wheel 的 runtime 包恰好包含一个 exe,tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64`。其 Hatch 钩子拒绝 sdist、通用 tag、混合可执行载荷以及不支持的平台。 +[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录 `package.json` 读取权威的稳定 `X.Y.Z`,并以该版本暂存两个包,同时让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布 tag 只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel;只提供 wheel 的 runtime 包恰好包含一个 exe,tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 或 `py3-none-macosx_11_0_arm64`。其 Hatch 钩子拒绝 sdist、通用 tag、混合可执行载荷以及不支持的平台。 exe「必须显式配置」的硬语义不变;零配置体验由 wrapper 恢复:调用方没给 `cordis`、没显式指定 runtime、环境无 `DSH_CORDIS_CONFIG` 时,客户端把检入的默认 `cordis.yml`(agent-core + 预载 llm-deepseek + JSONL 持久化 + bash-local + `dsh-jsonrpc` serving 条目,`!!js` 环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`。 diff --git a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md b/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md index 6ea62b2c4e..7a574a3388 100644 --- a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md +++ b/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md @@ -15,7 +15,7 @@ Adopt **pnpm 11.7.0**, pinned via the `packageManager` field and installed throu - **Workspaces** move from the `package.json` `workspaces` array + `.yarnrc.yml` to `pnpm-workspace.yaml` (`vendor/*`, `packages/*` — the same globs; `examples/*` stay non-workspace, matching the prior setup and tsdown's explicit globs). - **Strict symlinked linker** (pnpm's default) replaces Yarn's hoisted `node-modules` linker. We deliberately add **no** `node-linker=hoisted` / `shamefully-hoist` escape hatch: pnpm's non-flat `node_modules` makes phantom dependencies (importing an undeclared transitive dep) fail loudly, which is a *feature* for a repo whose whole quality story is mechanical gates ([mechanical quality gates](2026-06-11-quality-gates.md)). The gate suite — typecheck, lint, test, build, knip — is the safety net that proves no such phantom imports exist. - **Build-script allowlist.** pnpm 10+ does not run dependency lifecycle scripts unless allowlisted. `pnpm-workspace.yaml` carries an explicit `allowBuilds` map (`esbuild`, `lefthook`, `@google/genai`, `protobufjs`) — the same supply-chain-hardening posture the repo already takes toward model/tool output, now applied to install-time code execution. `peerDependencyRules.allowedVersions.typescript: '>=5 <7'` silences benign peer-range warnings for the in-repo TypeScript. -- **Constraints become package-manager-independent.** `yarn.config.cjs` (which imported `@yarnpkg/types` and used `Yarn.workspaces()` / `workspace.set()`) is replaced by `scripts/check-workspace-constraints.ts`, a plain tsx script run as `pnpm run constraints`. It enforces the identical invariants — every package `private: true`; `@deepseek-ai/dsh-*` packages declare `cordis` as both a peer- and dev-dependency with matching ranges, `version: 0.0.1`, `type: module`; vendored packages checked for privacy only — over the same `vendor` + `packages` scope. +- **Constraints become package-manager-independent.** `yarn.config.cjs` (which imported `@yarnpkg/types` and used `Yarn.workspaces()` / `workspace.set()`) is replaced by `scripts/check-workspace-constraints.ts`, a plain tsx script run as `pnpm run constraints`. It enforces the identical invariants — every package `private: true`; `@deepseek-ai/dsh-*` packages declare `cordis` as both a peer- and dev-dependency with matching ranges, use the root `package.json` version, and set `type: module`; vendored packages checked for privacy only — over the same `vendor` + `packages` scope. - All `yarn …` verbs across CI, lefthook hooks, `package.json` scripts, and docs become `pnpm …` / `pnpm run …`. `yarn.lock` → `pnpm-lock.yaml` (lockfile v9). `.gitignore` swaps `.yarn/` for `.pnpm-store/`. Vendored READMEs (e.g. `vendor/cordis/README.md`) keep their upstream `yarn` examples untouched per the Vendoring Policy. ## Alternatives considered diff --git a/python/README.i18n.yaml b/python/README.i18n.yaml index ec8976bf07..1ea214996e 100644 --- a/python/README.i18n.yaml +++ b/python/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -README.md: 2e2e3bff7e6903a1183cc445f590ecb327b92e65 -README.zh.md: f14dad4688ed3c54f46ae28475779b0a8d163ae0 +README.md: d9403733b7daff9b3e8dbf21e1bc0c68d61b88a7 +README.zh.md: 28859e43b7332a8ef4c3cf199b97860447a011a8 diff --git a/python/README.md b/python/README.md index 2e2e3bff7e..d9403733b7 100644 --- a/python/README.md +++ b/python/README.md @@ -49,15 +49,16 @@ Two flavors, both for repo members: ## Distributing the Python packages -Python releases use stable tags of the form `python-vX.Y.Z`. The common staging script derives both distribution versions from the tag, pins `deepseek-harness-runtime-bin==X.Y.Z` in the SDK metadata, and rejects any other tag form. Build the pure SDK wheel once and one runtime wheel on each native platform: +The root [`package.json`](../package.json) version is authoritative for both Python distributions. The common staging script reads that version, injects it into both wheels, and pins the SDK metadata to the same `deepseek-harness-runtime-bin==X.Y.Z`; an optional `python-vX.Y.Z` release tag is accepted only when it matches the repository version. Build the pure SDK wheel once and one runtime wheel on each native platform: ```sh -python scripts/build-python-release.py --package sdk --tag python-v0.1.0 --output-dir dist-python -python scripts/build-python-release.py --package runtime --tag python-v0.1.0 --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python -pip install --find-links dist-python deepseek-harness==0.1.0 +version="$(node -p "require('./package.json').version")" +python scripts/build-python-release.py --package sdk --output-dir dist-python +python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python +pip install --find-links dist-python deepseek-harness=="$version" ``` -The runtime distribution is wheel-only and rejects sdist builds, missing executables, and mixed-platform payloads. Its three wheel tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the SDK remains `py3-none-any`. A tag pipeline builds these four non-conflicting files and publishes them together, so a normal `pip install deepseek-harness==X.Y.Z` selects the matching runtime wheel and `import deepseek_harness` needs no `runtime_bin`. +The runtime distribution is wheel-only and rejects sdist builds, missing executables, and mixed-platform payloads. Its three wheel tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the SDK remains `py3-none-any`. A matching `python-vX.Y.Z` tag pipeline builds these four non-conflicting files and publishes them together, so a normal `pip install deepseek-harness==X.Y.Z` selects the matching runtime wheel and `import deepseek_harness` needs no `runtime_bin`. ## Zero-config semantics diff --git a/python/README.zh.md b/python/README.zh.md index f14dad4688..28859e43b7 100644 --- a/python/README.zh.md +++ b/python/README.zh.md @@ -49,15 +49,16 @@ print(DeepSeekHarness().run("say hi").final_response) # auto-resolution picks ## 分发 Python 包 -Python 发布只使用 `python-vX.Y.Z` 形式的稳定 tag。统一暂存脚本从 tag 推导两个分发物的版本,在 SDK 元数据中钉死 `deepseek-harness-runtime-bin==X.Y.Z`,并拒绝其他 tag 形式。纯 SDK wheel 只构建一次,runtime wheel 则在每个原生平台各构建一个: +根目录 [`package.json`](../package.json) 的版本是两个 Python 分发物的权威版本。统一暂存脚本读取这个版本并注入两个 wheel,同时在 SDK 元数据中钉死相同版本的 `deepseek-harness-runtime-bin==X.Y.Z`;可选的 `python-vX.Y.Z` 发布 tag 只有与仓库版本匹配时才会被接受。纯 SDK wheel 只构建一次,runtime wheel 则在每个原生平台各构建一个: ```sh -python scripts/build-python-release.py --package sdk --tag python-v0.1.0 --output-dir dist-python -python scripts/build-python-release.py --package runtime --tag python-v0.1.0 --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python -pip install --find-links dist-python deepseek-harness==0.1.0 +version="$(node -p "require('./package.json').version")" +python scripts/build-python-release.py --package sdk --output-dir dist-python +python scripts/build-python-release.py --package runtime --platform macos-arm64 --runtime-exe dist-exe/dsh-jsonrpc-agent-pkg-macos-arm64 --output-dir dist-python +pip install --find-links dist-python deepseek-harness=="$version" ``` -runtime 分发物只提供 wheel,并拒绝 sdist 构建、缺失可执行文件以及混合平台载荷。三个 wheel tag 分别是 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;SDK 保持 `py3-none-any`。tag 流水线统一构建并发布这 4 个互不冲突的文件,因此常规的 `pip install deepseek-harness==X.Y.Z` 会选中匹配平台的 runtime wheel,`import deepseek_harness` 不需要 `runtime_bin`。 +runtime 分发物只提供 wheel,并拒绝 sdist 构建、缺失可执行文件以及混合平台载荷。三个 wheel tag 分别是 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;SDK 保持 `py3-none-any`。匹配的 `python-vX.Y.Z` tag 流水线统一构建并发布这 4 个互不冲突的文件,因此常规的 `pip install deepseek-harness==X.Y.Z` 会选中匹配平台的 runtime wheel,`import deepseek_harness` 不需要 `runtime_bin`。 ## 零配置语义 diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index dfe047f3b4..542b26c719 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -README.md: 1a63f76f62b95cb36f53e3f8ff42898b26122d05 -README.zh.md: b214ed5e6d25a013def5429b92a14b7393f1e028 +README.md: 4beac57526761bb150e90b60f0030ed311c4034d +README.zh.md: 3c715bce17ed1630ef0439b042d2b67047968c55 diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 1a63f76f62..4beac57526 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -15,7 +15,7 @@ Both carriers hold the same content, defined once: the [package.json](package.js Missing carriers raise `FileNotFoundError` naming the acquisition routes: build via `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness checkout, or install the matching platform runtime wheel produced by the `build-exe-for-python-sdk` CI workflow. The workflow retains wheels rather than standalone executable archives. Acquisition strategy is deliberately separate from the lookup interface, so an on-demand download can replace it later without touching callers. -Each wheel contains exactly one executable. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the build hook rejects `py3-none-any`, absent or multiple executables, and unsupported platform tags. Stable `python-vX.Y.Z` releases use the same `X.Y.Z` for this package and the SDK. +Each wheel contains exactly one executable. The fixed tags are `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, and `py3-none-macosx_11_0_arm64`; the build hook rejects `py3-none-any`, absent or multiple executables, and unsupported platform tags. The repository root `package.json` supplies the shared version for this package and the SDK, and a `python-vX.Y.Z` release tag must match it. ## Resolution API diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index b214ed5e6d..3c715bce17 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -15,7 +15,7 @@ Python SDK 的运行时载体包(dist 名 `deepseek-harness-runtime-bin`,模 载体缺失时抛出 `FileNotFoundError` 并写明获取途径:在 deepseek-harness 检出中经 `scripts/build-exe-for-python-sdk.ts` 构建,或安装 `build-exe-for-python-sdk` CI 工作流生成的对应平台 runtime wheel。该工作流只保留 wheel,不保留独立 exe 归档。获取策略与查找接口刻意分离,之后可以换成按需下载而不动任何调用方。 -每个 wheel 只包含一个可执行文件。固定 tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;构建钩子会拒绝 `py3-none-any`、可执行文件缺失或重复以及不支持的平台 tag。稳定的 `python-vX.Y.Z` 发布为本包和 SDK 使用同一个 `X.Y.Z`。 +每个 wheel 只包含一个可执行文件。固定 tag 为 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64` 与 `py3-none-macosx_11_0_arm64`;构建钩子会拒绝 `py3-none-any`、可执行文件缺失或重复以及不支持的平台 tag。仓库根目录的 `package.json` 为本包和 SDK 提供共同版本,`python-vX.Y.Z` 发布 tag 必须与其匹配。 ## 解析 API diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py new file mode 100644 index 0000000000..38cb40862d --- /dev/null +++ b/python/sdk/tests/test_release_version.py @@ -0,0 +1,39 @@ +"""Tests for repository-owned Python release versions.""" + +from __future__ import annotations + +import json +import runpy +from pathlib import Path +from types import SimpleNamespace + +import pytest + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT = ROOT / "scripts" / "build-python-release.py" +build_python_release = SimpleNamespace(**runpy.run_path(str(SCRIPT))) + + +def test_repository_version_matches_root_package_json() -> None: + expected = json.loads((ROOT / "package.json").read_text())["version"] + + assert build_python_release.repository_version() == expected + + +def test_release_tag_is_optional_for_non_release_builds() -> None: + build_python_release.validate_release_tag(None, "1.2.3") + + +def test_release_tag_must_match_repository_version() -> None: + build_python_release.validate_release_tag("python-v1.2.3", "1.2.3") + + with pytest.raises(ValueError, match="expected 'python-v1.2.3'"): + build_python_release.validate_release_tag("python-v1.2.4", "1.2.3") + + +def test_repository_version_rejects_non_stable_versions(tmp_path: Path) -> None: + (tmp_path / "package.json").write_text('{"version":"1.2.3-dev"}\n') + + with pytest.raises(ValueError, match="must be stable X.Y.Z"): + build_python_release.repository_version(tmp_path) diff --git a/scripts/build-python-release.py b/scripts/build-python-release.py index a196973a53..e0e90aa818 100644 --- a/scripts/build-python-release.py +++ b/scripts/build-python-release.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -"""Stage and build one Python release wheel from a stable ``python-vX.Y.Z`` tag.""" +"""Stage and build one Python wheel at the repository version.""" from __future__ import annotations import argparse import email +import json import os import re import shutil @@ -26,12 +27,16 @@ PLATFORMS = { def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--package", choices=("sdk", "runtime"), required=True) - parser.add_argument("--tag", required=True) + parser.add_argument( + "--tag", + help="optional python-vX.Y.Z release tag; it must match package.json", + ) parser.add_argument("--output-dir", type=Path, required=True) parser.add_argument("--platform", choices=tuple(PLATFORMS)) parser.add_argument("--runtime-exe", type=Path) args = parser.parse_args() - version = version_from_tag(args.tag) + version = repository_version() + validate_release_tag(args.tag, version) if args.package == "runtime" and (args.platform is None or args.runtime_exe is None): parser.error("runtime builds require --platform and --runtime-exe") if args.package == "sdk" and (args.platform is not None or args.runtime_exe is not None): @@ -58,11 +63,28 @@ def main() -> None: print(expected) -def version_from_tag(tag: str) -> str: - match = re.fullmatch(r"python-v(\d+\.\d+\.\d+)", tag) - if match is None: - raise ValueError(f"release tag must match python-vX.Y.Z, got {tag!r}") - return match.group(1) +def repository_version(root: Path = ROOT) -> str: + package_json = root / "package.json" + try: + payload = json.loads(package_json.read_text()) + except (OSError, json.JSONDecodeError) as error: + raise ValueError(f"could not read repository version from {package_json}") from error + version = payload.get("version") if isinstance(payload, dict) else None + if not isinstance(version, str) or re.fullmatch(r"\d+\.\d+\.\d+", version) is None: + raise ValueError( + f"{package_json} version must be stable X.Y.Z, got {version!r}" + ) + return version + + +def validate_release_tag(tag: str | None, version: str) -> None: + if tag is None: + return + expected = f"python-v{version}" + if tag != expected: + raise ValueError( + f"release tag must match repository version: expected {expected!r}, got {tag!r}" + ) def copy_package(source: Path, destination: Path) -> None: diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 2d12a66d8f..d7d52aa421 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -61,6 +61,9 @@ function readJson(path: string): PackageManifest { return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest } +const rootManifest = readJson(join(root, 'package.json')) +const repositoryVersion = rootManifest.version + /** Repo-relative dirs holding a package.json, walked to the configured depth. */ function packageDirs(base: string, depth: number): string[] { if (depth === 1) { @@ -78,7 +81,7 @@ function packageDirs(base: string, depth: number): string[] { function workspaceManifests(): WorkspaceManifest[] { const manifests: WorkspaceManifest[] = [ - { dir: '.', manifest: readJson(join(root, 'package.json')) }, + { dir: '.', manifest: rootManifest }, ] for (const { dir: base, depth } of workspaceGlobs) { @@ -147,8 +150,8 @@ function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] { if (peer && dev && peer !== dev) { errors.push(`${label}: cordis peer (${peer}) and dev (${dev}) ranges must match`) } - if (manifest.version !== '0.0.1') { - errors.push(`${label}: package.json must set "version": "0.0.1"`) + if (manifest.version !== repositoryVersion) { + errors.push(`${label}: package.json version must match root version ${repositoryVersion ?? '(missing)'}`) } if (manifest.type !== 'module') { errors.push(`${label}: package.json must set "type": "module"`) @@ -205,7 +208,16 @@ function checkHierarchyShape(): string[] { return errors } -const errors = [...workspaceManifests().flatMap(checkWorkspace), ...checkHierarchyShape()] +function checkRepositoryVersion(): string[] { + if (repositoryVersion && /^\d+\.\d+\.\d+$/.test(repositoryVersion)) return [] + return ['package.json: version must be stable X.Y.Z'] +} + +const errors = [ + ...checkRepositoryVersion(), + ...workspaceManifests().flatMap(checkWorkspace), + ...checkHierarchyShape(), +] if (errors.length > 0) { console.error(errors.join('\n')) process.exitCode = 1