手把手教你:GitHub CI + PyPI 自动发包,从此告别手动 twine upload
最近写了个 Python 练手项目,本地跑得挺顺,盘算着发到 PyPI 上,让别人也能一行
pip install就用起来。本来以为poetry build+twine upload就完事了,结果一脚踩进 GitHub Actions 的世界,从 CI 配置到 Trusted Publisher 认证,从 Poetry 依赖解析到版本号踩坑,一路上“惊吓”不断。折腾了一晚上,才把整条链路跑通。谨以此文,纪念熬的又一个夜——不算什么高深教程,但每个坑都是实打实踩过的,希望能帮后来的同学少绕几个弯。
先搞清楚几个名词
在开始之前,得先弄明白三个东西,不然 YAML 文件抄都抄不明白。
CI 是啥?
CI = Continuous Integration,翻译过来就是"持续集成"。听着高大上,其实就是:你每次提 PR,GitHub 帮你自动跑测试。
▼text复制代码你 push 代码 → GitHub 开一台虚拟机 → 跑 pytest → 绿了 ✅ 或者 红了 ❌
说白了就是一个比你更勤快的同事,每次你改了代码都帮你检查一遍。
GitHub Actions 又是啥?
就是 GitHub 内置的"自动化引擎"。你在仓库的 .github/workflows/ 目录下扔一个 YAML 文件,GitHub 就会在云端给你开一台机器,按你说的干活。
▼yaml复制代码on: push jobs: say-hello: runs-on: ubuntu-latest steps: - run: echo "hello world"
就这么简单。每次 push 代码,GitHub 就帮你打印一个 hello world。
Release 呢?
Release 就是 GitHub 上的"版本快照"。你觉得代码写得差不多了,就打个 Release,绑一个 Git tag(比如 v0.1.0),写几句变更说明。它本身不干啥,但它是触发 PyPI 自动发包的"开关"。
三者的关系
简单画个流程图:
▼text复制代码你提 PR 到 main │ ▼ GitHub Actions 自动跑测试 │ ├── 红了 ❌ → 回去改 bug │ ▼ 绿了 ✅ 合并到 main │ ▼ 在 GitHub 上打个 Release │ ▼ GitHub Actions 自动构建 + 上传 PyPI │ ▼ 别人可以 pip install 你的包了 🎉
第一步:把项目结构搞对
项目的目录长这样:
▼text复制代码my-project/ ├── .github/ │ └── workflows/ │ ├── ci.yml # 自动测试 │ └── release.yml # 自动发包 ├── my_project/ # 你的 Python 代码 │ ├── __init__.py │ └── ... ├── pyproject.toml # 项目的"身份证" └── ...
重点是 pyproject.toml,这玩意儿是整个发布流程的核心。我用的是 Poetry,配置长这样:
▼toml复制代码[project] name = "my-project" version = "0.1.0" description = "一个示例项目" authors = [{name = "Your Name", email = "you@example.com"}] license = {text = "MIT"} readme = "README.md" requires-python = ">=3.12,<4.0" dependencies = [ "requests>=2.31", ] [project.scripts] my-cli = "my_project.cli:main" [tool.poetry] packages = [{include = "my_project"}] [tool.poetry.group.dev.dependencies] pytest = ">=7.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api"
这里面有几个坑,我后面会专门讲。先照着抄,别自己发挥。
第二步:搭 CI(自动测试)
创建 .github/workflows/ci.yml:
▼yaml复制代码name: CI on: pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12", "3.13"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | pip install poetry poetry config virtualenvs.create false - name: Install dependencies run: poetry install --with dev - name: Run tests run: pytest -v || [ $? -eq 5 ]
几个值得注意的地方:
matrix:同时在 Python 3.12 和 3.13 上跑测试。GitHub 会开两台机器并行,确保两个版本都兼容。
virtualenvs.create false:GitHub Actions 的 runner 每次都是全新的,不需要再搞个虚拟环境,直接装到系统 Python 就行。
pytest -v || [ $? -eq 5 ]:pytest 退出码 5 表示"没有收集到任何测试"。项目刚开始没测试文件的时候,CI 不会因为这个红掉。等你写了真正的测试,该红还是会红。
提 PR 到 main 之后,去 Actions tab 就能看到结果了。绿了就 merge,红了就改。
第三步:配置 PyPI Trusted Publisher
什么是 Trusted Publisher?
以前发 PyPI 要手动生成 API Token,然后存到 GitHub Secrets 里。Trusted Publisher 是 PyPI 推出的新方式:用 OIDC 认证,不需要管 Token。
原理说人话就是:GitHub Actions 运行的时候,可以向 GitHub 证明"我确实是这个仓库的这个 Workflow",然后 PyPI 验证这个证明是否和你之前配置的一致。一致就放行。
怎么配?
PyPI 那边:去 https://pypi.org/manage/account/publishing/,在「添加新的待定发布者」里填:
| 字段 | 填啥 |
|---|---|
| PyPI project name | 你的包名,比如 my-project |
| Owner | GitHub 用户名 |
| Repository name | GitHub 仓库名 |
| Workflow name | release.yml |
| Environment name | pypi |
GitHub 那边:去仓库 → Settings → Environments → New environment,名字填 pypi,直接保存。
两边的名字必须一模一样,大小写都不能差。我当时 Environment name 填了 Pypi,结果报了个 invalid-publisher,排查花费了2.5根头发。
第四步:配置自动发包
创建 .github/workflows/release.yml:
▼yaml复制代码name: Release to PyPI on: release: types: [published] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install Poetry run: pip install poetry - name: Build package run: poetry build - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist/ publish-pypi: needs: build runs-on: ubuntu-latest environment: pypi permissions: id-token: write steps: - name: Download build artifacts uses: actions/download-artifact@v4 with: name: dist path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1
environment: pypi 和 id-token: write 是 OIDC 认证的关键,少了哪个都发不上去。
流程是这样的:你在 GitHub 上点"Create Release" → Actions 自动构建 .whl 和 .tar.gz → 用 OIDC 认证上传到 PyPI → 别人就能 pip install 了。
发版的完整流程
日常开发和发版,就这三步:
▼bash复制代码# 1. 改版本号(pyproject.toml 里的 version) # version = "0.2.0" # 2. 提交推送 git add pyproject.toml git commit -m "chore: bump version to 0.2.0" git push origin main # 3. 去 GitHub 打 Release(tag 填 v0.2.0,点 Publish)
然后就不用管了。Actions 会自动帮你构建、上传。去 PyPI 搜一下你的包名,新版本就在那了。
版本号推荐用语义化版本(SemVer):
▼text复制代码v 主版本 . 次版本 . 补丁版本 │ │ │ │ │ └─ 修了个 bug │ └─────────── 加了个新功能 └───────────────────── 改了 API,不兼容旧版
注意:PyPI 不让重复上传同一个版本号。你要是忘了改 version 就打 Release,Actions 会报 400 Bad Request。别问我怎么知道的。
踩坑实录(血泪教训)
坑 1:Group(s) not found: dev
CI 跑到 poetry install --with dev 就挂了,报错说找不到 dev 组。
原因是 dev 依赖写错了位置。Poetry 只认 [tool.poetry.group.dev.dependencies],不认 [project.optional-dependencies]。
▼toml复制代码# ❌ 这样写 Poetry 不认 [project.optional-dependencies] dev = ["pytest>=7.0"] # ✅ 要这样写 [tool.poetry.group.dev.dependencies] pytest = ">=7.0"
这俩长得差不多,但 Poetry 就是不认前者。属于"看起来对但就是不行"的那种坑。
坑 2:No file/folder found for package
把 PyPI 包名从 mochi-agent 改成了 mochi-assistant,结果构建时报错说找不到包。
原因是 Poetry 默认按包名找目录。包名 mochi-assistant,它就找 mochi_assistant/ 目录。但实际目录叫 mochi_agent/。
解决办法:要么改目录名(我选了这个),要么在 pyproject.toml 里显式指定:
▼toml复制代码[tool.poetry] packages = [{include = "mochi_agent"}]
坑 3:Poetry lock 报 Python 版本不兼容
requires-python = ">=3.12" 写得挺好,结果 poetry lock 报了一堆版本冲突。
原因:没写上界,Poetry 认为你的包支持 Python 4.0+。但 langchain-core 这些库声明了 python < 4.0,Poetry 发现"你的范围比它的大",就觉得不兼容。
加个上界就好了:
▼toml复制代码# ❌ 没上界 requires-python = ">=3.12" # ✅ 加上界 requires-python = ">=3.12,<4.0"
坑 4:Trusted Publisher 认证失败
报错 invalid-publisher: valid token, but no corresponding publisher。
意思是:GitHub Actions 确实拿到了一个 OIDC token,但 PyPI 那边找不到和它匹配的配置。
排查方法:看 Actions 日志里的 claims,逐项和 PyPI 上的配置对比:
▼text复制代码sub: repo:Owner/Repo:environment:pypi ← Environment 要对 repository: Owner/Repo ← 仓库名要对 workflow_ref: .../release.yml@refs/tags/v0.1.0 ← Workflow 名要对 environment: pypi ← 大小写要对
我当时的问题是 GitHub 上没创建 Environment。光在 PyPI 配了 Trusted Publisher,GitHub 那边也要建一个同名的 Environment 才行。
坑 5:pip install 时疯狂下载历史版本
装我的包时,pip 把 langgraph 从 1.2.7 一路下载到 0.6.x,装了十几分钟。
原因是 pyproject.toml 里写了 langgraph>=0.1.0,pip 的依赖解析器从最新版开始试,发现和已安装的 langchain 版本不兼容,就一个一个往回试。
解决办法:收紧依赖下限。当前用的是 1.2.7,就写 >=1.2.0,别写 >=0.1.0。
不用 Trusted Publisher 的话
如果你不想配 Trusted Publisher(或者要发到 TestPyPI),可以用 API Token:
- 去 https://pypi.org/manage/account/token/ 创建 token
- 去 GitHub 仓库 → Settings → Secrets → Actions → New secret
- Name:
PYPI_API_TOKEN - Value:
pypi-开头的那串
- Name:
release.yml的 publish 步骤改成:
▼yaml复制代码- name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }}
不过还是推荐 Trusted Publisher,不用管 token 过期的问题,配一次就行。
最后
整套流程搞下来,发现其实不复杂,就是 YAML 文件 + PyPI 配置 + GitHub Environment 三件套。难的是第一次配,各种小坑会把你绊住。
配好之后就很舒服了:写代码 → 提 PR → CI 自动测 → merge → 打 Release → 自动发包。全程不用碰 twine,也不用记密码。
希望这篇文章能帮你少踩几个坑。祝发包顺利 🚀
