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。
127 lines
5.2 KiB
Go
127 lines
5.2 KiB
Go
// 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=<CHANGELOG 顶部已发布版本,没有就空> \
|
||
// -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() 据此给出
|
||
// `<主>.<次>.<n>`(`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<ReleaseVersion>` 以来的提交数(字符串形式的整数);
|
||
// 由构建期注入(`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")
|
||
}
|