如何在Python中正确更新__version__?能否通过Git+CI自动管理版本?
太懂这种困惑了——维护好几处硬编码的版本号简直是重复劳动,完全违背了单一数据源(SSOT)的原则!下面我给你详细说说怎么用CI实现Git托管版本的方案,再聊聊为啥很多热门Python包还在沿用老办法。
一、用Git Tag + CI实现版本统一的可行方案
核心思路就是只把Git Tag作为唯一的版本数据源,然后在CI流程里自动把版本号填充到所有需要的地方:
1. 先清理代码里的硬编码版本
- 把
__init__.py里的__version__改成占位符,比如:__version__ = "UNKNOWN" - 移除
setup.py/setup.cfg里的硬编码版本,改成从外部读取(后面CI会生成这个外部文件)。
2. 配置CI拉取完整Git历史
大部分CI工具默认只拉取最近一次提交,没法读取所有Tag,所以要设置完整克隆:
- GitHub Actions里,在
actions/checkout步骤加fetch-depth: 0:- uses: actions/checkout@v4 with: fetch-depth: 0 - GitLab CI里设置
GIT_DEPTH: 0。
3. 在CI中生成合规的版本号
用git describe获取版本信息,再转换成符合PEP 440的格式:
- 拿到最近的正式Tag(去掉前缀
v):# 输出示例:1.2.3 VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//') - 如果是开发版本(在Tag之后有新提交),生成带开发标识的版本:
# 输出示例:1.2.3.dev5+gabc123(5是提交数,abc123是哈希前缀) FULL_VERSION=$(git describe --tags | sed -E 's/^v(.*)-([0-9]+)-g([0-9a-f]+)$/\1.dev\2+g\3/; s/^v(.*)$/\1/')
4. 自动填充所有需要版本的文件
- 更新
__init__.py:用sed或Python脚本替换占位符:sed -i "s/__version__ = \"UNKNOWN\"/__version__ = \"$FULL_VERSION\"/" your_package/__init__.py - 更新打包配置:比如在CI里生成
version.txt,让setup.py读取它:
然后echo "$FULL_VERSION" > version.txtsetup.py里写:with open("version.txt", "r") as f: version = f.read().strip() setup( name="your-package", version=version, # 其他配置项 ) - 自动生成CHANGELOG:用
git-chglog这类工具,基于Git Tag和提交记录自动生成CHANGELOG,CI里运行命令后直接作为发布资产或者提交到仓库。
5. 发布环节的注意事项
确保CI的流程是:打Git Tag → 触发CI → 生成版本号 → 替换所有文件 → 构建wheel/sdist → 发布到PyPI,这样发布的包里面版本信息完全正确。
二、为啥大多数热门Python包还在硬编码版本?
其实不是他们不想用SSOT,而是有现实的考量:
- 兼容性与简单性:很多Python生态的工具(比如依赖扫描、本地调试)默认依赖代码里的硬编码版本号。对于小项目来说,硬编码的维护成本比搭建CI动态版本流程低得多。
- 离线可用性:如果用户下载了代码压缩包(不是Git克隆),或者克隆时没有拉取Tag,依赖Git的版本方案会显示
UNKNOWN,体验很差;硬编码版本则能随时查看。 - 历史遗留问题:很多热门包起步早,当时CI工具没现在普及,Python打包生态对动态版本的支持也不完善,一旦沿用下来,改动的成本就很高。
- 避免CI依赖:不同CI平台的Git配置可能有差异,处理PEP 440格式也需要额外脚本,硬编码版本更稳定可靠,不用操心CI环境的问题。
- PEP 440的严格限制:
git describe的原生输出不一定符合PEP 440规范,需要额外处理预发布、开发版本的格式,这增加了复杂度,很多团队觉得没必要。
总的来说,用Git Tag作为单一版本源是非常优雅的方案,适合有成熟CI流程、追求SSOT的项目。如果你走这条路,记得在代码里留个 fallback(比如当无法从Git获取版本时,显示一个默认的开发版本号),避免离线场景下的尴尬。
内容的提问来源于stack exchange,提问作者nowox
相关产品推荐
相关产品推荐

