5f3a71cade
build / build (push) Successful in 26s
背景:本仓此前没有任何版本/构建元信息 —— 没有 ldflags 注入、也没有产物可读的 版本号,"线上跑的是哪个提交"答不出来;"这次改了什么、怎么验收"也没有载体。 落地: - CHANGELOG.md:**版本号的唯一来源**(正本 §6.1:版本号只准有一处)。刻意不放 VERSION 文件、也不靠 git tag —— CI 是 `git fetch --depth 50`,取不到 tag, `git describe` 会静默退化。每个已发布版本必须有两节:### 改了什么 与 ### 怎么验收。 - scripts/version.py:从 CHANGELOG 顶部解析版本号;解析不到即非 0 退出(构建据此失败)。 - scripts/check_changelog.py:结构自检(两节齐、带日期、版本号不重复且严格递减、 小节不为空),带 --self-test 反向自检。 - internal/version:三个构建期注入变量 + 契约测试(注入的号必须等于 CHANGELOG 顶部)。 - Makefile:build 带 -ldflags 注入;verify 增加 changelog-check 与 version-contract。 - Dockerfile:ARG VERSION/COMMIT_SHA/BUILD_TIME + ldflags(未透传时落 dev —— 故意显眼, 好过看起来正常的假版本号)。 - CI:新增「版本与变更记录契约」步,并显式断言契约测试真的 PASS(被跳过的断言等于没有断言)。 - docs/提交规范.md(ci_gate.py 的报错文案一直在指它)、docs/agents/traps.md(7 条实测陷阱)。 验证(都真跑过): - make version → 0.1.0;make changelog-check → 合规 - make version-contract → PASS;注入 9.9.9(CHANGELOG 里没有)→ FAIL(反向自检) - 删掉真实 CHANGELOG 的「怎么验收」小节 → 报红(对真实文件做,不只是夹具) - go build ./... 与 go vet ./internal/version/... 通过 未做(会与在审 PR #2 冲突,待其合并后在 main 上补): - GET /version 端点(internal/httpapi/server.go)与 cmd/api 的 -version 开关 - compose.yaml 透传 build args(当前由 compose 构建的镜像会报 dev) ⚠️ 未跑 make verify 全量(含 go test ./... 与前端),CI 是本次的测试证据。
61 lines
2.2 KiB
Python
Executable File
61 lines
2.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""打印版本号 —— 唯一来源是仓库根的 CHANGELOG.md。
|
||
|
||
为什么是它而不是 git tag:CI 检出是 `git fetch --depth 50`,**取不到 tag**,
|
||
`git describe --tags` 会失败或者给出误导性的结果。仓库里的文件则一定在。
|
||
|
||
为什么不是另放一个 `VERSION` 文件:那是第二份副本,发版要改两处,忘一处就漂移
|
||
(正本 §3.1)。CHANGELOG.md 里本来就必须写版本号,让它当源头,副本数为一。
|
||
|
||
用法:
|
||
python3 scripts/version.py # 打印版本号;解析不到就以非 0 退出
|
||
python3 scripts/version.py --check # 同时跑结构自检(等价 check-changelog.py)
|
||
|
||
退出码非 0 表示"CHANGELOG 里没有已发布版本条目" —— 构建应当据此失败,
|
||
而不是回落成一个看起来正常的假版本号。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
CHANGELOG = ROOT / "CHANGELOG.md"
|
||
|
||
# 已发布版本标题:`## [0.1.0] - 2026-10-10` / `## [0.1.0]`;`## [Unreleased]` 不算
|
||
RELEASE_HEADING = re.compile(r"^##\s*\[(\d+\.\d+\.\d+)\]", re.MULTILINE)
|
||
|
||
|
||
def current_version(path: Path = CHANGELOG) -> str | None:
|
||
"""取最上面那个已发布版本号;没有就返回 None。"""
|
||
if not path.is_file():
|
||
return None
|
||
m = RELEASE_HEADING.search(path.read_text(encoding="utf-8"))
|
||
return m.group(1) if m else None
|
||
|
||
|
||
def main(argv: list[str]) -> int:
|
||
v = current_version()
|
||
if v is None:
|
||
print(
|
||
"❌ CHANGELOG.md 里找不到已发布版本条目(形如 `## [0.1.0] - 2026-10-10`)。\n"
|
||
" 版本号解析不到 ⇒ 构建应当失败,而不是回落成假版本号。",
|
||
file=sys.stderr,
|
||
)
|
||
return 1
|
||
if "--check" in argv:
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from check_changelog import check # type: ignore # noqa: PLC0415
|
||
|
||
problems = check()
|
||
if problems:
|
||
for p in problems:
|
||
print(f"❌ {p}", file=sys.stderr)
|
||
return 1
|
||
print(v)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main(sys.argv[1:])) |