Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/release-one-click.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
tags:
- '*'
- '!python-sdk-v*'

concurrency:
group: release-one-click-${{ github.ref }}
Expand Down
108 changes: 108 additions & 0 deletions .github/workflows/release-python-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Release Python SDK

on:
push:
tags:
- 'python-sdk-v*'
workflow_dispatch:
inputs:
target:
description: 'Publish target'
type: choice
options:
- testpypi
- pypi
default: testpypi

concurrency:
group: release-python-sdk-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write

defaults:
run:
working-directory: sdk/python

jobs:
build-and-publish:
name: Build and publish cubesandbox
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build twine

- name: Verify version consistency
if: github.event_name == 'push'
run: |
set -euo pipefail
tag_ref="${GITHUB_REF_NAME#python-sdk-v}"
init_ver="$(python -c 'import re,pathlib;print(re.search(r"__version__\s*=\s*\"([^\"]+)\"", pathlib.Path("cubesandbox/__init__.py").read_text()).group(1))')"
echo "tag=${tag_ref}"
echo "__version__=${init_ver}"
if [[ "${tag_ref}" != "${init_ver}" ]]; then
echo "::error::Tag (python-sdk-v${tag_ref}) does not match cubesandbox.__version__ (${init_ver})" >&2
exit 1
fi

- name: Build sdist and wheel
run: |
rm -rf dist
python -m build

- name: Verify built version matches __version__
run: |
set -euo pipefail
init_ver="$(python -c 'import re,pathlib;print(re.search(r"__version__\s*=\s*\"([^\"]+)\"", pathlib.Path("cubesandbox/__init__.py").read_text()).group(1))')"
ls dist/
test -f "dist/cubesandbox-${init_ver}.tar.gz"
test -f "dist/cubesandbox-${init_ver}-py3-none-any.whl"

- name: Twine check
run: python -m twine check --strict dist/*

- name: Publish to TestPyPI
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: sdk/python/dist
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.CUBE_PYPI_TOKEN }}
skip-existing: true

- name: Publish to PyPI
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: sdk/python/dist
password: ${{ secrets.CUBE_PYPI_TOKEN }}

- name: Ensure GitHub Release exists
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
working-directory: ${{ github.workspace }}
run: |
if ! gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
gh release create "${GITHUB_REF_NAME}" \
--title "${GITHUB_REF_NAME}" \
--notes "Python SDK release. Install with: pip install cubesandbox==${GITHUB_REF_NAME#python-sdk-v}"
fi

- name: Attach distributions to GitHub Release
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${GITHUB_REF_NAME}" dist/* --clobber
6 changes: 4 additions & 2 deletions docs/.vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ export default withMermaid(defineConfig({
{
text: 'Maintainer Docs',
items: [
{ text: 'Blog Maintenance', link: '/guide/maintainer/blog' }
{ text: 'Blog Maintenance', link: '/guide/maintainer/blog' },
{ text: 'Releasing the Python SDK', link: '/guide/maintainer/release-python-sdk' }
]
}
],
Expand Down Expand Up @@ -257,7 +258,8 @@ export default withMermaid(defineConfig({
{
text: '维护者文档',
items: [
{ text: '博客维护', link: '/zh/guide/maintainer/blog' }
{ text: '博客维护', link: '/zh/guide/maintainer/blog' },
{ text: '发布 Python SDK', link: '/zh/guide/maintainer/release-python-sdk' }
]
}
],
Expand Down
91 changes: 91 additions & 0 deletions docs/guide/maintainer/release-python-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Releasing the Python SDK

This guide explains how to publish the `cubesandbox` Python package on PyPI from CI.

The SDK source lives in [`sdk/python/`](https://github.com/TencentCloud/CubeSandbox/tree/main/sdk/python).
Releases are driven by the [`release-python-sdk.yml`](https://github.com/TencentCloud/CubeSandbox/blob/main/.github/workflows/release-python-sdk.yml)
GitHub Actions workflow.

## Prerequisites

- A maintainer-level GitHub account on the repository (push access for tags).
- The repository secret `CUBE_PYPI_TOKEN` must be configured with a valid PyPI API token.
- For first-ever release: a user-scoped token can be used. Replace it with a project-scoped token (`Scope = Project: cubesandbox`) immediately after the first successful publish.
- Optionally, add a TestPyPI token under the same `CUBE_PYPI_TOKEN` name in a separate environment if you want to dry-run against TestPyPI.

## Versioning rules

- Follow [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH` (pre-releases such as `0.3.0rc1` are also accepted by PyPI).
- Version is the **single source of truth** in `sdk/python/cubesandbox/__init__.py` (`__version__ = "X.Y.Z"`).
`pyproject.toml` reads it dynamically via `[tool.setuptools.dynamic].version`.
- The git tag must match the package version exactly:
- Tag format: `python-sdk-vX.Y.Z`
- The release workflow fails if `tag` ≠ `__version__`.

## Tag conventions and workflow isolation

| Workflow | Tag pattern |
|---|---|
| `release-one-click.yml` (main bundle release) | `*` excluding `python-sdk-v*` |
| `release-python-sdk.yml` (Python SDK on PyPI) | `python-sdk-v*` |

Both workflows are mutually exclusive: a `python-sdk-v*` tag never triggers the one-click bundle, and a regular `vX.Y.Z` tag never triggers a PyPI publish.

## Standard release procedure

1. **Bump the version** on `main` in a regular PR:

```python
# sdk/python/cubesandbox/__init__.py
__version__ = "0.2.1"
```

Update `sdk/python/README.md` and the changelogs (`docs/changelog.md`, `docs/zh/changelog.md`) if the release contains user-visible changes. Get the PR reviewed and merged like any other change.

2. **(Optional but recommended) Dry-run via TestPyPI.** Open the
[Release Python SDK workflow](https://github.com/TencentCloud/CubeSandbox/actions/workflows/release-python-sdk.yml),
click **Run workflow**, and pick `target: testpypi`. Then verify install:

```bash
python -m venv /tmp/venv && source /tmp/venv/bin/activate
pip install -i https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
"cubesandbox==0.2.1"
python -c "import cubesandbox; print(cubesandbox.__version__)"
```

3. **Tag and push** from the merge commit on `main`:

```bash
git checkout main && git pull
git tag python-sdk-v0.2.1
git push origin python-sdk-v0.2.1
```

4. **Watch the workflow.** The `Release Python SDK` workflow runs automatically and:
- Validates `tag` ↔ `__version__` consistency.
- Builds `sdist` and `wheel` with `python -m build`.
- Validates package metadata via `twine check --strict`.
- Publishes to PyPI using `CUBE_PYPI_TOKEN`.
- Creates the matching GitHub Release and attaches the built artifacts.

5. **Smoke-test the release** from a clean environment:

```bash
python -m venv /tmp/venv && source /tmp/venv/bin/activate
pip install "cubesandbox==0.2.1"
python -c "from cubesandbox import Sandbox; print('ok')"
```

## Recovering from a bad release

PyPI **does not allow re-uploading the same version**, even after deletion. If a published version has a serious bug:

1. Yank the affected version on PyPI (Project → Manage → Releases → Yank). Yanked versions stay installable when pinned but are skipped by `pip install cubesandbox`.
2. Bump the version (e.g. `0.2.1` → `0.2.2`) and follow the standard release procedure.
3. If the workflow itself failed before publish, simply fix the issue, delete the failed tag (`git push --delete origin python-sdk-v0.2.1 && git tag -d python-sdk-v0.2.1`), and re-tag.

## Future improvements

- **Trusted Publishing (OIDC).** Replace the `CUBE_PYPI_TOKEN` secret with PyPI Trusted Publishing. Add the workflow as a trusted publisher on the PyPI project page, set `permissions: id-token: write` on the publish job, and remove the `password:` input. Tokens then no longer need to be rotated.
- **Test gating.** Run `pytest -m "not e2e"`, `ruff check`, and `mypy` as a prerequisite job for `build-and-publish`, so a release cannot be cut from a broken state.
89 changes: 89 additions & 0 deletions docs/zh/guide/maintainer/release-python-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 发布 Python SDK

本文说明如何通过 CI 将 `cubesandbox` Python 包发布到 PyPI。

SDK 源码位于 [`sdk/python/`](https://github.com/TencentCloud/CubeSandbox/tree/main/sdk/python),
发布流水线为 [`release-python-sdk.yml`](https://github.com/TencentCloud/CubeSandbox/blob/main/.github/workflows/release-python-sdk.yml)。

## 前置条件

- 仓库的维护者权限(可以推送 tag)。
- 仓库 Secret `CUBE_PYPI_TOKEN` 已配置为有效的 PyPI API Token。
- 首次发版时可以使用 user-scoped token;首次发布成功后**立即**替换成 project-scoped token(`Scope = Project: cubesandbox`)。
- 如需先在 TestPyPI dry-run,可在独立 environment 下用同名 secret 配置 TestPyPI token。

## 版本规范

- 遵循 [SemVer](https://semver.org/) `MAJOR.MINOR.PATCH`,预发布版本(如 `0.3.0rc1`)也被 PyPI 接受。
- 版本号以 `sdk/python/cubesandbox/__init__.py` 中的 `__version__ = "X.Y.Z"` 为**唯一来源**,`pyproject.toml` 通过 `[tool.setuptools.dynamic].version` 动态读取,无需重复维护。
- git tag 必须与版本号严格一致:
- tag 格式:`python-sdk-vX.Y.Z`
- 不一致时,发布工作流会直接失败。

## tag 约定与流水线隔离

| 工作流 | 触发 tag |
|---|---|
| `release-one-click.yml`(主包一键部署 release) | `*`,但排除 `python-sdk-v*` |
| `release-python-sdk.yml`(Python SDK 发布到 PyPI) | `python-sdk-v*` |

两条流水线互不重叠:`python-sdk-v*` 不会触发 one-click bundle,普通的 `vX.Y.Z` 也不会触发 PyPI 发布。

## 标准发版流程

1. **在 `main` 分支提一个常规 PR 升版本号**:

```python
# sdk/python/cubesandbox/__init__.py
__version__ = "0.2.1"
```

如果发布内容有用户可见变更,同步更新 `sdk/python/README.md`、`docs/changelog.md`、`docs/zh/changelog.md`。然后走正常的 review 与合入流程。

2. **(可选,强烈建议)TestPyPI dry-run。** 打开
[Release Python SDK 工作流](https://github.com/TencentCloud/CubeSandbox/actions/workflows/release-python-sdk.yml),
点击 **Run workflow**,选 `target: testpypi`。完成后验证安装:

```bash
python -m venv /tmp/venv && source /tmp/venv/bin/activate
pip install -i https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
"cubesandbox==0.2.1"
python -c "import cubesandbox; print(cubesandbox.__version__)"
```

3. **在 `main` 上的合入 commit 打 tag 并推送**:

```bash
git checkout main && git pull
git tag python-sdk-v0.2.1
git push origin python-sdk-v0.2.1
```

4. **观察工作流**。`Release Python SDK` 自动执行:
- 校验 `tag` 与 `__version__` 一致;
- 用 `python -m build` 构建 sdist 和 wheel;
- `twine check --strict` 校验包元信息;
- 用 `CUBE_PYPI_TOKEN` 发布到 PyPI;
- 创建对应的 GitHub Release 并附上构建产物。

5. **在干净环境冒烟验证**:

```bash
python -m venv /tmp/venv && source /tmp/venv/bin/activate
pip install "cubesandbox==0.2.1"
python -c "from cubesandbox import Sandbox; print('ok')"
```

## 发版异常处理

PyPI **不允许同一版本号重复上传**,即使已删除。如果某个版本上线后发现严重问题:

1. 在 PyPI 上 yank 该版本(Project → Manage → Releases → Yank)。被 yank 的版本依然可以通过精确 pin 安装,但 `pip install cubesandbox` 会跳过它。
2. 升一位版本(例如 `0.2.1` → `0.2.2`),再走一遍标准发版流程。
3. 如果是工作流自身在发布前就失败,直接修问题,删掉失败的 tag(`git push --delete origin python-sdk-v0.2.1 && git tag -d python-sdk-v0.2.1`),再重新打 tag。

## 后续可改进项

- **切换到 PyPI Trusted Publishing(OIDC)**。在 PyPI 项目页将本工作流加为 trusted publisher,给 publish job 加 `permissions: id-token: write` 并移除 `password:` 字段,从此不再需要 `CUBE_PYPI_TOKEN`,也无需轮换。
- **加测试门禁**。在 `build-and-publish` 之前增加一个 job 跑 `pytest -m "not e2e"`、`ruff check`、`mypy`,避免从坏状态发版。
7 changes: 5 additions & 2 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ and control the full sandbox lifecycle — including pause/resume with memory sn

## Installation

> **Note:** `cubesandbox` is not yet published to PyPI.
> Install from source until the first release is available:
```bash
pip install cubesandbox
```

Or install the latest development version from source:

```bash
git clone https://github.com/TencentCloud/CubeSandbox.git
Expand Down
30 changes: 28 additions & 2 deletions sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,40 @@ build-backend = "setuptools.build_meta"

[project]
name = "cubesandbox"
version = "0.2.0"
description = "Python SDK for CubeSandbox"
readme = "README.md"
license = { text = "Apache-2.0" }
requires-python = ">=3.9"
authors = [
{ name = "Tencent CubeSandbox Authors" },
]
keywords = ["cubesandbox", "sandbox", "code-execution", "ai-agent", "microvm"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"httpx>=0.27",
"requests>=2",
]
dynamic = ["version"]

[project.urls]
Homepage = "https://github.com/TencentCloud/CubeSandbox"
Repository = "https://github.com/TencentCloud/CubeSandbox"
Issues = "https://github.com/TencentCloud/CubeSandbox/issues"
Documentation = "https://github.com/TencentCloud/CubeSandbox/tree/main/sdk/python"

[project.optional-dependencies]
dev = [
Expand All @@ -24,6 +47,9 @@ dev = [
"ruff>=0.4",
]

[tool.setuptools.dynamic]
version = { attr = "cubesandbox.__version__" }

[tool.setuptools.packages.find]
where = ["."]
include = ["cubesandbox*"]
Expand All @@ -47,4 +73,4 @@ ignore = ["E501"]
[tool.mypy]
python_version = "3.9"
strict = true
ignore_missing_imports = true
ignore_missing_imports = true
Loading