← 返回 Day 181 正文

Day 181 · 练习与清单

Day 181 · 练习与检查表

目标:亲手把「提交 → CHANGELOG → 版本号 → 构建 → 校验 → 上传」 这条链路跑通,并且亲眼看见三种坏情况被拦在门外。 前置:Python 3.9+(本机 3.12.3)。四个脚本纯标准库即可自检; 只有「官方 build 前端 + twine check」两步需要 python3 -m pip install --target /tmp/day181/.deps build twine。


一、动手题(必做)

练习 1 — 把版本号排成一队(5 分钟)

bash
cd days/day-181-release-management
python3 code/01-semver.py --self-test
python3 code/01-semver.py --sort 1.0.0 1.0.0-alpha 1.0.0-beta.2 1.0.0-beta.11 1.0.0-rc.1 0.9.9
参考答案 排序:`0.9.9 → 1.0.0-alpha → 1.0.0-beta.2 → 1.0.0-beta.11 → 1.0.0-rc.1 → 1.0.0` `beta.2 < beta.11`:数字段按**数值**比较(2 < 11),不是字典序。 `+build` 不参与优先级比较,两者**相等**(返回 0)。

练习 2 — 从假提交日志推出下一个版本(10 分钟)

bash
printf '%s\n' \
 'a1b2c3d feat(api): 新增分页参数' \
 '9f8e7d6 fix(api): 修正越界读取' \
 '7d6c5b4 docs: 补充说明' > /tmp/day181.log
python3 code/02-changelog.py --from /tmp/day181.log --current 1.1.4 --dry-run
参考答案 `1.2.0`(有 `feat`、无 breaking → MINOR +1)。 `docs` 属于内部改动,默认被跳过(`跳过 1 条`); 加 `--include-internal` 会让它落到 Changed。

练习 3 — 让「破坏性变更」改变结论(10 分钟)

bash
printf '%s\n' \
 'a1b2c3d feat(api): 新增分页参数' \
 '8b7a6f5 refactor(cli)!: 重命名 --output 为 --format' \
 '' \
 'BREAKING CHANGE: --output 已移除' > /tmp/day181b.log
python3 code/02-changelog.py --from /tmp/day181b.log --current 1.1.4 --dry-run
参考答案 变成 `2.0.0`(有 breaking → MAJOR +1)。 BREAKING 区一条:`重命名 --output 为 --format`。 **不会变**——正文里的 `BREAKING CHANGE:` 与 `!` 等价,两者任一成立即为 breaking。 (可以再说一次:这就是为什么两种写法都必须认。)

练习 4 — 真机构建一次,并核对三件事(15 分钟)

bash
mkdir -p /tmp/day181 && python3 -m pip install --quiet --target /tmp/day181/.deps build twine
python3 code/03-build-verify.py --self-test
参考答案 `demo_release_pkg-1.2.3-py3-none-any.whl`(本机实测 1458 字节)与 `demo_release_pkg-1.2.3.tar.gz`(实测 1210–1227 字节,**每次不同**, 因为 tar 写入打包时刻)。 wheel 内含 5 个文件;METADATA 里 `Name: demo-release-pkg`、 `Version: 1.2.3`。 改名成 9.9.9 后报 **2 个问题**(文件名版本与元数据不一致、wheel/sdist 版本不一致)—— 这证明校验器不是安慰剂。

练习 5 — 端到端跑一遍,看清哪一步在拦人(15 分钟,重点)

bash
python3 code/04-release-pipeline.py --self-test
参考答案 第 2 步(对版本)。拦下时**连构建都没做**——实测输出里那之后直接是下一条反向验证, 没有 `[4/6]`。 没有 token → 拒绝上传并返回失败,是 **fail closed**(宁可明确失败,也不发半成品)。 桩如果不算摘要,只会无条件回 200;那「篡改摘要必须被发现」这条断言就永远通过—— **一个自我麻醉的空测试**(本日翻车复盘第 5 条就是这个问题的真实版本)。

练习 6 — 真实索引只能验到边界(10 分钟)

bash
python3 - <<'PY'
import json, urllib.request
with urllib.request.urlopen('https://test.pypi.org/pypi/sampleproject/json', timeout=15) as r:
    d = json.load(r)
print(r.status, d['info']['name'], d['info']['version'])
for f in d['urls']:
    print(f['filename'], f['size'], f['digests']['sha256'][:16])
PY
参考答案 它们是**发布后校验**的依据:拿本地产物算 sha256,与索引公布的值逐字节比对, 就能确认中间没有损坏或错版。 本日没真传,是因为需要一个有效 token(真实凭据),且发出去就是公开且不可逆的—— 所以只实测到「假 token 被 403 拒绝」这个边界,成功上传标注为**未实测**, 并写清了原因、预期结果、验证方法(README 实验 6)。

二、判断题(对/错 + 一句话理由)

答案 1. ✗ 构建元数据不参与比较,两者优先级相等。 2. ✗ 预发布转正是 `1.2.3`(丢弃预发布段,patch 不再 +1)。 3. ✗ 它只检查元数据与 README 渲染;检查不到「包能不能 import」、 「依赖是否写全」、「包数据是否漏打」等。 4. ✗ 内部改动对使用者无意义,默认应过滤掉。 5. ✗ 用户名固定 `__token__`,密码才是 token;否则 403。 6. ✗ PyPI 不允许覆盖同名文件,只能发新版本(或 yank 旧的)。 7. ✗ **先怀疑校验器**——本日第 1 条翻车就是校验器自己的正则错了, 让所有合法产物都变红。

三、排障演练

给三个现象,说出「先查哪一步 / 哪个命令」:

现象 你的第一步
pip install 报 ModuleNotFoundError
twine upload 返回 403
索引上的版本与 git tag 不一致
参考答案 1. 先 `python3 -m zipfile -l dist/*.whl` 看包里到底有没有那个模块—— 多半是 `packages.find` 的 `where` 配错(src 布局常见)。 2. 检查 token:是不是设了 `DAY181_INDEX_TOKEN`、用户名是不是 `__token__`、 token 的 scope 是否覆盖这个项目名。本日实验 6 实测过:假 token 就是 403。 3. 跑流水线第 2 步的比对(`tag 版本 vs pyproject version`), 再确认 tag 打在了正确的提交上。

四、验收清单(做完打勾)


五、延伸(选做)

  1. 给 02 加一条 --commit-msg 检查:模拟 commit-msg 钩子, 非规范提交直接拒绝(退出码非 0),并想清楚为什么这一步放在本地比放在 CI 更省事。
  2. 用 setuptools_scm 把版本改成单一数据源,然后故意让 tag 与包内版本 不一致,看流水线第 2 步是否还能拦住(提示:这一改动会让该事故在结构上消失, 请想清楚「变安全」和「少了一个检查」之间的差别)。
  3. 把 03 的校验加一条「wheel 里必须包含 README 声明的 license 文件」, 并先构造一个缺文件的 wheel 验证这条新检查真的会红。