From 06865939e14f6720643bfa7c36554b232d565b1c Mon Sep 17 00:00:00 2001 From: laily Date: Tue, 8 Sep 2026 14:30:49 +0800 Subject: [PATCH] fix(builder): bump test versions to next patch After a stable tag vX.Y.Z, untagged commits now produce X.Y.(Z+1)~date.branch.n+gSHA so test DEBs sort above the last official release and still below the next official X.Y.(Z+1). --- skills/builder/README.md | 23 +++++++++++-------- skills/builder/references/contract.md | 17 ++++++++------ skills/builder/references/registry.md | 2 +- skills/builder/scripts/version.sh | 13 +++++++++-- tests/test_builder_version.py | 33 +++++++++++++++++++++++---- 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/skills/builder/README.md b/skills/builder/README.md index 276b5c7..68e1841 100644 --- a/skills/builder/README.md +++ b/skills/builder/README.md @@ -84,23 +84,26 @@ Docker registry/ns/foo:1.4.2 ### 测试(其它任何 commit) -基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`),再加上构建日 -`YYYYMMDD`、清洗后的分支名、相对距离、短 SHA。测试与正式进**同一个** -apt / Docker 仓库,所以 DEB 必须用 `~`,保证测试包不会 `apt upgrade` -盖住正式包。日期写在 `~` 后面,这样 `1.4.2~20260908…` 仍小于 `1.4.2`。 +基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`)。有基线时测试号把 +patch 机械 +1,再加构建日 `YYYYMMDD`、清洗后的分支名、相对距离、短 SHA。 +这是排序键,不是在选定下一正式 SemVer。测试与正式进**同一个** apt / +Docker 仓库,所以 DEB 必须用 `~` 挂在下一 patch 上:`1.4.2` < +`1.4.3~20260908…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包; +同号正式版仍盖住测试。 分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`, 构建日 `20260908`: ```text -规范 / DEB 1.4.2~20260908.feat-login-v2.7+gabc1234 -文件名 foo_1.4.2~20260908.feat-login-v2.7+gabc1234_amd64.deb -Docker registry/ns/foo:1.4.2-20260908.feat-login-v2.7.gabc1234 +规范 / DEB 1.4.3~20260908.feat-login-v2.7+gabc1234 +文件名 foo_1.4.3~20260908.feat-login-v2.7+gabc1234_amd64.deb +Docker registry/ns/foo:1.4.3-20260908.feat-login-v2.7.gabc1234 ``` Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。 -安装测试包必须显式指定版本或完整 tag,不能靠无参 `apt upgrade`。 -构建日取本机当前日期;设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 日期。 +无参 `apt upgrade` 会从上一正式版升到下一 patch 的测试包,但不会盖住同号 +正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建日取本机当前日期; +设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 日期。 ### 分支名清洗 @@ -132,7 +135,7 @@ Docker 只有你点名镜像时才走。token 不会写进输出、提交或回 - 初始化:`check.py --ready` 的构建项 PASS。只缺发布键算部分完成,仍可构建、不能上传。 - 构建:产物路径、规范版本、架构与 `make -f makefile.builder version` 一致。 - 发布:HTTP 成功或远端 digest 对得上;回复里有包名或镜像引用、SHA-256 / digest、源 commit。 -- 测试包不会通过无参 `apt upgrade` 盖住正式包。 +- 测试包不会通过无参 `apt upgrade` 盖住同号正式包;会作为下一 patch 候选盖住上一正式包。 ## 脚本一览 diff --git a/skills/builder/references/contract.md b/skills/builder/references/contract.md index 78c84c0..8d99f48 100644 --- a/skills/builder/references/contract.md +++ b/skills/builder/references/contract.md @@ -64,8 +64,9 @@ make -f makefile.builder `make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的 `Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh` (`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用 -manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release 事先打好。 -`version.sh` 不执行 `git fetch`。 +manage-release 选定下一正式 SemVer。测试号把祖先基线的 patch 机械 +1 作为排序键 +(`v1.4.2` 之后 → `1.4.3~…`),不是在宣布下一正式号就是 1.4.3。正式 tag 由 +manage-release 事先打好。`version.sh` 不执行 `git fetch`。 推导前 `git fetch --tags`(本地 linked worktree 共享 tags,不必再 fetch 才 能看见其它 worktree 新打的 tag)。基线是 **HEAD 祖先上最近的稳定 tag**, @@ -74,13 +75,15 @@ manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release | 判定 | 规范版本 | DEB 文件 | Docker tag | | --- | --- | --- | --- | | HEAD exact-match 稳定 tag `v1.4.2` | `1.4.2` | `name_1.4.2_.deb` | `1.4.2` | -| 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.2~..+g` | `name_1.4.2~..+g_.deb` | `1.4.2-...g` | +| 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.3~..+g` | `name_1.4.3~..+g_.deb` | `1.4.3-...g` | | 祖先中没有稳定 tag | `0.0.0~.+g` | 同上替换规范版本 | 同上映射 | -测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~`,使 -`1.4.2~…` < `1.4.2`,`apt upgrade` 不会装上测试包。日期必须写在 `~` -之后,不能写成 `1.4.2.` / `1.4.2+`,否则测试包会大于正式 -包。Docker tag 不得含 `~`、`/`、`:`,由规范版本把 `~` → `-`、`+g` → `.g`。 +测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~` 挂在基线的下一 +patch 上:`1.4.2` < `1.4.3~…` < `1.4.3`。已装上一正式版的机器可以升到 +bugfix 测试包;同号正式版仍盖住测试。不要写成 `1.4.2~…`(小于已发布的 +`1.4.2`),不要写成 `1.4.2+` / `1.4.3-beta`(Debian 里 `-beta` 大于 +`1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范 +版本把 `~` → `-`、`+g` → `.g`。 ``:构建日 `YYYYMMDD`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC 日期,否则用本机当前日期。正式 `X.Y.Z` 不加日期。 diff --git a/skills/builder/references/registry.md b/skills/builder/references/registry.md index 8c48701..fe36418 100644 --- a/skills/builder/references/registry.md +++ b/skills/builder/references/registry.md @@ -35,7 +35,7 @@ ## Tag 策略 - 未显式给出 `--tag` / `IMAGE_TAG` 时,使用契约「版本号」渲染出的 tag - (正式 `1.4.2`,测试 `1.4.2-feat-login.7.gabc1234`),不要退回 + (正式 `1.4.2`,测试 `1.4.3-.feat-login.7.gabc1234`),不要退回 `git describe`,不要询问后另选一套。 - 正式产物 tag 与 Git tag `vX.Y.Z` 对应,但镜像 tag **不含** `v`;默认视为不可变。 - `latest`、`stable`、裸分支名等浮动 tag 只有在用户明确要求时才额外发布, diff --git a/skills/builder/scripts/version.sh b/skills/builder/scripts/version.sh index dbbc4b9..152ed6c 100755 --- a/skills/builder/scripts/version.sh +++ b/skills/builder/scripts/version.sh @@ -11,7 +11,7 @@ With --docker, print the Docker tag rendering of that version. Derivation (builder contract «版本号»): official HEAD exact-match of vX.Y.Z → X.Y.Z - test nearest ancestor stable tag → X.Y.Z~YYYYMMDD.branch.n+gSHA + test nearest ancestor stable tag vX.Y.Z → X.Y.(Z+1)~YYYYMMDD.branch.n+gSHA no tag no stable tag reachable from HEAD → 0.0.0~YYYYMMDD.branch+gSHA YYYYMMDD is the build date. SOURCE_DATE_EPOCH, if set, is that instant in @@ -62,6 +62,15 @@ is_official_version() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] } +# Mechanical sort key: next patch of a stable tag / X.Y.Z version. +# v1.2.0 → 1.2.1. Not a claim that the next official release is 1.2.1. +bump_patch() { + local v=$1 major minor patch + v=${v#v} + IFS=. read -r major minor patch <<<"$v" + printf '%s.%s.%s' "$major" "$minor" "$((10#$patch + 1))" +} + # Strip one leading v if present. strip_v() { local v=$1 @@ -163,7 +172,7 @@ derive_canonical() { return fi n=$(gitc rev-list --count "${base}..HEAD") - printf '%s~%s.%s.%s+g%s' "${base#v}" "$ymd" "$branch" "$n" "$sha" + printf '%s~%s.%s.%s+g%s' "$(bump_patch "$base")" "$ymd" "$branch" "$n" "$sha" } to_docker() { diff --git a/tests/test_builder_version.py b/tests/test_builder_version.py index 023af3b..4543635 100644 --- a/tests/test_builder_version.py +++ b/tests/test_builder_version.py @@ -91,6 +91,29 @@ class BuilderVersionTests(unittest.TestCase): self.assertEqual(docker.returncode, 0, docker.stderr) self.assertEqual(docker.stdout, "1.4.2\n") + def test_post_release_commit_bumps_patch(self) -> None: + with tempfile.TemporaryDirectory() as temp: + repo = Path(temp) / "repo" + repo.mkdir() + init_repo(repo) + (repo / "a").write_text("1\n", encoding="utf-8") + commit(repo, "release") + git(repo, "tag", "-a", "v1.2.0", "-m", "1.2.0") + (repo / "b").write_text("fix\n", encoding="utf-8") + commit(repo, "bugfix") + sha = sha7(repo) + result = version_sh(cwd=repo) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + result.stdout.strip(), + f"1.2.1~{BUILD_DATE}.main.1+g{sha}", + ) + docker = version_sh("--docker", cwd=repo) + self.assertEqual( + docker.stdout.strip(), + f"1.2.1-{BUILD_DATE}.main.1.g{sha}", + ) + def test_test_version_uses_ancestor_not_global_max(self) -> None: with tempfile.TemporaryDirectory() as temp: root = Path(temp) @@ -118,12 +141,12 @@ class BuilderVersionTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( result.stdout.strip(), - f"1.4.2~{BUILD_DATE}.feat-login-v2.2+g{feature_sha}", + f"1.4.3~{BUILD_DATE}.feat-login-v2.2+g{feature_sha}", ) docker = version_sh("--docker", cwd=repo) self.assertEqual( docker.stdout.strip(), - f"1.4.2-{BUILD_DATE}.feat-login-v2.2.g{feature_sha}", + f"1.4.3-{BUILD_DATE}.feat-login-v2.2.g{feature_sha}", ) def test_ignores_prerelease_and_app_tags(self) -> None: @@ -141,7 +164,7 @@ class BuilderVersionTests(unittest.TestCase): sha = sha7(repo) result = version_sh(cwd=repo) self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout.strip(), f"1.0.0~{BUILD_DATE}.main.1+g{sha}") + self.assertEqual(result.stdout.strip(), f"1.0.1~{BUILD_DATE}.main.1+g{sha}") def test_no_stable_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: @@ -169,7 +192,7 @@ class BuilderVersionTests(unittest.TestCase): git(repo, "checkout", "--detach", "HEAD") result = version_sh(cwd=repo, env={"BUILD_BRANCH": "ci_job"}) self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(result.stdout.strip(), f"0.1.0~{BUILD_DATE}.ci-job.1+g{sha}") + self.assertEqual(result.stdout.strip(), f"0.1.1~{BUILD_DATE}.ci-job.1+g{sha}") def test_source_date_epoch_changes_test_date(self) -> None: with tempfile.TemporaryDirectory() as temp: @@ -184,7 +207,7 @@ class BuilderVersionTests(unittest.TestCase): sha = sha7(repo) later = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1725148800"}) self.assertEqual(later.returncode, 0, later.stderr) - self.assertEqual(later.stdout.strip(), f"1.0.0~20240901.main.1+g{sha}") + self.assertEqual(later.stdout.strip(), f"1.0.1~20240901.main.1+g{sha}") def test_official_version_ignores_build_date(self) -> None: with tempfile.TemporaryDirectory() as temp: