跳转至

发布流程(中文)

本页说明 ztxexp 从改版本号到 PyPI 自动发包的完整链路。发布完全由 git tag 驱动,人工只需要改版本、提交、打 tag,剩下交给 CI。

1. 一次发布要改什么

版本号有两个落点,必须同步修改,否则发布前的校验脚本会直接中止流水线:

位置 字段 示例
pyproject.toml [project] version version = "1.1.0"
ztxexp/__init__.py __version__ __version__ = "1.1.0"

pyproject.toml 是发行版本的唯一来源,__version__ 是运行时自报的版本,两者与 tag 三方必须一致。

另外还要更新 CHANGELOG.md:新版本号、发布日期、Breaking / Added / Fixed / Docs / Migration 各分类下的条目。README 里的版本历史是给用户看的摘要,可以顺带补一条,但不是发布的硬性前置条件。

2. 本地发布步骤

# 1. 改版本号:pyproject.toml 与 ztxexp/__init__.py 两处
# 2. 更新 CHANGELOG.md

# 3. 发布前校验:tag 名、pyproject 版本、__version__ 三方比对
#    脚本用标准库 tomllib 读 pyproject.toml,需要 Python >= 3.11
python3.12 scripts/check_release.py v1.1.0

# 4. 提交改动并打 tag
git add pyproject.toml ztxexp/__init__.py CHANGELOG.md
git commit -m "chore(release): v1.1.0"
git tag v1.1.0

# 5. 推 main 与 tag(tag 推送才会触发发布)
git push origin main
git push origin v1.1.0

python3.12 scripts/check_release.py v1.1.0 只读校验,退出码 0 表示三方一致,1 表示不一致、参数非法或版本信息解析失败。脚本依赖 Python 3.11 起才有的标准库 tomllib,用 3.10 及以下的解释器跑会直接报"需要标准库 tomllib(Python >= 3.11)"并列出当前版本,不会抛一堆 traceback;CI 里 build 作业钉的是 3.12。建议本地先跑一次,避免 tag 推上去才发现版本号漏改。

3. CI 侧的发布链路

推 v* 形式的 tag 会触发 .github/workflows/publish.yml,它与 ci.yml 分开:ci.yml 只跑 main 与 PR 的 lint / 测试 / 文档构建,发布不走那里。工作流分三个作业,前一个不通过就不会进入下一个:

  1. build:先校验 tag 格式(必须以 v 加数字开头),再跑 python scripts/check_release.py "$tag" 比对 tag、pyproject.toml、ztxexp/__init__.py 三方版本,然后 python -m build 构建 sdist + wheel,twine check dist/* 检查元数据,最后把产物作为 dist artifact 上传。
  2. verify:在 Python 3.10–3.13 四个版本的干净环境里只安装 wheel 做冒烟测试(import ztxexp 与 ztxexp --help)。这一步故意不 checkout 源码,能 import 成功只可能来自打好的包,所以漏打包的文件会在这里暴露。
  3. publish:用 pypa/gh-action-pypi-publish 上传,部署目标为 GitHub Environment pypi,靠 id-token: write 换取 PyPI 的 OIDC 短期凭证。这个权限在 workflow 里声明,仓库里不需要任何 PyPI token。

手工补发走 workflow_dispatch:需要显式填一个已经存在的 tag——工作流会把 ref 解析成 refs/tags/<填的 tag>,tag 不在仓库里连 checkout 都过不去。所以补发不是凭空发一个新版本,它只是把某次 tag 发布的链路重跑一遍,版本校验与推 tag 完全相同,没有绕过校验直接上传的入口。补发时用这个入口,不要重推同名 tag。

4. 一次性仓库配置(只做一次)

Trusted Publishing 靠 OIDC 信任关系代替 API token,所以第一次发布前要在两边各登记一次:

  1. PyPI 项目页 Publishing → Add a new pending publisher,填写:
  2. Owner:ztxtech
  3. Repository:ztxexp
  4. Workflow name:publish.yml
  5. Environment name:pypi
  6. GitHub 仓库 Settings → Environments 新建 pypi,不需要填任何 secret。

完成后不再需要 PYPI_API_TOKEN、TWINE_USERNAME、TWINE_PASSWORD 一类的长期凭证,仓库里也不要再存 PyPI token。若之后改了发布工作流的文件名或 environment 名,PyPI 那边的登记要同步改,否则 OIDC 换取凭证会被拒绝。

5. 常见问题

tag 推上去了但流水线报版本不一致:check_release.py 比对的是 tag 名、[project] version、__version__ 三处,最常见的是漏改 __init__.py。补一次提交、删掉旧 tag 重新打即可:

git tag -d v1.1.0
git push origin :refs/tags/v1.1.0
# 改好版本号后重新 git tag / git push

想重发同一个版本:PyPI 不允许覆盖已上传的版本,只能升版本号后重新发。本地 dist/ 随便删,已上传的文件不能覆盖。发布工作流也没有加并发锁去"放开重跑",因为真正的约束在 PyPI 侧:同名同版本第二次上传必然被拒。

只想验证、不想发布:推 main 分支即可,ci.yml 会跑 lint、多版本测试、mkdocs build --strict、python -m build 与 twine check,覆盖发布前的全部检查,只是没有上传步骤。