从手动发版到全自动发布:我用 GitHub Actions + Semantic Release + Trusted Publisher 搭建 Python 包发布流水线
前言
很多 Python 项目在刚开始时,发布流程都比较“原始”:
1 | 改版本号 |
项目少的时候问题不大。
但只要:
- 包开始持续迭代
- 有多人协作
- 版本越来越多
- 需要自动生成 Release
- 需要自动发布 PyPI
很快就会进入一种状态:
1 | 版本号经常忘记改 |
最近把自己的 Python 项目完整升级了一遍,做成了:
- 自动测试
- 自动 lint
- 自动安全扫描
- 自动版本升级
- 自动 Git Tag
- 自动 GitHub Release
- 自动发布 PyPI
整套流程。
这一篇把完整配置、踩坑和注意事项全部整理一下。
适合:
- Python SDK
- 工具库
- FastAPI 项目
- 内部公共组件
- 想做工程化的 Python 项目
项目地址, 文章缺少文件可以从项目中找到。
一、先看一下项目结构
这次做自动化发布的项目,是一个完整的 Python SDK。
目录结构如下:
1 | . |
整个项目已经是一个比较标准的 Python SDK 结构。
里面包含:
- API 模块拆分
- examples 示例
- tests 自动化测试
- docs 文档
- scripts 发布脚本
- CHANGELOG
- CONTRIBUTING
这也是后面能够顺利做 CI/CD 自动化的基础。
二、最终效果
现在整个发布流程已经变成:
开发时:
1 | git commit -m "feat: add cache support" |
GitHub Actions 自动:
1 | pytest |
如果提交符合版本发布规则:
1 | 自动升级版本 |
整个过程不需要手动改版本号。
三、核心技术栈
这一套主要用了:
| 功能 | 工具 |
|---|---|
| CI/CD | GitHub Actions |
| 自动版本管理 | python-semantic-release |
| 自动发布 PyPI | Trusted Publisher |
| 测试 | pytest |
| 类型检查 | mypy |
| 代码格式化 | black |
| lint | flake8 |
| 安全扫描 | bandit + safety |
四、为什么使用 pyproject.toml
以前很多 Python 项目:
1 | setup.py |
混在一起。
后面维护起来非常痛苦。
现在更推荐统一:
1 | pyproject.toml |
整个项目的:
- 包配置
- 依赖
- lint
- pytest
- semantic-release
全部集中管理。
这也是现在 Python 官方推荐方案。
五、我的 pyproject.toml 主要配置
项目里主要包含:
项目基础信息
1 | [project] |
开发依赖
1 | [project.optional-dependencies] |
这样 workflow 里:
1 | pip install ".[dev]" |
就能一次性安装所有开发工具。
semantic-release
1 | [tool.semantic_release] |
这个后面会自动:
- 管理版本
- 自动打 tag
- 自动生成 Release
六、为什么不用 PyPI Token
以前很多教程都还是:
1 | PYPI_API_TOKEN |
方式。
现在已经不太推荐了。
原因很简单:
- Token 会泄露
- Secret 管理麻烦
- 权限不好控制
- 长期有效风险高
现在更推荐:
1 | Trusted Publisher(OIDC) |
即:
1 | GitHub Actions |
整个过程:
1 | 不需要任何 PyPI Token |
也是目前 PyPI 官方推荐方案。
七、先配置 Trusted Publisher
打开:
PyPI 官方网站
需要提前注册账号
进入:
1 | Manage |
添加:
1 | Trusted Publisher |
这里几个字段非常重要。
Project Name
必须和:
1 | [project] |
完全一致。
Owner
1 | GitHub 用户名。 |
Repository
1 | 仓库完整连接。 |
Workflow name
1 | 填写: |
否则 GitHub Actions 发布时会报:
1 | No matching trusted publisher |
这是最常见的问题之一。
Environment name
1 | release |
保存
点击:
1 | Add |
八、完整 release.yml 配置
这是目前项目里实际使用的 workflow。
相比网上很多“玩具教程”,这个版本已经可以直接用于生产。
触发条件
1 | on: |
这里做了三件事:
1. push 自动运行
推送代码自动执行 CI。
2. PR 自动检查
Pull Request 自动测试。
3. workflow_dispatch
允许手动触发。
GitHub 页面可以直接点击运行。
这个功能非常实用。
九、为什么很多人 workflow 不触发
这是最常见的问题。
比如:
1 | branches: |
但实际仓库:
1 | master |
那么:
1 | workflow 永远不会执行 |
我一开始也踩了这个坑。
后来统一改成:
1 | branches: [master, develop] |
就正常了。
十、测试矩阵
测试部分:
1 | strategy: |
作用是:
1 | 自动测试多个 Python 版本 |
避免:
1 | 本地能跑 |
十一、依赖安装
1 | pip install ".[dev]" |
这里依赖:
1 | [project.optional-dependencies] |
这样开发依赖统一维护。
不用 workflow 里手写一堆 pip install。
十二、自动测试
项目 tests 目录里已经拆分得比较完整:
1 | tests/ |
workflow 中:
1 | pytest --cov=pydify_plus --cov-report=xml |
这里顺便加了:
1 | coverage |
并上传:
1 | codecov/codecov-action |
这样每次提交:
- 覆盖率
- 测试结果
都能自动统计。
十三、Lint 检查
项目里同时用了:
black
1 | black --check pydify_plus tests |
检查格式。
flake8
1 | flake8 --max-line-length 88 |
检查代码规范。
mypy
1 | mypy pydify_plus |
做类型检查。
十四、安全扫描
这个部分很多项目其实都没有。
但非常建议加。
Bandit
1 | bandit -r pydify_plus |
扫描:
- 危险函数
- subprocess
- eval
- pickle
等安全问题。
Safety
1 | safety check |
检查依赖漏洞。
十五、Build 阶段
Build 只在:
1 | master push |
时运行。
配置:
1 | if: github.event_name == 'push' && |
避免:
1 | develop 分支也发版 |
十六、自动版本管理
这一部分是核心。
项目里使用:
1 | python-semantic-release |
十七、为什么推荐 semantic-release
它会根据:
1 | Commit Message |
自动决定版本号。
例如:
feat
1 | git commit -m "feat: add redis cache" |
自动:
1 | 1.0.0 → 1.1.0 |
fix
1 | git commit -m "fix: websocket reconnect" |
自动:
1 | 1.0.0 → 1.0.1 |
十八、版本发布配置
workflow 中:
1 | uses: python-semantic-release/python-semantic-release@v10 |
这里会自动:
- 分析 commit
- 计算版本
- 创建 Git Tag
- 创建 GitHub Release
十九、Commit Message 必须规范
这一点很重要。
否则 semantic-release 不会工作。
推荐:
新功能
1 | feat: add xxx |
修复
1 | fix: resolve xxx |
文档
1 | docs: update readme |
重构
1 | refactor: optimize api |
二十、自动发布 PyPI
发布阶段:
1 | uses: pypa/gh-action-pypi-publish@release/v1 |
注意:
这里没有:
1 | PYPI_API_TOKEN |
因为项目已经切换到了:
1 | Trusted Publisher |
二十一、为什么 build 和 deploy 分开
这是一个非常重要的实践。
很多教程会:
1 | build + publish |
写在一起。
但实际上:
1 | 分离更稳定 |
因为:
- build 失败不会污染 release
- deploy 更容易重试
- 日志更清晰
- 更适合后续扩展
二十二、examples 目录的重要性
很多 Python SDK 发布后:
1 | 只有 README |
用户其实很难快速上手。
这个项目里专门保留了:
1 | examples/ |
包括:
1 | example_async.py |
这样:
- 同步调用
- 异步调用
- FastAPI 集成
都能快速演示。
对 SDK 类型项目来说非常重要。
二十三、docs 文档目录建议保留
项目里:
1 | docs/API_REFERENCE.md |
也是后面做:
- mkdocs
- sphinx
- GitHub Pages
的基础。
很多人前期不做 docs,后面维护成本会越来越高。
二十四、自动发布完整流程
现在整个流程:
1 | git push |
已经完全自动化。
二十五、几个踩坑记录
1. workflow 不触发
原因:
1 | branches: |
但仓库实际:
1 | master |
2. PyPI 发布失败
原因:
1 | Trusted Publisher workflow 名不一致 |
PyPI 配的是:
1 | publish.yml |
实际:
1 | release.yml |
3. wheel 里只有 dist-info
原因:
1 | 缺少 __init__.py |
4. twine check 报错
原因:
1 | 多个 Python 环境混用 |
Mac 上尤其容易出现。
后来统一:
1 | python -m xxx |
问题基本解决。
二十六、现在推荐的 Python 工程化组合
目前这套组合已经比较稳定:
| 功能 | 推荐 |
|---|---|
| 环境管理 | uv |
| 打包 | setuptools |
| 自动版本 | semantic-release |
| 自动发布 | Trusted Publisher |
| 测试 | pytest |
| lint | black + flake8 |
| 类型检查 | mypy |
| 安全扫描 | bandit + safety |
二十七、最后
Python 项目做到后面,其实核心已经不是:
1 | 功能开发 |
而是:
1 | 工程化 |
包括:
- 自动测试
- 自动发版
- 自动版本管理
- 自动发布
- CI/CD
- 安全扫描
这些东西一开始看起来复杂。
但真正搭起来之后,后续维护成本会低很多。
尤其是:
1 | 自动版本 + 自动发布 |
一旦习惯之后,基本就回不去了。