Skip to content

Sglang-jax Release

背景

about releases

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 流程,将经过验证的 main commit 转换为用户可消费的发布产物。
  • 定义并执行 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 流程如下:

  1. Maintainer 通过 GitHub Actions workflow_dispatch 手动触发 release-tag.yml,输入完整目标版本号字符串(如 0.0.3 / 0.0.4rc0 / 0.0.3.post1)。
  2. release-tag.yml 校验版本号格式与唯一性(不与已有 tag 冲突),在当前 commit SHA 上构建 PyPI wheel 和 Docker 镜像(不发布),执行强制验证:
    • 运行 smoke test:安装 wheel 并验证基本 import,启动 Docker 容器并验证基本功能。
    • 运行 benchmark 回归检测:执行关键性能测试套件。
  3. 验证全部通过后,release-tag.yml 才创建 annotated git tag 并推送到 GitHub。验证未通过则失败终止,不创建 tag、不发布任何产物
  4. Tag push 事件触发 release.yml 发布编排,并行执行:
    • 构建 wheel 并发布到 PyPI。
    • 从 tag checkout 的源码树构建 Docker 镜像(Dockerfile 内 COPY . /src && pip install /src拉取 PyPI 包)并推送到 Docker Hub。
  5. 两者完成后,CI 基于 conventional commits 自动生成 Changelog,创建 GitHub Release。

PyPI 包和 Docker 镜像共享同一个版本号。版本号递增规则:积累特性发版升 minor(如 0.2.00.3.0), 紧急 bug 修复升 patch(如 0.3.00.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 应使用动态版本:

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.01.0.0
  • minor:向后兼容的功能新增(如 0.2.00.3.0
  • patch:向后兼容的 bug 修复(如 0.3.00.3.1

参考:

Release 触发

Release 分为两个阶段,由两个独立 workflow 承担,分离发布准备和外部产物发布:

阶段一:验证并创建 Tag(release-tag.yml)

由 maintainer 通过 workflow_dispatch 手动触发,负责版本号校验、release 验证与 tag 创建。Validate 前置到 tag 创建之前,确保失败时不会留下脏 tag。

yaml
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: string

Workflow 执行流程:

  1. actions/checkout 使用 fetch-depth: 0,以便能做 ancestor 校验(验证当前 HEAD 在 main 历史中,防止在非 main commit 上误打 tag)。
  2. 校验输入版本号字符串符合 PEP 440 且匹配本 RFC 的版本约定(stable / rcN / .postN)。
  3. 验证当前 HEAD 在 main 分支历史中。
  4. 检查目标 tag v<version> 不存在(存在则失败,避免覆盖)。
  5. 在当前 commit SHA 上构建 PyPI wheel 和 Docker 镜像(通过 SETUPTOOLS_SCM_PRETEND_VERSION 强制对齐到输入版本号,但不上传 PyPI、不推送 Docker tag),运行 smoke test 与 benchmark 回归检测。
  6. 验证全部通过后,创建 annotated tag 并推送:git tag -a v<version> -m "sglang-jax v<version>" && git push origin v<version>
  7. 验证未通过则失败终止,不创建任何 tag、不发布任何产物。

首次发布也由 maintainer 直接指定版本号(如 0.0.1),无需特殊 fallback 逻辑。

阶段二:发布编排(release.yml)

被 tag push 事件自动触发,同时支持 workflow_dispatch 指定 tag 手动重跑(用于 PyPI / Docker 发布失败后的重试)。

编排结构:publish-pypipublish-docker 并行执行,两者都成功后由 github-release 创建 GitHub Release。

publish-docker 从 tag checkout 的源码树构建(Dockerfile 内 COPY 源码后 pip install),不依赖 PyPI 发布结果。两条发布路径独立,单边失败不阻塞另一条;GitHub Release 在两者都成功之前不创建。Validate 阶段的 Docker 镜像同样从源码构建,与 publish 阶段走完全相同的安装路径,确保"测的就是发的"。

其中 publish-pypipublish-docker 分别调用可复用工作流 release-pypi.ymlrelease-docker.yml(均为 workflow_call 触发)。

Workflow 文件布局

text
.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: 0release-tag.yml 需要历史做 ancestor 校验,release.yml 需要历史让 git-cliff 遍历两个 tag 之间的 commits 生成 changelog。

Git Tag 创建

Git tag 由 CI 自动创建,必须满足以下规则:

  • Tag 是 release 版本的唯一来源:v0.0.3 对应 PyPI version 0.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 的命令示例:

bash
# Workflow 内部执行,VERSION 来自 maintainer 输入
git tag -a v${VERSION} -m "sglang-jax v${VERSION}"
git push origin v${VERSION}

rcNpostN 也通过 release-tag.yml 触发(在 version 输入中填写如 0.0.4rc00.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-publish action 发布。与 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,包含:
    • Featuresfeat: 开头的 commits
    • Bug Fixesfix: 开头的 commits
    • Performanceperf: 开头的 commits
    • Breaking Changes:包含 BREAKING CHANGE: 的 commits
    • 其他docs:, chore:, refactor: 等按类型分组

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

  • 稳定别名:latest

  • Nightly 版本(如启用):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.ymlworkflow_dispatch 指定同一个 tag 重跑。publish-pypi 使用 twine upload --skip-existing(或 pypa/gh-action-pypi-publishskip-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.ymltag 未推,无任何残留
GitHub Actions runner 网络超时,PyPI 发布失败从同一个 tag 重跑 release.ymltag 已存在,重跑 publish-pypi
PyPI 成功但 Docker 推送失败从同一个 tag 重跑 release.ymlPyPI 不需重发,跳过 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.ymlrelease.ymlrelease-pypi.ymlrelease-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 凭据约束在 prod environment 的 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 生成完全人工撰写 highlightsgit-cliff 基于 conventional commits 自动生成保持差异:早期发版稀疏、maintainer 资源有限,自动生成更划算;高质量 highlights 可作为可选补充
GitHub Release 创建人工CI 自动保持差异:同上
幂等性姿势twine upload --skip-existing + imagetools create同(已采纳)publish-pypi--skip-existingpublish-dockerimagetools 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

  • 自动回滚机制的具体实现方案。