Files
knowledge-governance/docs/VERSION_HISTORY.md
T
姚章浩 0974d8dfa2
build / build (push) Successful in 57s
feat(version-history): 「版本记录」页 —— 追踪远端 main 全部提交(只读 + 管理员手动同步)
按已确认的契约实现(口径见 docs/VERSION_HISTORY.md):

- **版本号口径(只给有证据的)**:注释 tag + tag 指向该提交 + 该提交内 CHANGELOG 顶部三者一致 ⇒ 正式版本;
  tag 之后的提交按 `<主>.<次>.<自该 tag 的提交数>` 显示开发号(与 scripts/version.py **同规则**);
  证据不足 ⇒ 「历史提交 · 短SHA」+ 原因码,**不补造版本号**。完整 SHA 是唯一身份、版本号不是唯一键
  (tag 之后两条并行分支各自的第 1 个提交都算 0.1.1 ⇒ 两条都保留,用短 SHA 区分)。
- **迁移 000046**:version_history_commits(提交记录,主键 repo_key+commit_sha)+ version_history_sync_runs
  (同步状态)。快照 = 一次同步:抓完并与 X-Total-Count 对账通过才切换;失败不切换快照、不清空历史、
  last_success_at 不动(页面显示"上次同步失败(原因)· 仍展示上次成功数据");「同步成功」≠「历史完整」。
- **只读客户端(匿名,不使用任何凭据)**:白名单 origin + 固定仓库、拒绝任意 URL 与跨主机重定向、
  响应上限 5MB / 超时 15s、仅 5xx/429 退避重试(最多 3 次);逐页抓到短页为止;每次全量扫 tag
  ⇒ 旧提交后补的 tag 会更新既有记录(不新增第二行)。
- **接口**(三个都登记进 api/openapi.yaml):GET /api/v1/build-info(viewer)、
  GET /api/v1/version-history(viewer;游标绑定快照,过期返回 409 cursor_expired;不返回伪造 total)、
  POST /api/v1/version-history/sync(admin;并发同步复用同一作业,靠 IdempotencyKey)。
- **「当前运行」只认精确匹配**:产物注入的完整 SHA 命中某条记录才标该条;开发构建显式标注"开发构建";
  没有可用标识 ⇒ "当前运行版本未知";**不拿最新 main / 最新 tag 顶替**,CI 成功不等于已部署。
- **前端**:一级菜单第 5 项(版本记录)+ 时间线页面(全部/正式/开发/历史四段筛选、正文展开、
  加载更多、760px 单列无横向溢出、提交正文与变更记录按纯文本渲染);无打 tag / 发布 / 部署入口。

新增配置(留空即未接线,页面降级):VERSION_HISTORY_GITEA_BASE_URL / _REPO / _ALLOWED_ORIGINS /
_HTTP_TIMEOUT / _MAX_PAGES。

验证:`make verify` 15/15 success(tests-go 578 通过 / 0 失败;tests-web 209 通过 / 0 失败);
新增 Go 用例 30(版本口径与图遍历 12、客户端守卫 6、同步编排 5、接口 7);前端新增 18(组件 10 + 客户端 8);
真实仓库匿名快照拉取(`VERSION_HISTORY_LIVE_BASE_URL=… go test -run TestLiveSyncAgainstRealRepository`):
抓到 113 条与 X-Total-Count 一致、v0.1.0 是注释 tag 且目标与 `git rev-parse v0.1.0^{commit}` 一致、
开发号逐个等于 `git rev-list --count v0.1.0..<sha>`(正式 1 / 开发 22 / 历史 90)。
2026-10-13 19:40:19 +08:00

80 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 版本记录(全部 main 提交时间线)
「版本记录」是平台一级菜单的只读页面:追踪**远端 main 可达的每一个提交**(含合并提交与已并入 main
的侧分支提交),按提交时间倒序展示,并标注它对应哪一版、有没有正式 tag、是不是当前正在跑的那份产物。
## 一、版本号口径(只给有证据的号)
| 判定 | 条件(全部可机器核验) | 页面显示 |
|---|---|---|
| **正式版本** | ① 有 tag `vX.Y.Z` 指向该提交;② 该 tag 是**注释 tag**(Gitea `/git/refs/tags` 的 `object.type == "tag"`);③ 该提交内 `CHANGELOG.md` 顶部版本 == `X.Y.Z` | `0.1.0` + `[正式版本]`,正文取该提交内的变更记录 |
| **开发版本** | 该提交内 CHANGELOG 顶部有版本号,且存在 `v<该版本>` tag、能算出「自该 tag 以来的提交数」n | `<主>.<次>.<n>` + `[开发版本]`(与 `scripts/version.py` **同规则**:n=0 时等于版本号本身) |
| **历史提交** | 证据不足(轻量 tag / tag 与提交内记录不一致 / 找不到基线 / 提交图不完整) | `历史提交 · <短SHA>` + 具体原因,**不补造版本号** |
- **完整 SHA 是唯一身份**,版本号不是唯一键:两个提交算出同一个号(例如 tag 之后两个并行分支各自第 1 个提交都是 `0.1.1`)时**两条都保留**,用短 SHA 区分。
- 提交数按提交图算(等价 `git rev-list --count <tag>..<sha>`),**不执行历史提交里的任何脚本**;提交图不完整(检出被截断)时拒绝给号,原因码 `shallow_or_incomplete_graph`。
## 二、数据与同步
- 两张表(迁移 `000046_version_history`):`version_history_commits`(提交记录,主键 `repo_key + commit_sha`)、`version_history_sync_runs`(同步状态)。
- **快照 = 一次同步**(`snapshot_id = sync_runs.id`):抓取期间旧快照继续对外可见;抓完并**与 `X-Total-Count` 对账通过**后才成为事实。失败**不切换快照**、不清空历史,`last_success_at` 保持不动 ⇒ 页面显示"上次同步失败(原因)· 仍展示上次成功的数据(时间)"。
- **同步成功 ≠ 历史完整**:`complete=false` 时页面显示"部分历史(N/M)"。
- 抓取方式:`GET /repos/{owner}/{repo}/commits?limit=50&page=N` 逐页到短页为止(本仓 113 条 = 3 页);**每次同步都全量扫 tag**,因此"旧提交后补的 tag"会更新到既有记录(不新增第二行)。
- 游标绑定快照(`base64(snapshot_id|committed_at|sha)`),排序 `committed_at DESC, commit_sha DESC` ⇒ 翻页确定性、不重不漏;游标指向的快照被清理后返回 `409 cursor_expired`,页面提示刷新。
- 只保留最近 **3** 个成功快照供游标使用(`VersionHistoryKeptSnapshots`)。
## 三、当前运行(只认精确匹配)
- `GET /api/v1/build-info` 回运行产物由 `-ldflags -X` 注入的身份:`release_version / build_kind / commit_sha(完整 40 位)/ display_version / built_at / identity_available`。
- 页面判据:完整 SHA **精确命中**某条记录 ⇒ 标该条;`build_kind=development` 且命中不了 ⇒ 顶部显示"开发构建 `<号>`(短SHA)",**不标任何条目**;没有可用标识 ⇒ "当前运行版本未知"。
- **绝不**用"最新 main"或"最新 tag"顶替当前运行;**CI 成功不等于已部署**。
## 四、配置(匿名只读,不使用凭据)
```text
VERSION_HISTORY_GITEA_BASE_URL=http://192.168.50.223:3000 # 留空 = 未接线(页面降级,同步按钮不可用)
VERSION_HISTORY_REPO=Weknora/knowledge-governance # 固定 owner/name
VERSION_HISTORY_ALLOWED_ORIGINS= # 逗号分隔白名单;留空 = 只允许上面那个 origin
VERSION_HISTORY_HTTP_TIMEOUT=15s
VERSION_HISTORY_MAX_PAGES=200
```
安全边界:来源必须落在白名单 origin 与固定仓库内(**拒绝任意 URL**)、拒绝跨主机重定向、单响应大小上限 5MB、
超时 15s、仅 5xx/429 退避重试(最多 3 次);**请求不携带任何凭据**;提交正文与变更记录一律按**纯文本**渲染。
## 五、接口与权限
| 接口 | 最小角色 | 说明 |
|---|---|---|
| `GET /api/v1/build-info` | viewer | 当前运行产物的构建身份(只回身份字段) |
| `GET /api/v1/version-history` | viewer | 游标分页列表;带 `sync` 状态与 `current_running` 判定 |
| `POST /api/v1/version-history/sync` | **admin** | 手动同步一次;并发同步**复用同一个作业**(`IdempotencyKey`) |
三个接口都登记在 `api/openapi.yaml`(契约测试会核对)。
## 六、同步触发
初版只有**管理员手动同步**(页面上「同步记录」按钮,仅管理员可见);**不默认新增定时调度**。
## 七、验证
```bash
# 单元与规则(含分页去重、对账失败、不可达保留旧快照、后补 tag 升级、同号不同 SHA)
go test ./internal/versionhistory/ ./internal/governance/ ./internal/httpapi/ -count=1
# 真实仓库交叉核对(**匿名**;未设变量即按设计跳过,理由在 scripts/test-scope.json)
VERSION_HISTORY_LIVE_BASE_URL=http://192.168.50.223:3000 \
go test ./internal/versionhistory/ -run TestLiveSyncAgainstRealRepository -count=1 -v
# 全量门禁
make verify
```
真实仓库交叉核对会断言:抓到条数 == `X-Total-Count`、SHA 不重复、`vX.Y.Z` 是注释 tag 且目标与
`git rev-parse vX.Y.Z^{commit}` 一致、开发号与本地 `git rev-list --count vX.Y.Z..<sha>` 逐个相等。
## 八、不做什么
- 不提供打 tag / 发布 / 部署入口;同步不改远端任何东西(只读 GET)。
- 不用最新 tag 给历史提交重新编号,不给证据不足的提交编版本号。
- 不把「软件版本」与「知识发布批次」混在一起(发布批次在「发布管理」)。