Files
knowledge-governance/internal/version/version_test.go
T
t 6de5d52363
build / build (push) Successful in 51s
feat(version): 开发号随提交自动递增 + 首个正式版本 0.1.0
按使用者要求定的版本规则(自动升的是**开发标识**,发布版本与 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

175 lines
7.0 KiB
Go
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.
package version
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)
// changelogTopVersion 返回仓库根 CHANGELOG.md 顶部的已发布版本号;还没有正式版本时返回 ""。
func changelogTopVersion(t *testing.T) string {
t.Helper()
path := filepath.Join("..", "..", "CHANGELOG.md")
raw, err := os.ReadFile(path)
if err != nil {
t.Fatalf("读不到 %s:%v(版本号的唯一来源就在这里)", path, err)
}
m := regexp.MustCompile(`(?m)^##\s*\[(\d+\.\d+\.\d+)\]`).FindSubmatch(raw)
if m == nil {
return ""
}
return string(m[1])
}
// 版本契约:注入进二进制的**正式版本基线**,必须等于仓库根 CHANGELOG.md 顶部的已发布版本。
//
// 为什么要有这条断言:CHANGELOG.md 是版本号的**唯一来源**,而注入是构建期动作。
// 两者只要没人对账,就会出现"改了 CHANGELOG 没重新构建"(产物报旧号)或
// "构建时手填了一个号"(产物报的号 CHANGELOG 里根本没有)—— 两种都让
// "线上跑的是哪个提交/版本"这个问题失去答案,而症状是静默的。
//
// ⚠️ "还没有正式版本"不等于出错:那时 ReleaseVersion 必须是空,**不许**编一个基线出来。
//
// 未注入时(普通 `go test ./...`)本用例 Skip:本地跑全量测试不必先构建。
// 但 CI 必须带 -ldflags 跑它,**并断言没有 SKIP**(被跳过的断言等于没有断言)。
func TestNumberMatchesChangelog(t *testing.T) {
if !HasInjection() {
t.Skip("构建身份未注入(没有 -ldflags);CI 会注入后跑这条断言,并断言它没有 Skip")
}
want := changelogTopVersion(t)
if want == "" {
if ReleaseVersion != "" {
t.Fatalf("CHANGELOG.md 里还没有已发布版本,ReleaseVersion 却是 %q —— 不许编一个版本基线出来", ReleaseVersion)
}
} else if ReleaseVersion != want {
t.Fatalf("注入的 ReleaseVersion %q != CHANGELOG 顶部 %q。"+
"常见原因:改了 CHANGELOG 却没重新构建(产物还报旧号),"+
"或者构建时手填了一个 CHANGELOG 里没有的号", ReleaseVersion, want)
}
if Number != ReleaseVersion && Number != "dev" {
t.Fatalf("Number %q 既不是 ReleaseVersion %q 也不是 dev —— 注入口径不一致", Number, ReleaseVersion)
}
}
// 开发构建不许自称正式版:这是"用正式版号代表开发版"那个问题的守门断言。
//
// - `BuildKind=release` 只允许出现在**确实有正式版本基线**的构建上,且显示就是那个版本;
// - 开发构建的 DisplayVersion 必须带 `dev` 形状,**不允许**退化成只显示上一正式版本号。
func TestDevBuildDoesNotClaimRelease(t *testing.T) {
if !HasInjection() {
t.Skip("构建身份未注入(没有 -ldflags);CI 会带 -ldflags 跑它,并断言它没有 Skip")
}
want := changelogTopVersion(t)
disp := DisplayVersion()
if BuildKind == "release" {
if want == "" {
t.Fatalf("标成了发布构建(BuildKind=release),但 CHANGELOG.md 里没有任何已发布版本条目 —— "+
"发布构建必须基于正式版本记录(现在显示 %q)", disp)
}
if ReleaseVersion != want || !semver.MatchString(ReleaseVersion) {
t.Fatalf("发布构建的 ReleaseVersion %q 不是 CHANGELOG 顶部的正式版本 %q", ReleaseVersion, want)
}
if disp != ReleaseVersion {
t.Fatalf("发布构建 DisplayVersion %q 应等于 ReleaseVersion %q", disp, ReleaseVersion)
}
return
}
if BuildKind != "development" {
t.Fatalf("BuildKind %q 既不是 development 也不是 release", BuildKind)
}
if strings.Contains(disp, "+g") && ShortSHA() == "" {
t.Fatalf("DisplayVersion %q 带了 +g 但 CommitSHA %q 不是有效 SHA", disp, CommitSHA)
}
if want == "" {
// 还没有正式基线 ⇒ 只能是 dev 形状
if ShortSHA() != "" && disp != "dev+g"+ShortSHA() {
t.Fatalf("无正式基线时 DisplayVersion %q,期望 dev+g%s", disp, ShortSHA())
}
return
}
// 有正式基线:开发号 = <主>.<次>.<自 v<版本> 以来的提交数>;浅仓库/取不到 tag ⇒ 回退 dev 形状
n, ok := commitsSinceTag(want)
if !ok {
if disp != want+"-dev+g"+ShortSHA() {
t.Fatalf("取不到 tag 时 DisplayVersion %q,期望 %s-dev+g%s", disp, want, ShortSHA())
}
return
}
parts := strings.Split(want, ".")
exp := want
if n > 0 {
exp = fmt.Sprintf("%s.%s.%d", parts[0], parts[1], n)
}
if disp != exp {
t.Fatalf("开发版本号 %q,期望 %q(= <主>.<次>.<自 v%s 以来的提交数 %d>)", disp, exp, want, n)
}
// 交叉核对:注入进来的提交数必须真的等于 git 算出的(防注入错号)
if injected, ok2 := commitsCount(); ok2 && injected != n {
t.Fatalf("注入的 CommitsSinceRelease=%d 与 git 算出的 %d 不一致", injected, n)
}
if disp != want {
// 不许"只显示上一正式版本号":有提交在 tag 之后,就必须带自己的号
if n == 0 {
t.Fatalf("tag 之后还有提交却仍显示正式版本号:%q", disp)
}
}
}
// commitsSinceTag 用 git 算「自 v<版本> 以来的提交数」;浅仓库 / 取不到 tag ⇒ ok=false(不许猜)。
func commitsSinceTag(ver string) (int, bool) {
if out, err := exec.Command("git", "rev-parse", "--is-shallow-repository").Output(); err == nil &&
strings.TrimSpace(string(out)) == "true" {
return 0, false
}
out, err := exec.Command("git", "rev-list", "--count", "v"+ver+"..HEAD").Output()
if err != nil {
return 0, false
}
n, convErr := strconv.Atoi(strings.TrimSpace(string(out)))
if convErr != nil {
return 0, false
}
return n, true
}
// 正式构建必须**有证据**:完整提交 SHA + 一个指向该提交的**注释 tag** `v<ReleaseVersion>`。
// 光有"CHANGELOG 顶部有版本号"不算 —— 那会把每个开发构建都说成正式版。
func TestReleaseBuildRequiresEvidence(t *testing.T) {
if !HasInjection() {
t.Skip("构建身份未注入(没有 -ldflags);CI 会带 -ldflags 跑它,并断言它没有 Skip")
}
if BuildKind != "release" {
return // 开发构建由 TestDevBuildDoesNotClaimRelease 管
}
if !fullSHA.MatchString(CommitSHA) {
t.Fatalf("标成了正式构建,但 CommitSHA=%q 不是完整 40 位 —— 没有准确提交就谈不上正式版", CommitSHA)
}
name := "v" + ReleaseVersion
out, err := exec.Command("git", "tag", "--points-at", CommitSHA).Output()
if err != nil {
t.Fatalf("跑 `git tag --points-at %s` 失败:%v(正式构建必须能核到 tag 证据)", CommitSHA[:12], err)
}
tags := strings.Fields(string(out))
found := false
for _, x := range tags {
if x == name {
found = true
}
}
if !found {
t.Fatalf("标成了正式构建(%s),但提交 %s 上没有 tag %s(现有指向该提交的 tag:%v)—— "+
"正式构建必须有指向该提交的合规 tag", ReleaseVersion, CommitSHA[:12], name, tags)
}
kind, err := exec.Command("git", "cat-file", "-t", "refs/tags/"+name).Output()
if err != nil || strings.TrimSpace(string(kind)) != "tag" {
t.Fatalf("tag %s 不是**注释 tag**(`git tag -a`)—— 轻量 tag 不算发布证据", name)
}
}