Sglang-jax Release
背景
Releases are deployable software iterations you can package and make available for a wider audience to download and use.
在当前仓库中,一次 release 会把一个经过验证的 main commit 转换成带版本号的发布产物:PyPI 包和 Docker 镜像。
目标
- 建立可重复、可追溯的 Release 流程,将经过验证的
maincommit 转换为用户可消费的发布产物。 - 定义并执行
sglang-jax的 Semantic Versioning 策略,在 CI 中实现版本号自动递增与 tag 自动创建。 - 自动化 Python package 和 Docker image 两类分发产物的打包与发布。
- 基于 conventional commits 自动生成 Changelog,并随 Release 发布到 GitHub Releases。
- 发布前强制验证 Release 质量,在 CI 中执行 smoke test 和 benchmark 回归检测。
- 分离发布准备和外部产物发布,降低误发布风险。
非目标
- 不覆盖线上服务的自动部署;本文中的 CD 仅指 release artifacts 的自动化交付。
- 自动回滚机制(从 PyPI 撤包、删除 Docker 镜像 tag)
使用案例
- 距离上一次发版本之后,当前 main 分支积累了足够多的特性后经过讨论就可以 release。
- 修复了一个严重的 bug 需要紧急 release。
设计细节
Release 验证
Release workflow 在创建 git tag 之前执行强制验证(validate 步骤位于 release-tag.yml 中,在 tag push 之前):
- Smoke test:构建 PyPI wheel 并验证能安装和 import;构建 Docker 镜像并验证能启动
- Benchmark 回归检测:重新运行关键 benchmark 套件,确保 release commit 的性能符合预期。具体的回归阈值(吞吐/延迟容忍度、统计方法、失败判定)由后续 benchmark CI RFC/Issue 定义,本 RFC 只规定门禁位置。
注:初始实现中 smoke test 和 benchmark 回归检测使用 placeholder,后续接入 TPU runner(arc-runner-v6e-*)后补充实际验证逻辑。
设计原则:Release 阶段的 benchmark 作为最终发布门禁,防止 main 分支在 PR 合并后到打 tag 之间引入的性能退化。
Release 流程
整个 release 流程如下:
- Maintainer 通过 GitHub Actions
workflow_dispatch手动触发release-tag.yml,输入完整目标版本号字符串(如0.0.3/0.0.4rc0/0.0.3.post1)。 release-tag.yml校验版本号格式与唯一性(不与已有 tag 冲突),在当前 commit SHA 上构建 PyPI wheel 和 Docker 镜像(不发布),执行强制验证:- 运行 smoke test:安装 wheel 并验证基本 import,启动 Docker 容器并验证基本功能。
- 运行 benchmark 回归检测:执行关键性能测试套件。
- 验证全部通过后,
release-tag.yml才创建 annotated git tag 并推送到 GitHub。验证未通过则失败终止,不创建 tag、不发布任何产物。 - Tag push 事件触发
release.yml发布编排,并行执行:- 构建 wheel 并发布到 PyPI。
- 从 tag checkout 的源码树构建 Docker 镜像(Dockerfile 内
COPY . /src && pip install /src,不拉取 PyPI 包)并推送到 Docker Hub。
- 两者完成后,CI 基于 conventional commits 自动生成 Changelog,创建 GitHub Release。
PyPI 包和 Docker 镜像共享同一个版本号。版本号递增规则:积累特性发版升 minor(如
0.2.0→0.3.0), 紧急 bug 修复升 patch(如0.3.0→0.3.1)。
版本来源
版本号由 maintainer 在触发 release 时手填字符串作为唯一来源,CI 只负责校验(PEP 440 合规、本文版本约定、与已有 tag 不冲突)后写入 git tag。CI 不做"读上一个 tag 自动 bump"逻辑,避免 rc/post 这类多维选择带来的歧义;具体版本号由 maintainer 根据下文版本递增规则决定。
Python package version 由 setuptools-scm 从 git tag 动态推导,不在源码中手动维护发布版本号。setuptools-scm 是 Python packaging 生态中常用的 SCM-based versioning 工具,SGLang 主仓也使用同样模式。
由于 validate 阶段在 tag 创建之前构建 wheel,setuptools-scm 此时还读不到目标 tag,会产出形如 0.0.3.dev1+g<sha> 的开发版本号。为此 validate 阶段的构建必须通过 SETUPTOOLS_SCM_PRETEND_VERSION=<maintainer 输入的版本号> 强制覆盖,保证 wheel 文件名与最终发布版本一致。tag push 后 release.yml 的发布构建同样使用 SETUPTOOLS_SCM_PRETEND_VERSION,版本号从 GITHUB_REF_NAME 去除 v 前缀后提取。两段构建保持同一注入机制,避免 shallow clone、fetch-depth 配置错误等环境差异让 setuptools-scm 隐式回退到 dev 版本号产出废品 wheel。
python/pyproject.toml 应使用动态版本:
[build-system]
requires = ["setuptools>=80", "setuptools-scm[simple]>=9.2"]
build-backend = "setuptools.build_meta"
[project]
dynamic = ["version"]
[tool.setuptools_scm]
fallback_version = "0.0.0.dev0"版本递增规则:
- major:不兼容的 API 变更(如
0.3.0→1.0.0) - minor:向后兼容的功能新增(如
0.2.0→0.3.0) - patch:向后兼容的 bug 修复(如
0.3.0→0.3.1)
参考:
setuptools-scm: https://pypi.org/project/setuptools-scm/- SGLang 主仓
pyproject.toml: https://github.com/sgl-project/sglang/blob/main/python/pyproject.toml
Release 触发
Release 分为两个阶段,由两个独立 workflow 承担,分离发布准备和外部产物发布:
阶段一:验证并创建 Tag(release-tag.yml)
由 maintainer 通过 workflow_dispatch 手动触发,负责版本号校验、release 验证与 tag 创建。Validate 前置到 tag 创建之前,确保失败时不会留下脏 tag。
on:
workflow_dispatch:
inputs:
version:
description: 'Target version string without v prefix (e.g. 0.0.3, 0.0.4rc0, 0.0.3.post1)'
required: true
type: stringWorkflow 执行流程:
actions/checkout使用fetch-depth: 0,以便能做 ancestor 校验(验证当前 HEAD 在 main 历史中,防止在非 main commit 上误打 tag)。- 校验输入版本号字符串符合 PEP 440 且匹配本 RFC 的版本约定(stable /
rcN/.postN)。 - 验证当前 HEAD 在 main 分支历史中。
- 检查目标 tag
v<version>不存在(存在则失败,避免覆盖)。 - 在当前 commit SHA 上构建 PyPI wheel 和 Docker 镜像(通过
SETUPTOOLS_SCM_PRETEND_VERSION强制对齐到输入版本号,但不上传 PyPI、不推送 Docker tag),运行 smoke test 与 benchmark 回归检测。 - 验证全部通过后,创建 annotated tag 并推送:
git tag -a v<version> -m "sglang-jax v<version>" && git push origin v<version>。 - 验证未通过则失败终止,不创建任何 tag、不发布任何产物。
首次发布也由 maintainer 直接指定版本号(如 0.0.1),无需特殊 fallback 逻辑。
阶段二:发布编排(release.yml)
被 tag push 事件自动触发,同时支持 workflow_dispatch 指定 tag 手动重跑(用于 PyPI / Docker 发布失败后的重试)。
编排结构:publish-pypi 与 publish-docker 并行执行,两者都成功后由 github-release 创建 GitHub Release。
publish-docker 从 tag checkout 的源码树构建(Dockerfile 内 COPY 源码后 pip install),不依赖 PyPI 发布结果。两条发布路径独立,单边失败不阻塞另一条;GitHub Release 在两者都成功之前不创建。Validate 阶段的 Docker 镜像同样从源码构建,与 publish 阶段走完全相同的安装路径,确保"测的就是发的"。
其中 publish-pypi 和 publish-docker 分别调用可复用工作流 release-pypi.yml 和 release-docker.yml(均为 workflow_call 触发)。
Workflow 文件布局
.github/workflows/
├── release-tag.yml # 新增:创建 tag(workflow_dispatch)
├── release.yml # 新增:发布编排器(push: tags + workflow_dispatch)
├── release-pypi.yml # 新增:PyPI 发布(workflow_call),替代旧 release-pypi.yml
└── release-docker.yml # 新增:Docker 发布(workflow_call)所有 release workflow 必须检查:
- 当前仓库是
sgl-project/sglang-jax。 - 输入的版本号符合 PEP 440 且匹配本文的版本约定(stable /
rcN/.postN)。 - 当前 commit 属于
main分支历史。 - 构建 PyPI 包时 checkout 必须使用
fetch-depth: 0:release-tag.yml需要历史做 ancestor 校验,release.yml需要历史让git-cliff遍历两个 tag 之间的 commits 生成 changelog。
Git Tag 创建
Git tag 由 CI 自动创建,必须满足以下规则:
- Tag 是 release 版本的唯一来源:
v0.0.3对应 PyPI version0.0.3。 - Tag 由 release workflow 根据 maintainer 输入的版本号字符串创建并推送。
- Tag 必须指向
main分支的 HEAD commit。 - 使用 annotated tag,tag message 使用
sglang-jax vX.Y.Z。 - 如果目标 tag 已存在,workflow 必须失败,不能覆盖、移动或删除已有 tag。
- Tag 创建成功后,PyPI、Docker 和 GitHub Release workflow 都以该 tag 作为版本号的唯一来源。
CI 创建 tag 的命令示例:
# Workflow 内部执行,VERSION 来自 maintainer 输入
git tag -a v${VERSION} -m "sglang-jax v${VERSION}"
git push origin v${VERSION}rcN 和 postN 也通过 release-tag.yml 触发(在 version 输入中填写如 0.0.4rc0 或 0.0.3.post1),与 stable 走同一套 validate-then-push 流程,maintainer 无需手动在本地 git tag。
发布产物
release.yml 编排发布以下产物:
- PyPI package:由
release-pypi.yml(workflow_call)负责。从 tag checkout 构建 wheel,通过pypa/gh-action-pypi-publishaction 发布。与publish-docker并行执行。 - Docker image:由
release-docker.yml(workflow_call)负责,与publish-pypi并行执行。Docker 构建从 tag checkout 的源码树进行(Dockerfile 内COPY . /src && pip install /src),不拉取 PyPI 包,推送固定版本 tag 和必要别名。与 validate 阶段 Docker 走完全相同的安装路径,保证 validate 测的产物即用户拿到的产物。 - GitHub Release:在编排器
release.yml中作为最终 job,PyPI 和 Docker 发布都成功后创建。Changelog 由git-cliff基于 conventional commits 自动生成,范围从上一个稳定 tag 到当前 tag,包含:- Features:
feat:开头的 commits - Bug Fixes:
fix:开头的 commits - Performance:
perf:开头的 commits - Breaking Changes:包含
BREAKING CHANGE:的 commits - 其他:
docs:,chore:,refactor:等按类型分组
- Features:
Changelog 生成工具使用 git-cliff(原生支持 PEP 440 tag pattern,官方 GitHub Action orhun/git-cliff-action)。
GitHub Release 的 prerelease 标记规则:
rcN版本必须标记为 prerelease。- 稳定版本和
postN版本不标记为 prerelease。 postN版本属于当前稳定线的发布修订,changelog 应说明它修复的是打包、依赖、镜像或发布流程问题。
Tag 规范
PyPI version 遵循 PEP 440;稳定版本主体采用 SemVer 风格。PyPI、Git tag、GitHub Release 和 Docker release tag 共享同一个基础版本号。
rcN 用于预发布验证;postN 只用于同一代码版本的发布包装修复,如依赖、镜像或打包修正;真实代码 bug fix 升 patch。
PyPI
- 稳定版本:
0.0.3 - 预发布版本:
0.0.4rc0 - 发布后修订:
0.0.4.post1
GitHub Release
- 稳定版本:
v0.0.3 - 预发布版本:
v0.0.4rc0 - 发布后修订:
v0.0.4.post1
Docker Hub
Docker registry:Docker Hub - lmsysorg/sglang-jax(与 SGLang 主仓共用 lmsysorg 组织,便于用户记忆和发现)。
当前仓库只发布 TPU Docker 镜像,不发布 CPU/GPU 变体。Docker release tag 和 Git tag 使用一致的固定版本 tag,维护稳定别名。
固定版本 tag:
v0.0.3稳定别名:
latestNightly 版本(如启用):
nightly-YYYYMMDD-<short_sha>
Docker alias 更新规则(不变量:latest 始终指向最新的 stable 或 postN 版本,永远不指向 rcN):
rcN版本只推固定版本 tag,不更新latest。- 稳定版本更新固定版本 tag,并更新
latest。 postN版本更新固定版本 tag,并更新latest。
部分失败处理
release-tag.yml 把 validate 前置到 tag 创建之前,因此不存在 "tag 已推但 validate 失败" 的尴尬场景。所有失败按 tag 是否已推送分两类处理:
Tag push 之前的失败(在 release-tag.yml 中)
- 没有 tag 被创建,没有任何外部产物被推送。
- 由 maintainer 修复代码或环境问题后,重新触发同一份
release-tag.yml并输入相同版本号字符串(修复中途的额外 commit 不影响版本号本身,因为版本号是手填的,不依赖 tag 历史推导)。
Tag push 之后的失败(在 release.yml 中)
- Tag 已存在,不允许移动或删除,PyPI 包和 Docker tag 已发布部分不自动撤回。
- 暂时性失败(如 runner 网络抖动):通过
release.yml的workflow_dispatch指定同一个 tag 重跑。publish-pypi使用twine upload --skip-existing(或pypa/gh-action-pypi-publish的skip-existing: true)让已上传版本自动跳过;publish-docker使用docker buildx imagetools create基于已构建的 digest 重新合并 manifest,避免同名 tag 指向不同 digest。 - 内容性错误(如
pyproject.toml漏依赖、Dockerfile 系统库不兼容):不撤回已发布产物,发.postN修复打包问题,或升 patch 修复代码 bug。 - PyPI 与 Docker 一方成功一方失败:两条发布路径并行独立,已成功的一方不撤回。重跑
release.yml时已成功的 job 通过幂等机制自动跳过(PyPI 用--skip-existing,Docker 用imagetools create重新合并),只需让失败那一条重新跑完,再跑github-release。 - 任一产物未成功,GitHub Release 不创建。Maintainer 必须在 release issue 中显式记录失败原因、当前外部产物状态、以及后续处理决策(是
.postN还是升 patch)。
场景对照
| 场景 | 处理方式 | 说明 |
|---|---|---|
| validate(smoke test / benchmark)失败 | 修复代码后重跑 release-tag.yml | tag 未推,无任何残留 |
| GitHub Actions runner 网络超时,PyPI 发布失败 | 从同一个 tag 重跑 release.yml | tag 已存在,重跑 publish-pypi |
| PyPI 成功但 Docker 推送失败 | 从同一个 tag 重跑 release.yml | PyPI 不需重发,跳过 publish-pypi;重跑 publish-docker |
pyproject.toml 漏写依赖,用户 pip install 后缺包 | 修复依赖声明,触发 release-tag.yml 并输入 .postN 版本号 | 代码没变,只是打包声明有误 |
| Docker 基础镜像的系统库版本不兼容 | 修复 Dockerfile,触发 release-tag.yml 并输入 .postN 版本号 | Python 代码没动,只是构建配置问题 |
| 推理结果错误 | 修复代码,触发 release-tag.yml 并输入 patch 版本号 | 代码行为变更,必须升版本号 |
外部产物(PyPI 版本、Docker tag)失败后均不自动撤回;如确需 yank PyPI 或删除 Docker tag,由 maintainer 人工处理并在 release issue 中说明。
权限模型
- Tag push:创建和推送
v*release tag。仓库应配置 tag protection rules,限制v[0-9]+.*。 - Release commit:必须位于受保护的
main分支历史中,且已经通过 PR CI。 - GitHub Environment:所有 release workflow(
release-tag.yml、release.yml、release-pypi.yml、release-docker.yml)必须配置environment: prod。仓库Settings → Environments → prod启用 Required reviewers,审批人为 sglang-jax maintainer 名单。release-tag.yml触发时(validate 前)和release.yml的每个发布 job 启动时都会卡在 environment approval gate,需 maintainer 在 GitHub UI 上点击 approve,以防止单个仓库写权限被滥用导致误发布,并把 PyPI / Docker / GitHub 凭据约束在prodenvironment 的 secrets 中。
备选方案
参考 SGLang 主仓(sgl-project/sglang)现行 release 体系作为对比基线。主仓的整体哲学是:maintainer 控制策略、CI 只做机械执行——版本号 maintainer 手填、changelog 手写、validate 通过"release branch + 全平台测试矩阵"在打 tag 之前完成,tag push 后 PyPI 和 Docker 并行发布、互不依赖。
本 RFC 选择性借鉴,不直接照搬,原因是两个项目所处阶段不同:sglang-jax 目前是 0.0.x 早期版本,只有 TPU 一条硬件线,发版频率稀疏,maintainer 资源有限;主仓覆盖 NVIDIA / AMD / NPU / Xeon / XPU 多平台,发版频繁且需要 hotfix 通道。下列子项明确说明选择与权衡:
| 维度 | SGLang 主仓 | 本 RFC | 取舍理由 |
|---|---|---|---|
| 版本号来源 | maintainer 手填字符串 | maintainer 手填字符串(已采纳) | 避免 rc-of-{major|minor|patch} 的二维选择难题,也更便于偶发的非常规版本号 |
setuptools-scm 与 pre-tag 构建 | SETUPTOOLS_SCM_PRETEND_VERSION 注入 | 同(已采纳) | 解决 validate 阶段 wheel 版本号与发布版本号不一致的问题 |
| Validate 时机 | 打 tag 之前在 release branch 上跑全平台测试 | 打 tag 之前在 release-tag.yml 内 inline 跑 | 保持差异,但需重新论证:sglang-jax 暂不引入 release branch;但 inline 跑 benchmark 对 TPU runner 调度是挑战,validate 是否应改为"main 上常规 nightly CI 已覆盖即可,release workflow 只跑 smoke test"是 open question |
| Release branch | 引入,承担测试缓冲 + hotfix 隔离 | 不引入,基于 main HEAD 打 tag | 保持差异:当前没有 hotfix 通道需求,引入 release branch 会显著增加流程复杂度,待真正出现并发线时再补 |
| PyPI / Docker 关系 | 并行触发,Docker 内部 pip install 走 PyPI CDN | 并行触发,Docker 从源码构建不走 PyPI(已采纳) | 让 validate 与 publish 两段 Docker 走完全相同的安装路径,消除"validate 测本地 wheel 但发布拉 PyPI"的双路径风险 |
| Changelog 生成 | 完全人工撰写 highlights | 用 git-cliff 基于 conventional commits 自动生成 | 保持差异:早期发版稀疏、maintainer 资源有限,自动生成更划算;高质量 highlights 可作为可选补充 |
| GitHub Release 创建 | 人工 | CI 自动 | 保持差异:同上 |
| 幂等性姿势 | twine upload --skip-existing + imagetools create | 同(已采纳) | publish-pypi 用 --skip-existing,publish-docker 用 imagetools create 重新合并 manifest |
| 多硬件镜像 | 多套 release-docker-*.yml + 复用 _docker-build-and-publish.yml | 仅 TPU 一套 | 当前保持简化,未来扩 GPU/CPU 变体时再抽复用工作流 |
| rc 通道 | 不通过 tag 表达,rc = 在 release branch 上测试 | 用 rcN tag | 保持差异:在没有 release branch 的前提下,rcN tag 是 sglang-jax 表达预发布的最低成本方式 |
Open Questions
- 自动回滚机制的具体实现方案。