windows-python-release · git:20261027.0f9d1cd · 2026-10-27 · sha256 5f0b284743950f6b

windows-python-release git:20261027.0f9d1cdA

Immutable. This exact content never changes and is served at /api/v1/blob/5f0b284743950f6b.

---
name: windows-python-release
description: 当要手动发布 Python 包(codewiki-plus)到 PyPI 并建 GitHub Release 时——CI 不发布、Windows 控制台为 GBK、可能无 gh CLI、本机网络对 upload.pypi.org 的 Python TLS 握手有干扰——先核对 tag 区间与工作区未提交改动定 bump 级别,再按 8 步主干执行:同步四处版本→bump 提交(只 add 具体文件)→聚焦+ruff+全量闸门→清 dist 构建→PR 合并 main→tag merge commit 建 Release→uv publish(TLS 超时则 curl.exe 直传)→PyPI JSON API 核对
type: Skill
status: stable
generated:
  by: codewiki/5.8.0
  at: "2026-09-08T06:04:10Z"
stale_after: 2027-01-24
metadata:
  summary: 手动发布 7 步主干 SOP + 每步避坑注解
  source_refs: ["wiki/scenarios/发布与依赖治理方法.md", "notes/2026-09-10-codewiki-手动发布完整流程同步四处版本测试build清distpublishrelease核对.md", "notes/2026-10-26-发版时-tag-区间无新提交发布内容可能在未提交的工作区改动里提交只-add-具体文件.md"]
  revisions: ["at: \"2026-09-08T06:04:10Z\"", {"at": "2026-09-10T02:02:25Z", "reason": "按主干优先重构:原 SOP 8 步中 6 步是避坑,缺发布主流程;改为 7 步主干(每步带判定点),避坑降级为步骤注解。来源为新增的流程型笔记。", "source": "skill_creator"}, {"at": "2026-09-23T04:05:06Z", "reason": "v5.13.0 发布实操回写:①新增步骤 5「PR 合并 main」(check-runs 轮询,非 commit status);②tag 位置统一为 PR merge commit(main),废弃 v5.12.0 打在 develop bump commit 的旧做法;③步骤 7 补 TLS 握手干扰归因与 curl.exe(schannel)直传 legacy API 绕法,digest 字段名 sha256_digest;④核对端点补版本直查规避 JSON API 缓存延迟。来源:任务「发版本」v5.13.0 发布会话(2026-09-23)。", "source": "skill_creator"}, {"at": "2026-10-26T08:00:29Z", "reason": "v5.16.2/5.16.3 发布实操回写:①新增步骤 0「判定发布内容与 bump 级别」——tag 区间无新提交 ≠ 无发布内容,先查工作区未提交改动;②步骤 2 补提交纪律——只 add 具体文件、fix 与 bump 分 commit、git add -A 会扫入无关未跟踪文件;③步骤 3 闸门细化为聚焦测试→ruff(CI 同口径)→全量;④步骤 6 补 Release API target_commitish 远端自动打 tag 的做法。来源:notes/2026-10-26-发版时-tag-区间无新提交发布内容可能在未提交的工作区改动里提交只-add-具体文件.md(任务「发版本」v5.16.2/5.16.3 会话)。", "source": "skill_creator"}]
  reason: created from candidate materials
  source: skill_creator
  installed_at: "2026-10-26T11:01:51Z"
  installed_to: .codebuddy/skills/windows-python-release/
  installed_hash: "sha256:8d681945e85c6a784325343f0b5b55c172343c100ce7f69149e37cf24c11633e"
---


## 工作场景

手动发布 Python 包(`codewiki-plus`)到 PyPI 并创建 GitHub Release。**CI 只跑测试与 lint,不负责构建与发布**(`.github/workflows/ci.yml:13-74`),因此发布主干没有脚本承载,必须按序手工执行。Windows 控制台默认 GBK,环境可能无 gh CLI。

## 适用条件

- 发布是手动流程,仓库无发布脚本
- 提交信息或 Release 正文含中文/emoji
- 环境无 gh CLI / 未配置 GH_TOKEN,但 git credential manager 已保存凭据

## 核心 SOP

按序执行 8 步,**每步有判定点,不通过就停,不要带着问题往下走**。

0. **判定发布内容与 bump 级别**:`git log <上一tag>..HEAD --oneline` 看提交,**同时 `git status --short` + `git diff --stat` 核对工作区未提交改动**——tag 区间无新提交 ≠ 没有发布内容,发布内容可能全在未提交的工作区里(v5.16.2 实例:log 为空但 fix 改动全在工作区)。据实际内容定 patch/minor bump。
   判定点:发布内容清单明确,bump 级别有依据。

1. **同步四处版本引用**:`pyproject.toml`(`version`)、`codewiki/__init__.py`(`__version__`)、`uv.lock`(用 `uv lock` 刷新,勿手改)、`codewiki/mcp/server.py`(`version=__version__` 注入,勿手写常量)。CLI 与 actor id 也从 `__version__` 读取,自动跟随。
   判定点:四处版本字符串完全一致。

2. **提交 bump 并推送 develop**:消息 `chore: bump version to X`;推 `origin develop`。
   判定点:`git show` 确认版本改动齐全。
   ⚠️ 中文提交信息用 `git commit -F <utf8 文件>`,**不要用 `-m`**(GBK 破坏,乱码或拆词)。
   ⚠️ **只 add 具体文件**:fix 改动与版本 bump 分两个 commit;工作区混着的 repowiki 产物等非发布文件不入库。`git add -A` 会扫入未跟踪的 `.scratch/`、草稿文档等无关文件(v5.16.3 实例:误暂存 157 文件 8 万行),用 `git add -u` + 显式路径精确暂存。

3. **发布闸门(按序)**:聚焦测试(改动模块,秒级)→ `ruff check` / `ruff format --check`(本次变更的 py 文件,与 CI 增量 lint 同口径)→ 全量 `pytest -q`。
   判定点:无失败用例,无 lint 错误。
   ⚠️ 全量 `ruff format --check .` 会报 repowiki/conversations/*.md 内嵌代码块——不在 CI Lint 范围,按 CI 同口径逐文件验证即可。

4. **构建并清理产物**:`uv build`;发布前**清 `dist/`** 或只精确指定本次产物。
   判定点:`dist/` 下只剩本次版本的 wheel 与 sdist。
   ⚠️ 不清 `dist/` 会连带上传旧产物(5.6.0 发布时残留 6 个)。

5. **合并 main**:建 PR develop→main(无 gh CLI 时用 Python `subprocess` 调 `git credential fill` 取凭据再调 REST API,请求体显式 UTF-8);轮询 head sha 的 **check-runs**(不是 commit status)等 CI 绿后合并。
   判定点:PR merged,main 含 bump commit。

6. **打 tag 并建 GitHub Release**:lightweight tag `vX.Y.Z` 打在 **PR merge commit 上(main 分支)**——Release 应反映 main 的事实状态;不要打在 develop 的 bump commit 上(v5.12.0 旧做法,与 Release 所属分支不一致)。Release 正文经 REST API 创建,显式 UTF-8;也可用 Release API 的 `target_commitish` 让远端自动打 tag。
   ⚠️ Release 正文乱码**不可逆**(UTF-8 被按 GBK 解码写入,字符永久丢失),只能基于 `git log` 事实重写。
   ⚠️ 给 `git credential fill` 喂 stdin 不用 PowerShell 管道(stdin 常为空、引号被破坏),用 `subprocess` 精确传字节;凭据只留进程内。

7. **发布到 PyPI**:优先 `uv publish dist/codewiki_plus-<version>*`(精确指定,默认传 dist/ 全部)。
   **token 来源:Windows 凭据管理器(keyring)**——`keyring.get_password('pypi', 'upload')`,首次配置:`keyring.set_password('pypi', 'upload', '<token>')`。**不要让用户把 token 粘贴到对话里**(本仓库 4 次泄露事故全部源于此),也不要写进仓库内任何文件(gitignore 不是安全边界,`.codebuddy/` 是被跟踪目录)。
   ⚠️ **本机网络对 upload.pypi.org 的 Python OpenSSL TLS 握手会被干扰**:uv publish / twine / requests 全部连接超时(TCP 通、curl 秒通)。绕法:curl.exe(schannel 栈)直传 legacy API——Python 从 whl 的 `*.dist-info/METADATA`、sdist 的 `PKG-INFO` 提取元数据构造完整表单(`:action=file_upload`、`protocol_version=1`、`filetype`、`pyversion` + 全部元数据字段 + `description` 正文),写 curl `-K` 配置文件(`form-string`,命令行会超长),`-u __token__:<token>` POST。**digest 字段名是 `sha256_digest`**(不是 `digests_sha256`,400 报 missing digest)。token 经参数传进程内,用完删临时文件。
   ⚠️ **email.parser 提取元数据时 `Message.keys()` 对重复头返回 N 次**(如 4 个 `Project-URL`),循环内再 `get_all(key)` 会产生 N×N 重复字段(v5.13.1 时 project_urls 重复 16 条被 400 拒绝)。遍历前必须对 key 去重。

8. **核对真实上传**:`https://pypi.org/pypi/codewiki-plus/json` 查最新版本(JSON API 有缓存延迟,可直接查 `/pypi/codewiki-plus/<version>/json`)。
   判定点:API 返回版本 == 目标版本,双产物 size 正确。

## 判断逻辑

- **主干优先**:先有完整流程,避坑是各步的注解,不是流程的替代品。缺任何一步(尤其是 3 闸门与 8 核对)都不算发布完成。
- **tag 区间无新提交 ≠ 无事可发**:发布内容可能全在未提交的工作区改动里,先看 `git status` 再下结论(依据: notes/2026-10-26-发版时-tag-区间无新提交发布内容可能在未提交的工作区改动里提交只-add-具体文件.md 的「tag 区间无提交容易被误判为无事可发」)。
- 乱码/拆词/stdin 失效同源:都是 cmd/PowerShell 中间层破坏参数与编码。统一绕法是「落文件 / 精确 subprocess」,不是加转义。
- TLS 握手超时先做栈归因:TCP 通 + curl(schannel)通 + Python requests 超时 → 是 OpenSSL 栈被网络层干扰,换传输栈(curl.exe)而不是加超时重试。
- PyPI 只增不改会幂等跳过已存在文件,但这是侥幸,不是安全网。

## 禁忌与反模式

- 不要 `git commit -m "中文"`
- 不要 `git add -A` 一把梭——只 add 具体文件,fix 与 bump 分 commit
- 不要不清 `dist/` 就直接 `uv publish`
- 不要跳过闸门(步骤 3)或发布后核对(步骤 8)
- 不要用 PowerShell 管道或 `cmd /c echo` 给 `git credential fill` 喂 stdin
- 不要试图反向还原 GBK 乱码文本(字符已丢失,只能重写)
- PyPI 上传超时不要盲目加超时重试——先区分是读取慢(加超时有用)还是 TLS 握手被掐(换 curl.exe)
- 不要把 PyPI token 写进任何落盘文件后不清理
- 不要把 PyPI token 存进仓库内文件(含 gitignore 的文件)——统一走 keyring(Windows 凭据管理器)
- 不要让用户把 token 粘贴到对话里——对话采集 hook 会把它原样写进 repowiki/conversations/ 并随提交推送

依据: notes/2026-09-10-codewiki-手动发布完整流程同步四处版本测试build清distpublishrelease核对.md 的「7 步主干与每步判定点」;wiki/scenarios/发布与依赖治理方法.md 的「dist 残留被一并上传」「Release 正文乱码不可逆」「Windows 参数与 stdin 编码统一绕法」;任务「发版本」v5.13.0 发布实操(2026-09-23):TLS 握手干扰归因与 curl.exe 绕法、sha256_digest 字段名、tag 打在 merge commit 的约定;notes/2026-10-26-发版时-tag-区间无新提交发布内容可能在未提交的工作区改动里提交只-add-具体文件.md 的「tag 区间无提交先查工作区」「只 add 具体文件、fix 与 bump 分 commit」「闸门顺序聚焦→ruff→全量」