From 852dee3a694d4a507869f21beefb6e6e95aca948 Mon Sep 17 00:00:00 2001 From: laily Date: Tue, 8 Sep 2026 10:59:17 +0800 Subject: [PATCH] feat(builder): append YYYYMMDD to test versions --- skills/builder/README.md | 17 ++++++----- skills/builder/references/contract.md | 13 +++++---- skills/builder/scripts/version.sh | 24 +++++++++++---- tests/test_builder_version.py | 42 +++++++++++++++++++++++---- 4 files changed, 74 insertions(+), 22 deletions(-) diff --git a/skills/builder/README.md b/skills/builder/README.md index 9ef85ef..276b5c7 100644 --- a/skills/builder/README.md +++ b/skills/builder/README.md @@ -84,20 +84,23 @@ Docker registry/ns/foo:1.4.2 ### 测试(其它任何 commit) -基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`),再加上清洗后的 -分支名、相对距离、短 SHA。测试与正式进**同一个** apt / Docker 仓库, -所以 DEB 必须用 `~`,保证测试包不会 `apt upgrade` 盖住正式包。 +基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`),再加上构建日 +`YYYYMMDD`、清洗后的分支名、相对距离、短 SHA。测试与正式进**同一个** +apt / Docker 仓库,所以 DEB 必须用 `~`,保证测试包不会 `apt upgrade` +盖住正式包。日期写在 `~` 后面,这样 `1.4.2~20260908…` 仍小于 `1.4.2`。 -分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`: +分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`, +构建日 `20260908`: ```text -规范 / DEB 1.4.2~feat-login-v2.7+gabc1234 -文件名 foo_1.4.2~feat-login-v2.7+gabc1234_amd64.deb -Docker registry/ns/foo:1.4.2-feat-login-v2.7.gabc1234 +规范 / 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 ``` Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。 安装测试包必须显式指定版本或完整 tag,不能靠无参 `apt upgrade`。 +构建日取本机当前日期;设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 日期。 ### 分支名清洗 diff --git a/skills/builder/references/contract.md b/skills/builder/references/contract.md index 1aaf74d..78c84c0 100644 --- a/skills/builder/references/contract.md +++ b/skills/builder/references/contract.md @@ -74,17 +74,20 @@ 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` | -| 祖先中没有稳定 tag | `0.0.0~+g` | 同上替换规范版本 | 同上映射 | +| 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.2~..+g` | `name_1.4.2~..+g_.deb` | `1.4.2-...g` | +| 祖先中没有稳定 tag | `0.0.0~.+g` | 同上替换规范版本 | 同上映射 | 测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~`,使 -`1.4.2~…` < `1.4.2`,`apt upgrade` 不会装上测试包。Docker tag 不得含 -`~`、`/`、`:`,由规范版本把 `~` → `-`、`+g` → `.g`。 +`1.4.2~…` < `1.4.2`,`apt upgrade` 不会装上测试包。日期必须写在 `~` +之后,不能写成 `1.4.2.` / `1.4.2+`,否则测试包会大于正式 +包。Docker tag 不得含 `~`、`/`、`:`,由规范版本把 `~` → `-`、`+g` → `.g`。 +``:构建日 `YYYYMMDD`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC +日期,否则用本机当前日期。正式 `X.Y.Z` 不加日期。 ``:`git rev-parse --abbrev-ref HEAD`,detached 时用 `BUILD_BRANCH` / `CI_COMMIT_BRANCH` / `GITHUB_REF_NAME`,再没有则 `detached`。清洗:小写;`/`、`_` → `-`;去掉非 `[a-z0-9-]`;压缩连续 -`-`;过长截断(给 base、距离、SHA 留位置;Docker tag 上限 128)。 +`-`;过长截断(给 base、日期、距离、SHA 留位置;Docker tag 上限 128)。 `` 为基线 tag 到 HEAD 的 commit 数;`` 为 7 位短哈希。不要把 `--dirty` 写入版本;脏树发布仍走既有门禁。显式 `VERSION=` / `IMAGE_TAG=` 可覆盖推导,但不得把非 exact-match 的 commit 标成正式 `X.Y.Z`。 diff --git a/skills/builder/scripts/version.sh b/skills/builder/scripts/version.sh index 735a191..dbbc4b9 100755 --- a/skills/builder/scripts/version.sh +++ b/skills/builder/scripts/version.sh @@ -11,8 +11,11 @@ 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~branch.n+gSHA - no tag no stable tag reachable from HEAD → 0.0.0~branch+gSHA + test nearest ancestor stable tag → X.Y.Z~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 +UTC; otherwise the local calendar date. Official X.Y.Z never gets a date. Stable tags match v.. only. Baseline is ancestry, not the highest version in the repository. This script does not fetch tags. @@ -100,6 +103,16 @@ short_sha() { gitc rev-parse HEAD | cut -c1-7 } +# Build-day YYYYMMDD. SOURCE_DATE_EPOCH (UTC) wins when set, for reproducible +# test packages; otherwise the machine's current local date. +build_date() { + if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then + date -u -d "@${SOURCE_DATE_EPOCH}" +%Y%m%d + else + date +%Y%m%d + fi +} + # Highest stable tag pointing at HEAD, or empty. official_tag_at_head() { local tag best= @@ -135,7 +148,7 @@ nearest_stable_tag() { } derive_canonical() { - local tag base n sha branch + local tag base n sha branch ymd tag=$(official_tag_at_head) if [[ -n "$tag" ]]; then printf '%s' "${tag#v}" @@ -143,13 +156,14 @@ derive_canonical() { fi sha=$(short_sha) branch=$(branch_slug) + ymd=$(build_date) base=$(nearest_stable_tag) if [[ -z "$base" ]]; then - printf '0.0.0~%s+g%s' "$branch" "$sha" + printf '0.0.0~%s.%s+g%s' "$ymd" "$branch" "$sha" return fi n=$(gitc rev-list --count "${base}..HEAD") - printf '%s~%s.%s+g%s' "${base#v}" "$branch" "$n" "$sha" + printf '%s~%s.%s.%s+g%s' "${base#v}" "$ymd" "$branch" "$n" "$sha" } to_docker() { diff --git a/tests/test_builder_version.py b/tests/test_builder_version.py index 32bc8fc..023af3b 100644 --- a/tests/test_builder_version.py +++ b/tests/test_builder_version.py @@ -14,6 +14,10 @@ VERSION_SH = SCRIPTS / "version.sh" PUBLISH_DOCKER = SCRIPTS / "publish_docker.sh" VERSION_MK = SCRIPTS / "version.mk" +# Frozen build date so test versions are deterministic. +SOURCE_DATE_EPOCH = "1704067200" # 2024-01-01T00:00:00Z +BUILD_DATE = "20240101" + def git(cwd: Path, *args: str, env: dict[str, str] | None = None) -> str: merged = os.environ.copy() @@ -48,6 +52,7 @@ def sha7(cwd: Path) -> str: def version_sh(*args: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]: merged = os.environ.copy() + merged["SOURCE_DATE_EPOCH"] = SOURCE_DATE_EPOCH if env: merged.update(env) return subprocess.run( @@ -113,12 +118,12 @@ class BuilderVersionTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( result.stdout.strip(), - f"1.4.2~feat-login-v2.2+g{feature_sha}", + f"1.4.2~{BUILD_DATE}.feat-login-v2.2+g{feature_sha}", ) docker = version_sh("--docker", cwd=repo) self.assertEqual( docker.stdout.strip(), - f"1.4.2-feat-login-v2.2.g{feature_sha}", + f"1.4.2-{BUILD_DATE}.feat-login-v2.2.g{feature_sha}", ) def test_ignores_prerelease_and_app_tags(self) -> None: @@ -136,7 +141,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~main.1+g{sha}") + self.assertEqual(result.stdout.strip(), f"1.0.0~{BUILD_DATE}.main.1+g{sha}") def test_no_stable_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: @@ -148,7 +153,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"0.0.0~main+g{sha}") + self.assertEqual(result.stdout.strip(), f"0.0.0~{BUILD_DATE}.main+g{sha}") def test_detached_uses_env_branch(self) -> None: with tempfile.TemporaryDirectory() as temp: @@ -164,7 +169,34 @@ 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~ci-job.1+g{sha}") + self.assertEqual(result.stdout.strip(), f"0.1.0~{BUILD_DATE}.ci-job.1+g{sha}") + + def test_source_date_epoch_changes_test_date(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, "init") + git(repo, "tag", "v1.0.0") + (repo / "b").write_text("2\n", encoding="utf-8") + commit(repo, "next") + 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}") + + def test_official_version_ignores_build_date(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, "init") + git(repo, "tag", "-a", "v1.4.2", "-m", "1.4.2") + result = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1725148800"}) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "1.4.2") def test_override_official_rejected_off_tag(self) -> None: with tempfile.TemporaryDirectory() as temp: