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

5.8 KiB
Raw Blame History

版本记录(全部 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 成功不等于已部署。

四、配置(匿名只读,不使用凭据)

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(契约测试会核对)。

六、同步触发

初版只有管理员手动同步(页面上「同步记录」按钮,仅管理员可见);不默认新增定时调度。

七、验证

# 单元与规则(含分页去重、对账失败、不可达保留旧快照、后补 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 给历史提交重新编号,不给证据不足的提交编版本号。
  • 不把「软件版本」与「知识发布批次」混在一起(发布批次在「发布管理」)。