// Package version 承载"这个二进制是什么"的构建期身份。 // // 字段由构建期注入(正本 §6.1:版本号只有一个来源,且要能从产物读回来): // // VERSION=$(python3 scripts/version.py 2>/dev/null || echo dev) // go build -trimpath -ldflags "\ // -X knowledge-governance/internal/version.Number=${VERSION} \ // -X knowledge-governance/internal/version.ReleaseVersion= \ // -X knowledge-governance/internal/version.BuildKind=development|release \ // -X knowledge-governance/internal/version.CommitSHA=$(git rev-parse HEAD) \ // -X knowledge-governance/internal/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" ./cmd/api // // 五条纪律: // // 1. **不要**把它们改成编译期常量,也**不要**另建仓库根的 VERSION 文件 —— // 版本号的唯一来源是 CHANGELOG.md,由 scripts/version.py 解析。 // 2. **不要**给这些变量设"看起来正常的默认值"。未注入时保持零值, // 好让"忘了注入"表现成一个显眼的 dev,而不是一个假的正经版本号。 // 3. **开发构建不许自称正式版**:`BuildKind` 只能来自显式的发布上下文 // (`BUILD_KIND=release`);普通 main CI 永远是 development, // DisplayVersion() 必须带 `-dev+g<短SHA>`,**不允许只显示上一正式版本号**。 // 4. **开发号随提交递增**:`CommitsSinceRelease` 由构建期注入,DisplayVersion() 据此给出 // `<主>.<次>.`(`0.1.12`);没注入就退回 `-dev+g<短SHA>` —— 宁可没号,也不报错号。 // 5. 契约由本包的测试兜底:发布构建的 ReleaseVersion 必须等于 CHANGELOG.md 顶部的 // 已发布版本,开发构建必须是 dev 形状;CI 带 -ldflags 跑它并断言**没有 SKIP** // (被跳过的断言等于没有断言)。 package version import ( "fmt" "regexp" "strconv" "strings" ) // Number / ReleaseVersion / BuildKind / CommitSHA / BuildTime 由 -ldflags -X 注入; // 未注入时保持下面的值。 var ( Number = "dev" // 已发布版本号或 dev(= ReleaseVersion,或 dev) ReleaseVersion = "" // 正式版本基线;还没有正式发布过 ⇒ 空 BuildKind = "development" // development | release CommitSHA = "unknown" // 完整 40 位提交 SHA BuildTime = "unknown" // 构建时间(built_at) // CommitsSinceRelease:自 `v` 以来的提交数(字符串形式的整数); // 由构建期注入(`scripts/version.py --identity` 的 commits_since_release)。 // 空 / 非整数 ⇒ 开发号算不出来,退回 `<版本>-dev+g<短SHA>`(**不猜号**)。 CommitsSinceRelease = "" ) var intRe = regexp.MustCompile(`^\d+$`) // commitsCount 解析注入的提交数;没注入 / 不是整数 ⇒ ok=false。 func commitsCount() (int, bool) { if !intRe.MatchString(CommitsSinceRelease) { return 0, false } n, err := strconv.Atoi(CommitsSinceRelease) if err != nil { return 0, false } return n, true } var ( fullSHA = regexp.MustCompile(`^[0-9a-f]{40}$`) semver = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$`) ) // IsRelease 只在**显式声明**且确实有正式版本基线时为真。 func IsRelease() bool { return BuildKind == "release" && semver.MatchString(ReleaseVersion) } // ShortSHA 返回 12 位短 SHA;没有注入有效 SHA 时返回空串。 func ShortSHA() string { if !fullSHA.MatchString(CommitSHA) { return "" } return CommitSHA[:12] } // DisplayVersion 是**用户可见标识**,规则与 scripts/version.py 一致: // // 发布构建 → 0.1.0 // 开发构建(有基线 + 计数) → **<主>.<次>.<自 v<版本> 以来的提交数>**,如 0.1.12(随提交递增) // 开发构建(恰在发布提交上)→ 就是那个正式版本号(提交数 0) // 开发构建(无基线/无计数)→ 0.1.0-dev+g<短SHA> / dev+g<短SHA> // 什么都拿不到 → dev // // ⚠️ 开发构建**不会**退化成"只显示上一正式版本号"——那正是要修的问题(有提交在 tag 之后就必须带自己的号)。 func DisplayVersion() string { if IsRelease() { return ReleaseVersion } short := ShortSHA() if n, ok := commitsCount(); ok && semver.MatchString(ReleaseVersion) { if n == 0 { return ReleaseVersion // HEAD 恰在发布提交上 } parts := strings.Split(ReleaseVersion, ".") return fmt.Sprintf("%s.%s.%d", parts[0], parts[1], n) } switch { case ReleaseVersion != "" && short != "": return ReleaseVersion + "-dev+g" + short case ReleaseVersion != "": return ReleaseVersion + "-dev" case short != "": return "dev+g" + short default: return "dev" } } // Info 是给人看的一行摘要(不含密钥,可安全打印进日志)。 func Info() string { sha := CommitSHA if len(sha) > 12 { sha = sha[:12] } return fmt.Sprintf("%s(%s,提交 %s,构建于 %s)", DisplayVersion(), BuildKind, sha, BuildTime) } // HasInjection 判断构建期是否真的注入了身份:未注入时契约测试应当 Skip, // 而 CI 必须断言它**没有** Skip。 func HasInjection() bool { return !(Number == "dev" && ReleaseVersion == "" && CommitSHA == "unknown") }