6de5d52363
build / build (push) Successful in 51s
按使用者要求定的版本规则(自动升的是**开发标识**,发布版本与 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。
175 lines
7.0 KiB
Go
175 lines
7.0 KiB
Go
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)
|
||
}
|
||
}
|