【Bug已解决】[Build] 1.27.0 on PyPI but no release tag or notes on github 解决方案
【Bug已解决】[Build] 1.27.0 on PyPI but no release tag or notes on github 解决方案
一、现象长什么样
onnxruntime==1.27.0已经能在 PyPI 上pip install到,但去 GitHub Releases 一看:没有v1.27.0的 tag,也没有对应的 Release Notes,源码 tag 缺失,用户没法对照 changelog,CI 里依赖git describe的脚本也拿不到版本。
pip install onnxruntime==1.27.0 # 成功 git ls-remote --tags https://github.com/microsoft/onnxruntime v1.27.0 # 无输出 gh release view v1.27.0 # "release not found"最小影响链:
# 用户想从 tag 拉对应源码 git clone --branch v1.27.0 https://github.com/microsoft/onnxruntime # 失败:tag 不存在,尽管 PyPI 上 1.27.0 已经发布注意:这不是构建失败(wheel 已经在 PyPI),而是发布流程里“打 tag + 写 release notes”这一步和“传 PyPI”脱节了,属于发布编排问题。
二、背景
ONNX Runtime 的发布通常由两条相对独立的流水线组成:
- PyPI 发布:在打 tag(
vX.Y.Z)的 commit 上触发,或在一个“发布分支”上触发,构建 wheel 并twine upload到 PyPI。 - GitHub Release 创建:同一个 tag 上触发
gh release create,自动从 changelog / 提交生成 notes,并打 tag(如果 tag 还没打)。
常见的脱节原因:
- PyPI 发布被手动/分支触发:维护者直接在
main或某个发布分支上跑了 PyPI 发布(或从本地twine upload),但没有先打v1.27.0tag,于是 PyPI 有了包,GitHub 没有 tag/release。 - release 创建 job 失败但 PyPI job 独立成功:两条流水线
needs关系没串起来,PyPI job 不依赖 release job 成功,所以一边成一边败,用户只看到 PyPI 有包。 - notes 生成依赖未合并的 changelog:release notes 是从
docs/ReleaseNotes.md或自动git log生成,如果那次发的 changelog 没合进触发 commit,release 创建步骤报错(或生成空 notes 后跳过)。
三、根因
根因是PyPI 发布与 GitHub tag/release 创建没有形成原子、有序的发布单元:
- 触发源不一致:PyPI 上传的触发条件(分支 push / 手动)和 tag 创建不是同一个事件,导致“包先上、tag 后无”。
- 缺乏先后依赖与门禁:PyPI job 没有
needs: create-release,也没有“release 必须存在”的前置校验,于是 release 缺失也不阻止 PyPI 发布。 - tag 创建非强制:流水线里 tag 是 release 创建的“副作用”,一旦 release 步骤被跳过/失败,tag 就不打,而包的版本号仍来自
pyproject/setup.py的version,不依赖 tag,所以 PyPI 照常成功。
所以这不是编译错,而是发布编排里 tag/release 不是发布的硬前置,导致 PyPI 可以先于 tag 独立成功。
四、最小可运行复现
下面用一段发布脚本思路 + 校验,复现“PyPI 上传成功但 tag 缺失”的脱节:
#!/usr/bin/env bash set -u VERSION="1.27.0" REMOTE="https://github.com/microsoft/onnxruntime" # 步骤 A:构建并上传 PyPI(不检查 tag) echo "building wheel for $VERSION ..." # python -m build && twine upload dist/* echo "PyPI upload done (假设成功)" # 步骤 B:打 tag + 创建 release(可能独立、可能失败) if git rev-parse "$VERSION" >/dev/null 2>&1; then echo "tag $VERSION exists" else # 关键缺陷:这里如果没被执行(或被跳过),PyPI 已有包但无 tag echo "creating tag v$VERSION ..." # git tag "v$VERSION" && git push origin "v$VERSION" # gh release create "v$VERSION" --notes-file ReleaseNotes.md fi # 校验:PyPI 有包,但 tag 不一定有(脱节) if ! git ls-remote --tags "$REMOTE" "v$VERSION" | grep -q "v$VERSION"; then echo "::error::PyPI 有 $VERSION 但 GitHub 无 tag" >&2 # 正确做法:exit 1,阻止“只上 PyPI 不打 tag” exit 1 fi把“创建 tag”那行注释掉再跑,脚本会因最后的校验exit 1暴露脱节;修复前(无校验)则会“PyPI 成功、GitHub 无 tag”地结束,正是题面现象。
五、解决方案(第一层:最小直接修复)
最小修复:让发布成为原子单元——先打 tag + 创建 release,再(或同时)上传 PyPI,且上传前校验 tag 存在。
# .github/workflows/publish.yml (修复要点) publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - name: Create tag + release env: { GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} } run: | VERSION=$(python -c "import onnxruntime; print(onnxruntime.__version__)") git tag "v$VERSION" git push origin "v$VERSION" gh release create "v$VERSION" --generate-notes - name: Build and publish to PyPI # 必须等上一步成功(同 job 顺序执行天然满足) run: | python -m build twine upload dist/* - name: Verify consistency run: | VERSION=$(python -c "import onnxruntime; print(onnxruntime.__version__)") git ls-remote --tags origin "v$VERSION" | grep -q "v$VERSION" \ || { echo "::error::tag missing after publish"; exit 1; }要点:(1) tag + release 创建在 PyPI 上传之前且同 job 顺序执行;(2) 最后校验 tag 存在,缺失即 fail。这一层立刻消除“PyPI 有、GitHub 无”的脱节。
六、解决方案(第二层:结构性改进)
把“发布必须包含 tag + release notes + PyPI 三者一致”收口成唯一的配置对象OrtPypiReleaseTagPolicy,CI 读它做断言:
from dataclasses import dataclass, field from typing import Tuple, List @dataclass(frozen=True) class OrtPypiReleaseTagPolicy: """ORT 发布一致性的单一事实来源。""" # 发布必须产出的“交付物” required_deliverables: Tuple[str, ...] = ( "git_tag", # vX.Y.Z tag "github_release", # GitHub Release + notes "pypi_wheel", # PyPI 包 ) # 顺序:先 tag/release,后 PyPI(保证不脱节) order: Tuple[str, ...] = ("git_tag", "github_release", "pypi_wheel") # 是否强制三者版本号一致 enforce_version_consistency: bool = True # notes 来源 notes_source: str = "RELEASE_NOTES.md" # 缺失即失败(而非静默) fail_on_missing: bool = True def verify(self, done: List[str]) -> List[str]: missing = [d for d in self.required_deliverables if d not in done] if missing and self.fail_on_missing: raise RuntimeError(f"发布缺失交付物: {missing}") return missing def describe(self) -> str: return "tag/release/PyPI 为原子发布单元,顺序固定、版本一致" POLICY = OrtPypiReleaseTagPolicy() def check_publish(done: List[str], policy: OrtPypiReleaseTagPolicy = POLICY) -> List[str]: return policy.verify(done)所有发布流水线读同一份POLICY,tag/release/PyPI 成为原子单元,脱节在 CI 里就报错。
七、解决方案(第三层:断言 / CI 守护)
把“tag/release/PyPI 三者齐备且一致”做成断言。下面用 pytest 风格守护:
import pytest def test_three_deliverables_required(policy): assert set(policy.required_deliverables) == {"git_tag", "github_release", "pypi_wheel"} def test_order_tag_before_pypi(policy): assert policy.order.index("git_tag") < policy.order.index("pypi_wheel") assert policy.order.index("github_release") < policy.order.index("pypi_wheel") def test_missing_tag_fails(policy): with pytest.raises(RuntimeError): check_publish(["pypi_wheel", "github_release"]) # 故意缺 git_tag def test_complete_publish_passes(policy): assert check_publish(["git_tag", "github_release", "pypi_wheel"]) == []这四组断言锁住:(1) 三个交付物必含;(2) tag/release 在 PyPI 之前;(3) 缺 tag 即失败;(4) 齐备则通过。CI 跑通即代表发布原子性被守护。
八、排查清单
遇到“PyPI 有包但 GitHub 无 tag/notes”:
- 确认触发源:PyPI 上传是被什么事件触发的?是不是绕过了打 tag 的流水线。
- 查 release job:GitHub Release 创建步骤是否跑了、是否失败被忽略。
- 看依赖关系:PyPI job 是否
needsrelease job,还是完全独立。 - 手动补救:补打
v1.27.0tag 并gh release create --generate-notes。 - 统一策略对象:用
OrtPypiReleaseTagPolicy固化“tag→release→PyPI”顺序。 - CI 守护:断言三个交付物齐备、版本一致,缺失即失败。
- 避免手动 twine:所有上传走流水线,保证和 tag 同源。
九、小结
[Build] 1.27.0 on PyPI but no release tag or notes on github的根因是发布编排里 PyPI 上传与 GitHub tag/release 创建不是原子、有序的单元:PyPI 发布可由分支/手动触发且不依赖 tag 存在,而 tag/release 创建是另一条可能失败被忽略的流水线,于是 PyPI 先成功、GitHub 却无 tag 与 notes。
最小修复是让发布同 job 先打 tag + 创建 release 再上传 PyPI,并校验 tag 存在;结构性改进是用唯一的OrtPypiReleaseTagPolicy把 tag/release/PyPI 固化为原子单元、固定顺序;CI 用四组断言守护“三者齐备、tag 先于 PyPI、缺即失败”。记住:发布是原子动作,包、tag、notes 必须一起成功或一起失败,不能只上 PyPI 不打 tag。
