Files
knowledge-governance/internal/version/version.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

127 lines
5.2 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 承载"这个二进制是什么"的构建期身份。
//
// 字段由构建期注入(正本 §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")
}