手把手教你: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
OwnerGitHub 用户名
Repository nameGitHub 仓库名
Workflow namerelease.yml
Environment namepypi

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: pypiid-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:

  1. https://pypi.org/manage/account/token/ 创建 token
  2. 去 GitHub 仓库 → Settings → Secrets → Actions → New secret
    • Name: PYPI_API_TOKEN
    • Value: pypi- 开头的那串
  3. 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,也不用记密码。

希望这篇文章能帮你少踩几个坑。祝发包顺利 🚀

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP