5 Commits

Author SHA1 Message Date
t 950c1b429b feat(release): 抬档门禁(破坏性 ⇒ 0.x 抬次 / ≥1.0.0 抬主)+ 1.0.0 前置(稳定契约承诺)
原来预检只"打印破坏性条数",抬哪一档靠人自觉 ⇒ 改成硬门禁:

- **抬档门禁**:破坏性条数取自**目标提交里候选版本那一段**(发版事实看提交,不看工作区)。
  `0.x` 阶段有破坏性 ⇒ 必须抬**次版本**(或主版本),只抬修订 ⇒ 阻断并报出该抬到哪一档;
  `≥1.0.0` 之后有破坏性 ⇒ 必须抬**主版本**,抬次/修订都阻断。
  另加"候选号必须大于上一个正式版本"(版本只增不减)。
- **1.0.0 前置**:上一版本是 `0.x` 时,候选 `1.0.0` = 对外承诺契约稳定 ⇒ 必须先有写实的
  《稳定契约承诺》(覆盖 接口 / 数据模型 / 配置 / CLI 输出与退出码,不留 `待填`/`TODO`/`TBD`),
  **且候选发布记录里链接它**,缺任一条 ⇒ 阻断;`1.x → 2.0.0` 不重复要求。
- 预检新增"版本走向"事实行(上一版本 → 候选 / 抬哪一档 / 破坏性 N 条 ⇒ 要求哪一档 ✅⛔);
  `docs/RELEASE_RUNBOOK.md` 新增"抬哪一档"对照表与《稳定契约承诺》段(故意留占位)。

验证:release_preflight --self-test 32 分支全过(含新增 9 条);反向夹具新增 §24 六条真实临时仓库用例
(0.x 只抬修订 ⇒ 阻断、0.x 抬次 ⇒ 放行、≥1.0.0 抬次 ⇒ 阻断、1.0.0 留占位 ⇒ 阻断、写实+链接 ⇒ 放行、
写实但没链接 ⇒ 仍阻断)⇒ 夹具 91/0;make verify 15/15。
于是 0.1.0 之后的第一个提交(本提交)开发号 = 0.1.1。
2026-10-13 17:13:44 +08:00
t 6de5d52363 feat(version): 开发号随提交自动递增 + 首个正式版本 0.1.0
build / build (push) Successful in 51s
按使用者要求定的版本规则(自动升的是**开发标识**,发布版本与 tag 仍由人批准):

- **开发号自动递增**:display_version(development) = `<正式版本的主.次>.<自 v<正式版本> 以来的提交数>`
  (正式 0.1.0 之后第 1 个提交 0.1.1、第 12 个 0.1.12;恰在发布提交上 = 正式号)。
  它**不写进 CHANGELOG**(发布记录只在发版时动),由 git 算出来 ⇒ 每个提交都有自己的号、数字只增。
- **浅克隆不猜号**:`--depth` 会把条数算少 ⇒ 浅仓库里拒绝算号,退回 `<版本>-dev+g<短SHA>`;
  检出步骤在仓库有 tag 时 `--unshallow` 补全历史。
- 接线三处齐全:`version.py --identity` → 门禁 `-X …CommitsSinceRelease=<n>`;Makefile 的
  `COMMITS_SINCE_RELEASE`;Dockerfile 同名 build-arg;Go 侧新增 `CommitsSinceRelease` 与
  DisplayVersion() 新规则;契约测试**交叉核对**"注入的提交数 == git 算出的"(防注入错号)。
- **首个正式版本 0.1.0**:CHANGELOG 的 `[Unreleased]` 提成 `## [0.1.0] - 2026-10-13`;
  头部补"版本怎么走"两行;RELEASE_RUNBOOK 补递增规则与"下一次发布把次版本加一"。
- 发版预检新增:打印**当前开发号**,并挡住"候选正式版本 <= 当前开发号"(会把开发号当正式号;建议次版本加一)。

验证:version.py --self-test(含 tag 后第 2 个提交 = 0.2.2、恰在 tag 上 = 0.2.0、无 tag 不猜号);
release_preflight --self-test(21 分支,含开发号两条);go test 本地三态 + 临时仓库 tag+2 提交实测
(Python 与 Go 都得出 0.2.2);反向夹具 85/0;make verify 15/15 success。
2026-10-13 16:45:56 +08:00
姚章浩 97f4b29661 fix(version): 就绪看目标提交的顶部版本 + 禁用自动发布路径
build / build (push) Successful in 52s
补两处残余(本轮不推、不打真实 tag、不部署;全部用临时仓库与 spy 验):

1) version_in_commit 必须比"目标提交 CHANGELOG 的**顶部**已发布版本",不是"历史里出现过":
   - 新增 top_version_in_commit(),version_in_commit() 直接比它;预检输出把该提交的顶部版本打出来
     (诊断可审计:例如"顶部是 `## [0.3.0]`,不是 0.2.0"),阻断信息里也带上真实顶部版本。
   - 顺带兜住"最新在最上面 / 版本只增不减"的排序要求。
   - 负例(自检 + 夹具各一遍):顶部 0.3.0、历史里 0.2.0 ⇒ 候选 0.2.0 拒绝;候选 0.3.0 通过。

2) 按只读约定**禁用 --execute 自动发布路径**:
   - 删掉写路径(打 tag / 推 tag / 建 Release 的代码整段移除),`--execute` / `--approve-version` /
     `--approve-sha` 只保留参数兼容(不报 unknown argument),传了就**明确拒绝**(rc=2)。
   - 拒绝发生在收集事实之前 ⇒ **零写操作、零 API 调用**;自检用 spy 断言拒绝路径
     "一个子进程都没跑、一个 API 都没发",并扫描生产路径不许出现直接执行 git 写命令的行。
   - 输出里也不再推荐自动路径;RELEASE_RUNBOOK 第 4 步改为由人执行。

验证:release_preflight --self-test 绿(就绪 20 分支 + 顶部/历史两态 + --execute 拒绝零写 +
源码写路径扫描);反向夹具 84/0(含 --execute rc=2 且跑完仓里 tag 不变);make verify 15/15 success。
2026-10-13 15:48:50 +08:00
姚章浩 7a9d51cc70 fix(version): 正式构建核证据 + 预检分"建议/就绪" + CI 就绪按必需 context
按要求补三项(本轮不推、不打真实 tag、不部署;全部用临时仓库与假对象验):

1) 正式构建身份不能只认 --release:
   - 新增 release_evidence():核①完整 40 位 SHA ②**指向该提交的注释 tag** v<版本号>
     ③该提交里 CHANGELOG 顶部版本一致。缺任一条 ⇒ rc=2,身份退回 development 并把
     release_blocked 的原因写进 JSON(给机器读,也给人看)。
   - version.py --self-test:六条负例(有版本无 tag / unknown SHA / 短 SHA / tag 指错提交 /
     轻量 tag / 版本不一致)+ 证据齐正例,**tag 全部在 mktemp 临时仓库里创建**。
   - Go 侧新增 TestReleaseBuildRequiresEvidence:release 构建必须 CommitSHA 完整且
     v<ReleaseVersion> 是**注释 tag** 并指向该提交(独立于 Python 的二次复核)。

2) 发布预检区分"准备建议"与"发布就绪":
   - 新增纯函数 readiness(facts) → (是否就绪, 阻塞项, 建议);输出分两段。
   - **就绪要求候选版本已写进目标提交**(version_in_commit:查 git show <sha>:CHANGELOG.md,
     工作区写了没提交不算);首发按**显式指定版本**处理,**不把"记录里有这个版本"当已发布**
     (已发布的证据是 tag,不是记录条目)。
   - 预检**不执行、也不在输出里推荐自动发布路径**(正式发版由人按 docs/RELEASE_RUNBOOK.md)。

3) CI 就绪判据:必需 context 集合来自 .gitea/required-contexts.txt(缺失/为空 ⇒ 没有策略 ⇒
   阻断,不用观察到的状态顶替);**全部 success 才通过**,失败/缺项/pending/查询异常一律阻断。
   Release 查询加 release_state():**只有 HTTP 404 才算不存在**,403/5xx/超时算查询异常 ⇒ 阻断。

验证:version.py --self-test 绿;release_preflight --self-test 绿(就绪判定 19 个分支 +
临时仓库验证 git show 语义 + 五个 HTTP 码语义);go test 三态(未注入 SKIP / 开发 PASS /
自称正式 RED);反向夹具 83/0(含临时仓库 go test 六例、预检"记录未进提交"两态);
make verify 15/15 success。
2026-10-13 15:26:49 +08:00
姚章浩 4f8ee0a8c5 feat(version): 变更记录改固定四类,破坏性变化带发布文档链接
按项目方给的规则重做 CHANGELOG 与检查器(本轮不推、不打 tag、不部署):

- 条目形状:四个自定义必填节(改了什么/兼容性与迁移影响/怎么验收/已知问题)
  → 固定四类 Added/Changed/Fixed/Security;分类可省略,**不许空栏目**;每个版本块
  至少一个非空分类;`[Unreleased]` 段不得写日期。
- 破坏性变化:固定写在 Changed 里、以 `**破坏性**` 开头,**必须链接发布文档**;
  仓内 `docs/….md` 链接**对账**(不存在 ⇒ 红)。写在别的分类里也判红。
- 结构判据补强:非法日期、重复版本、顺序必须严格递减、前导零;围栏代码块里的
  `##`/`###` 不算标题;**块正文以任意二级标题为界**(「## 能力基线(未发布)」这类
  不带方括号的标题原先不是边界,会把后文算成上一节的内容 ⇒ 空栏目被兜成"有内容",
  夹具真的抓到了这个假绿)。
- tag 一致性:远端专属清单 `.ci-remote-tags` → `ls-remote` → 本地 refs 三级来源;
  **没核对远端就不许说校验成功**(CI/预检用 `--require-remote`,拿不到清单即红),
  非正式 tag(如本地 `x`)不当作发布版本、不判红,没有 tag 不判红。
- `0.1.0` 定为**未发布的能力基线**(远端无 tag、无 Release):改成不带方括号的
  「## 能力基线(未发布)」,不补造 tag/发布日期/发布事实。
- 过程记录迁出:审查轮次与命令输出 → `docs/archive/CI_接入留档.md`(原文保留);
  未合并事项 → `docs/BACKLOG.md`;补 `docs/RELEASE_RUNBOOK.md`(破坏性变化的链接目标,
  同时修掉 CHANGELOG 里指向不存在文档的悬空引用)。
- 发版预检:无版本基线时把「首个正式版本号由人指定」列成阻塞项(不再要求先有基线才能跑);
  打印破坏性变化条数;远端 tag 查不到时明说未核对并阻塞;不再硬读已删除的『已知问题』节。
- traps trap-5 口径修正:不是"不用 tag",而是"构建时不要用 git describe 取版本号"。

验证:check_changelog --self-test 全过;反仓夹具 80/0(含五条真改文件的结构负例、
六条只在临时仓库里造 tag 的场景);make verify 15/15 success。
2026-10-13 14:59:35 +08:00