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)。
80 lines
5.8 KiB
Markdown
80 lines
5.8 KiB
Markdown
# 版本记录(全部 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 给历史提交重新编号,不给证据不足的提交编版本号。
|
||
- 不把「软件版本」与「知识发布批次」混在一起(发布批次在「发布管理」)。 |