Files
deepseek-harness/.github/workflows/build-exe-for-python-sdk.yml
T
Workflow config file is invalid. Please check your config file: getMatrixes: matrix include must be a list of mappings
Tianyi Cui 3c1c6a89b1 test(python): gate installed runtime wheels across release targets (#2953)
* test(python): exercise installed wheels as black boxes

Add an installed-wheel mode that refuses source/editable imports, repository working directories, mismatched SDK/runtime versions, unpinned runtime dependencies, and executables outside the installed runtime distribution. The mode resolves the wheel-owned executable itself, so callers cannot accidentally prove an explicit checkout artifact.

Add a real-API scenario that drives two tool-using turns through the public synchronous SDK, verifies the file bytes outside the agent, checks completed turn/tool events and persistence, and projects provider failures without retaining credential-bearing error text. The existing deterministic scenario set remains the keyless behavior oracle.

Refs #2952.

* ci(python): require installed-wheel checks on every release target

Move the complete deterministic runtime scenarios behind construction and clean installation of the SDK and matching runtime wheels. Each native leg runs outside the checkout with source-resolution environment variables removed; Linux manylinux smokes assert the same installed provenance.

Expand the required pull-request call from Linux x64 to Linux x64, Linux arm64, and macOS arm64. Trusted heads receive only DEEPSEEK_API_KEY_EXTERNAL for a fail-loud live two-turn smoke on each carrier, while fork and Dependabot heads retain the full keyless path without exposing secrets.

Pin the reusable secret declaration, matrix call, aggregate dependency, untrusted-head condition, and live/keyless commands in the workflow contract test.

Refs #2952.

* docs(testing): make installed wheels the Python CI authority

Record the clean-wheel provenance boundary, complete keyless scenario set, trusted real-API contract, secret handling, and three-target required topology in a new implemented testing decision. Update the SEA distribution and portable-CI authorities plus the Python contributor reference to describe the same current state.

Archive the fully superseded Linux-x64-only decision after consolidating its rationale and alternatives into the new owner. Preserve its bilingual triplet as a sealed historical snapshot and redirect every active current-state reference.

Refs #2952.
2026-08-23 16:53:55 +08:00

369 lines
15 KiB
YAML

name: Build single-exe
# Native builds for the release targets; see
# .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
# A full target run retains one SDK wheel and three runtime wheels; subset
# dispatch retains the SDK wheel and selected runtime wheels. Bare executables
# and source closures are test inputs. Run manually, label a PR `build-exe`
# (remove and reapply to rerun), or call it from the Python release workflow.
# Checkout uses the triggering ref, so dispatch needs no separate ref input.
on:
workflow_call:
inputs:
targets:
description: Comma-separated pkg targets to build; empty builds all three.
type: string
required: false
default: ''
release:
description: Run as the native builder for the Python release workflow.
type: boolean
required: false
default: false
ci:
description: Run as the required all-target Python runtime pull-request check.
type: boolean
required: false
default: false
secrets:
DEEPSEEK_API_KEY_EXTERNAL:
description: Real DeepSeek API key for trusted installed-wheel pull-request tests.
required: false
workflow_dispatch:
inputs:
targets:
description: >-
Comma-separated pkg targets to build. Any subset of:
node24-linux-x64, node24-linux-arm64, node24-macos-arm64.
Empty builds all three.
type: string
required: false
default: ''
pull_request:
types: [labeled]
concurrency:
# Keep the called workflow distinct from its caller's concurrency group;
# github.workflow identifies the caller inside a reusable workflow and keeps
# an ordinary CI run from cancelling a full release validation on the same ref.
group: build-single-exe-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
# Job-level conditions cannot inspect `matrix`, so validate target names and
# construct the matrix before the dependent jobs.
plan:
name: plan targets
if: inputs.ci || inputs.release || github.event_name == 'workflow_dispatch' || github.event.label.name == 'build-exe'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
version: ${{ steps.version.outputs.version }}
repository-version: ${{ steps.version.outputs.repository-version }}
steps:
- uses: actions/checkout@v6
- name: Resolve repository version
id: version
run: |
set -euo pipefail
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import runpy
release = runpy.run_path("scripts/build-python-release.py")
repository_version = release["repository_version"]()
wheel_version = release["pep440_version"](repository_version)
print(f"repository-version={repository_version}")
print(f"version={wheel_version}")
PY
- name: Compute matrix from targets input
id: plan
env:
# Label runs and blank dispatch inputs build all targets.
TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64' }}
run: |
set -euo pipefail
matrix='[]'
IFS=',' read -r -a targets <<< "$TARGETS"
for raw in "${targets[@]}"; do
t="$(echo "$raw" | xargs)" # trim surrounding whitespace
[ -z "$t" ] && continue
# Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while
# macos-latest is Apple Silicon.
case "$t" in
node24-linux-x64) runner=ubuntu-latest ;;
node24-linux-arm64) runner=ubuntu-24.04-arm ;;
node24-macos-arm64) runner=macos-latest ;;
*)
echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64."
exit 1
;;
esac
matrix="$(jq -c --arg target "$t" --arg runner "$runner" '. + [{target: $target, runner: $runner}]' <<< "$matrix")"
done
if [ "$matrix" = '[]' ]; then
echo "::error::The targets input selected nothing to build."
exit 1
fi
echo "Matrix: $matrix"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
sdk-wheel:
needs: plan
name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6.3.0
with:
python-version: '3.10'
- name: Install Python build tooling
run: python -m pip install uv==0.11.23
- name: Build release-shaped SDK wheel
run: >-
python scripts/build-python-release.py
--package sdk
--output-dir dist-python
- uses: actions/upload-artifact@v7
with:
name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
path: dist-python/deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
if-no-files-found: error
retention-days: 7
build:
needs: [plan, sdk-wheel]
name: ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
# setup-node's built-in pnpm store cache keys on platform AND arch, so
# the Linux architectures sharing runner.os stay on separate caches.
- uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
- uses: actions/setup-python@v6.3.0
with:
python-version: '3.10'
- name: Install Python build tooling
run: python -m pip install uv==0.11.23
# Cache pkg's target Node binary; lockfile changes roll the
# exact key while the restore prefix can seed its replacement.
- uses: actions/cache@v4
with:
path: ~/.pkg-cache
key: pkg-fetch-${{ matrix.target }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pkg-fetch-${{ matrix.target }}-
- name: Install (immutable)
run: pnpm install --frozen-lockfile
- name: Rebuild Linux node-pty against manylinux 2.28
if: runner.os == 'Linux'
env:
RUNNER_ARCH: ${{ runner.arch }}
run: |
set -euo pipefail
case "$RUNNER_ARCH" in
X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
*) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
esac
addon_dir="$(realpath packages/subprocess/subprocess-local/node_modules/node-pty)"
(cd "$addon_dir" && npm_config_build_from_source=true pnpm run install)
addon="$addon_dir/build/Release/pty.node"
[ -f "$addon_dir/build/Makefile" ] || {
echo "::error::node-pty install did not generate $addon_dir/build/Makefile"
exit 1
}
docker run --rm \
--user "$(id -u):$(id -g)" \
-v "$PWD:$PWD" \
-v "$HOME/.cache/node-gyp:$HOME/.cache/node-gyp:ro" \
-v "$HOME/setup-pnpm:$HOME/setup-pnpm:ro" \
-w "$addon_dir" \
"$image" \
bash -euxo pipefail -c \
'rm -rf build/Release && make -C build -j2 BUILDTYPE=Release'
[ -f "$addon" ] || { echo "::error::$addon missing after manylinux rebuild"; exit 1; }
readelf --version-info "$addon" | tee node-pty-glibc-versions.txt
maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' node-pty-glibc-versions.txt | sort -V | tail -1)"
[ -n "$maximum" ] || { echo "::error::No GLIBC requirements found in $addon"; exit 1; }
dpkg --compare-versions "$maximum" le 2.28 || {
echo "::error::node-pty addon requires GLIBC_$maximum but wheel claims manylinux_2_28"
exit 1
}
- name: Build single-exe
env:
DSH_BUILD_CLIENT_PROFILE: official
run: pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=${{ matrix.target }}
- name: Resolve platform outputs
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-$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_14_0_arm64.whl ;;
*) echo "::error::Unsupported runtime platform $platform"; exit 1 ;;
esac
echo "platform=$platform" >> "$GITHUB_OUTPUT"
echo "exe=$exe" >> "$GITHUB_OUTPUT"
echo "wheel=$wheel" >> "$GITHUB_OUTPUT"
- name: Build release-shaped runtime wheel
run: >-
python scripts/build-python-release.py
--package runtime
--platform "${{ steps.runtime.outputs.platform }}"
--runtime-exe "${{ steps.runtime.outputs.exe }}"
--output-dir dist-python
- uses: actions/download-artifact@v8
with:
name: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
path: dist-python
- name: Install local SDK and runtime wheels into a clean venv
env:
RUNTIME_WHEEL: ${{ steps.runtime.outputs.wheel }}
SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
run: |
set -euo pipefail
python -m venv "$RUNNER_TEMP/dsh-sdk-smoke"
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" -m pip install \
"dist-python/$SDK_WHEEL" \
"dist-python/$RUNTIME_WHEEL"
- name: Run installed-wheel keyless black-box tests
run: |
set -euo pipefail
blackbox_root="$RUNNER_TEMP/dsh-sdk-blackbox"
mkdir -p "$blackbox_root"
cd "$blackbox_root"
env -u PYTHONPATH -u DSH_RUNTIME_MODE \
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" \
"$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
--scenario all \
--installed-wheel
- name: Preflight installed-wheel real API test
if: >-
inputs.ci
&& (github.event_name != 'pull_request'
|| !(github.event.pull_request.head.repo.fork
|| github.event.pull_request.user.login == 'dependabot[bot]'))
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
run: |
set -euo pipefail
if [ -z "${DEEPSEEK_API_KEY:-}" ]; then
echo "::error::DEEPSEEK_API_KEY_EXTERNAL is empty; the installed-wheel real API test cannot self-skip."
exit 1
fi
- name: Run installed-wheel real API black-box test
if: >-
inputs.ci
&& (github.event_name != 'pull_request'
|| !(github.event.pull_request.head.repo.fork
|| github.event.pull_request.user.login == 'dependabot[bot]'))
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }}
DEEPSEEK_BASE_URL: https://api.deepseek.com
run: |
set -euo pipefail
blackbox_root="$RUNNER_TEMP/dsh-sdk-blackbox-live"
mkdir -p "$blackbox_root"
cd "$blackbox_root"
env -u PYTHONPATH -u DSH_RUNTIME_MODE \
"$RUNNER_TEMP/dsh-sdk-smoke/bin/python" \
"$GITHUB_WORKSPACE/scripts/smoke-python-runtime.py" \
--scenario sdk-live \
--installed-wheel
- name: Check Linux GLIBC requirements
if: runner.os == 'Linux'
run: |
set -euo pipefail
readelf --version-info "${{ steps.runtime.outputs.exe }}" | tee glibc-versions.txt
maximum="$(sed -n 's/.*Name: GLIBC_\([0-9.]*\).*/\1/p' glibc-versions.txt | sort -V | tail -1)"
[ -n "$maximum" ] || { echo "::error::No GLIBC requirements found"; exit 1; }
dpkg --compare-versions "$maximum" le 2.28 || {
echo "::error::Executable requires GLIBC_$maximum but wheel claims manylinux_2_28"
exit 1
}
- name: Check macOS deployment target
if: runner.os == 'macOS'
env:
EXE: ${{ steps.runtime.outputs.exe }}
run: >-
python3 scripts/check-macos-deployment-target.py
"$EXE" "$EXE-spawn-helper"
- name: Run wheel in a manylinux 2.28 container
if: runner.os == 'Linux'
env:
RUNNER_ARCH: ${{ runner.arch }}
RUNTIME_WHEEL: ${{ steps.runtime.outputs.wheel }}
SDK_WHEEL: deepseek_harness_sdk-${{ needs.plan.outputs.version }}-py3-none-any.whl
run: |
set -euo pipefail
case "$RUNNER_ARCH" in
X64) image=quay.io/pypa/manylinux_2_28_x86_64 ;;
ARM64) image=quay.io/pypa/manylinux_2_28_aarch64 ;;
*) echo "::error::Unsupported Linux runner architecture $RUNNER_ARCH"; exit 1 ;;
esac
docker run --rm -e RUNTIME_WHEEL -e SDK_WHEEL -e DSH_TELEMETRY_DISABLED -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 "/work/dist-python/$SDK_WHEEL" "/work/dist-python/$RUNTIME_WHEEL"
mkdir -p /tmp/dsh-sdk-manylinux-smoke
cd /tmp/dsh-sdk-manylinux-smoke
env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default --installed-wheel
env -u PYTHONPATH -u DSH_RUNTIME_MODE /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp --installed-wheel
'
- uses: actions/upload-artifact@v7
with:
name: ${{ steps.runtime.outputs.wheel }}
path: dist-python/${{ steps.runtime.outputs.wheel }}
if-no-files-found: error
retention-days: 7