0974d8dfa2
build / build (push) Successful in 57s
按已确认的契约实现(口径见 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)。
5.8 KiB
5.8 KiB
版本记录(全部 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 给历史提交重新编号,不给证据不足的提交编版本号。
- 不把「软件版本」与「知识发布批次」混在一起(发布批次在「发布管理」)。