Repository navigation
Conversation
- 按加号后缀区分预发布、正式构建和热修复版本,仅纯版本创建 patch 升版 PR - 可选调用 GitHub 原生接口生成默认分支发布说明并冻结结果供续跑复用 - 保持两站类型及补发回执一致,补充发布规则和回归检查
🌿 Roseau API Breaking Change Report
|
Gugle2308
left a comment
There was a problem hiding this comment.
本地实跑验证:19 项回归测试全部通过(10 项既有 + 9 项新增,0.115s)。版本规则和发布类型判断与文档表格完全一致,但 generate-notes 没有传 previous_tag_name,连续多个资质版本的说明可能重叠;另外,+build.N、+hotfix.N 会取代 GitHub 的 Latest 标记,这与“Latest 由正式版维护”的表述需要确认意图。
审查范围:base dev/26.1 → head 046fa4da,10 文件 +281/−19(patch-diff 与 API 声明逐位一致)、draft=false、mergeable=true、state=open、无既有 review。
✅ 一、实跑结果:19 项全过(我把 head 的 6 个脚本抽出来本地执行)
Ran 19 tests in 0.115s — OK
10 项既有测试(test_release.py)+ 9 项新增(test_release_options.py)全部通过,说明本次改动没有破坏既有的封存、续传、Central 回执、两站重传语义。
✅ 二、逐项核对结论
| 检查点 | 结论 |
|---|---|
| 版本正则 7 组合法形式 | 2.0.0 / +alpha.1 / +beta.2 / +pre-release.3 / +rc.4 / +build.5 / +hotfix.6 全部接受,且 mod_version() 与 release_type() 结论一致 |
| 版本正则 8 组非法形式 | -beta.1(连字符)、+alpha(缺 N)、+rc.01(前导零)、+beta.-1、+snapshot.5、+unknown.1、+build.1+rc.2(双后缀)、+hotfix.1/other(路径穿越)全部拒绝 ✅ |
| 类型映射 | alpha→alpha、beta/pre-release/rc→beta、build/hotfix/无限定→release,与 README 三列表格逐行一致 |
| 升 patch 范围 | 仅纯 X.Y.Z 升级;release.py:47 生成 bump_patch,release.yml:178 用该输出做 job 级开关,bump() 内再拦一次 if "+" in version(双保险,测试验证 github/contents 均未被调用) |
| 两站类型同源 | Modrinth version_type 与 CurseForge releaseType 复用同一个 kind;测试断言 CurseForge 的三次上传(主包 + sources + javadoc)releaseType 全一致 ✅ |
| Modrinth 幂等对账 | 远端已是 beta 时直接复用不回传;类型不符报 different release metadata ✅ |
| 续跑不重复生成 | prepare() 命中 source-plan 时不再调 generate-notes(测试断言 github.call_count) ✅ |
| 封存幂等 | 连续两次 seal() 只产生 1 个 tag + 1 个 draft,正文中生成说明只出现 1 次(不重复拼接) ✅ |
| 类型漂移防护 | seal()/publish() 均校验 release.prerelease,不匹配直接报 Conflicting release type,且测试验证此时不发任何 PATCH ✅ |
| Central 门槛 | 七个 release/<tag>/<line>/central 全部 success 才解除草稿,与文档一致 ✅ |
| CI 接入 | build_and_test.yml 新增 discover -s .github/release;pull_request.yml 新增 .github/release/** 与两个 workflow 路径 ⇒ 本 PR 自身也会跑这些测试 ✅ |
⚠️ 三、generate-notes 未传 previous_tag_name:连续资质版本的说明会重叠
# release.py:41
plan["release_notes"] = github("releases/generate-notes", {
"tag_name": plan["tag"], "target_commitish": targets[ANCHOR]["sha"],
})["body"]只传了 tag_name + target_commitish,"上一个 tag"完全交给 GitHub 自动挑选。PR 只预览了 v2.0.0+beta.1 一次(此时上一个 tag 是普通版,结论正确),但连续两版资质版本没有实测:
v2.0.0+beta.1→v2.0.0+rc.1→v2.0.0三连发时,若 GitHub 因+后缀或 prerelease 属性跳过上一版 tag,rc.1的说明会把 beta 阶段的 PR 再列一遍;正式版又会把 beta+rc 全列一遍。- 该接口本身支持
previous_tag_name,成本极低。
建议:显式传 "previous_tag_name": <上一个已发布 tag>(可从 releases 列表取最新非 draft),或补一次"连续两版"的实测再合。
⚠️ 四、+build.N / +hotfix.N 会抢 GitHub Latest,请确认意图
# release.py:137 / 145
prerelease = release_type(plan["version"]) != "release" # build/hotfix → False
...
"make_latest": "false" if prerelease else "true"而 PR 表格把 X.Y.Z+build.N、X.Y.Z+hotfix.N 归为 Release 类型,所以它们会以 make_latest: "true" 发布 ⇒ 把 v2.0.0 从 Releases 页的 Latest 位置挤下去。
RELEASING.md 写的是「Pre-release 使用独立 tag,GitHub 的 Latest 标记由正式版维护」。若"正式版"指 X.Y.Z,此处与代码不符;若指"Release 类型",则文档措辞需要更明确。另外 +build.N(同版本重建)夺走 Latest 在产品意义上值得商榷,而 +hotfix.N 夺走 Latest 更合理。
建议:若意图是"只有纯 X.Y.Z 维护 Latest",改成 prerelease = "+" in plan["version"](但注意这同时会把 build/hotfix 变成 GitHub Pre-release,与表格冲突)——更稳的做法是拆成两个独立判断:prerelease(GitHub)与 take_latest,并在文档里写清 build/hotfix 的 Latest 归属。
⚠️ 五、资质版本跳过 bump ⇒ 分支停留在已发布版本,内部快照与 Central 同号
+ 版本一律跳过升版(设计如此),但七条分支的 mod_version 会继续停留在已发布版本,而:
// build.gradle:12
version = mod_version
// ci.gradle:39(内部快照发布)
version: main.version.toString()即内部 Maven 快照会以 2.0.0+beta.1 这一已在 Central 发布的坐标继续产出。RELEASING.md 只写了「后续发 2.0.0+rc.1 或 2.0.0 时,再将七条分支同步到对应版本」,没有要求"发完立即同步"。
建议:在 RELEASING.md 明确"qualified 版本发布后应立即把七条分支同步到下一个版本"(否则该窗口内的内部快照与正式版同号),或让 bump() 对 qualified 版本改为升后缀序号(beta.1 → beta.2)而非整体跳过。
💡 六、其余建议
- 最小权限回退:
prepare现在无条件contents: write(release.yml:32),而 notes 默认关闭。GitHub 不支持条件permissions,可考虑把generate-notes拆到单独的 job(带 write)后再回传,或至少在 workflow 顶部注明"默认关闭时该权限仍被授予"。 release_url()未编码 tag(mod_sites.py:117-118):return f"https://github.com/{repo()}/releases/tag/{plan['tag']}"
+首次进入 tag 后,Modrinth/CurseForge 的 changelog 链接依赖服务端把路径里的+当字面量。建议quote(plan['tag'], safe=''),或直接复用 release 对象的html_url(内部已有该字段)。- 双空行:
release.py:88在 notes 后固定追加"",若 GitHub 返回内容本身以换行结尾,说明与校验表之间会出现两个空行(仅外观)。 - 封存后无法再开启 notes:
seal()复用草稿时不刷新 body,而prepare()有「tag 属于哪个 run」检查 ⇒ 首次 seal 完成后,该版本只能删 tag/release 重来才能补生成说明。建议在 RELEASING.md 写一句,或让seal()在existing且plan["release_notes"]非空时对 body 做 PATCH。
验证边界
脚本从 head 抽出后在本地 Python 3.11.2 实跑(unittest discover),未联网、未调用 GitHub API;工作流 YAML 仅做静态阅读,未在 Actions 中实际执行。PR 描述中"用实际 GitHub 接口预览 v2.0.0+beta.1"与"2.0.0+rc.1 实际 stageRelease 构建 22 组坐标"两项我无法独立复核,请以作者提供的运行记录为准;generate-notes 的 previous-tag 行为建议由作者补一次连续两版实测。
发布总控现在按七条分支一致的
mod_version识别版本类型,并支持可选的默认分支发布说明。X.Y.Z+alpha.N、X.Y.Z+beta.N、X.Y.Z+pre-release.N、X.Y.Z+rc.NX.Y.Z+build.N、X.Y.Z+hotfix.NX.Y.Zalpha在两站上架为 Alpha,beta、pre-release和rc上架为 Beta,正式类型上架为 Release。补发和回执核对使用同一规则。全部类型均等待七个 Central 部署成功后公开 GitHub Release,纯版本的升版 PR 继续使用[skip ci]。新的
generate_release_notes开关默认关闭。开启后调用 GitHub 原生releases/generate-notes,以锁定的dev/26.1SHA 生成内容,正文保留 GitHub 的 PR、贡献者和完整变更链接,并附构件校验表。生成结果写入发布计划和 tag 注释,续跑复用。预检任务因此需要 GitHub 对此接口要求的contents: write权限。本地 19 项发布回归检查、2 项 CI 发布检查和 Actions 语法检查通过。用实际 GitHub 接口预览了
v2.0.0+beta.1的说明,包含默认分支 PR,七种类型、跳过升版和预发布补发均有覆盖。发布脚本的回归检查也接入了本 PR 的 CI。另以
2.0.0+rc.1完成了stageRelease实际构建,22 组 Maven 发布坐标、主包内 21 个模块、源码和 Javadoc 附件均通过构件校验,完整的加号后缀得到保留。