fix(builder): 版本时间格式改回 YYYYMMDDTHHMM
按要求保留字母分隔符:20260918T1722。分钟精度不变,同一分支同日多次 构建仍能正确排序。 记录切换边界:Debian 里字母排在标点之前,YYYYMMDDT… 小于旧格式的 YYYYMMDD.…,切换当天同日的旧格式包会压住新包,需要在仓库里把那些旧包 移除或停用;次日新格式始终更大。回归测试显式断言这行边界。
This commit is contained in:
+12
-11
@@ -134,29 +134,30 @@ Docker registry/ns/foo:1.4.2
|
|||||||
### 测试(其它任何 commit)
|
### 测试(其它任何 commit)
|
||||||
|
|
||||||
基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`)。有基线时测试号把
|
基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`)。有基线时测试号把
|
||||||
patch 机械 +1,再加构建时间 `YYYYMMDDHHMM`、清洗后的分支名、相对距离、短 SHA。
|
patch 机械 +1,再加构建时间 `YYYYMMDDTHHMM`、清洗后的分支名、相对距离、短 SHA。
|
||||||
这是排序键,不是在选定下一正式 SemVer。测试与正式进**同一个** apt /
|
这是排序键,不是在选定下一正式 SemVer。测试与正式进**同一个** apt /
|
||||||
Docker 仓库,所以 DEB 必须用 `~` 挂在下一 patch 上:`1.4.2` <
|
Docker 仓库,所以 DEB 必须用 `~` 挂在下一 patch 上:`1.4.2` <
|
||||||
`1.4.3~202609181430…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;
|
`1.4.3~20260918T1430…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;
|
||||||
同号正式版仍盖住测试。
|
同号正式版仍盖住测试。
|
||||||
|
|
||||||
分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`,
|
分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`,
|
||||||
构建时间 `202609181430`(带分钟,同一天重建才排得对):
|
构建时间 `20260918T1430`(带分钟,同一天重建才排得对):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
规范 / DEB 1.4.3~202609181430.feat-login-v2.7+gabc1234
|
规范 / DEB 1.4.3~20260918T1430.feat-login-v2.7+gabc1234
|
||||||
文件名 foo_1.4.3~202609181430.feat-login-v2.7+gabc1234_amd64.deb
|
文件名 foo_1.4.3~20260918T1430.feat-login-v2.7+gabc1234_amd64.deb
|
||||||
Docker registry/ns/foo:1.4.3-202609181430.feat-login-v2.7.gabc1234
|
Docker registry/ns/foo:1.4.3-20260918T1430.feat-login-v2.7.gabc1234
|
||||||
```
|
```
|
||||||
|
|
||||||
Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。
|
Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。
|
||||||
无参 `apt upgrade` 会从上一正式版升到下一 patch 的测试包,但不会盖住同号
|
无参 `apt upgrade` 会从上一正式版升到下一 patch 的测试包,但不会盖住同号
|
||||||
正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建时间取本机当前本地
|
正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建时间取本机当前本地
|
||||||
时间(含分钟);设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 时间。时间和日
|
时间(含分钟);设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 时间。`+g<sha>`
|
||||||
期之间不写分隔符:Debian 把字母排在标点之前,`20260918T1430` 会小于同日旧
|
只用于记录 commit、不参与排序:只写到日期时,同一天重复构建的版本只差
|
||||||
格式包的 `20260918.…`,同一天的包反而抢不到最新。`+g<sha>` 只用于记录
|
`+g<sha>`,Debian 版本比较把哈希当普通文本比,会把新包判成比旧包小。
|
||||||
commit、不参与排序:只写到日期时,同一天重复构建的版本只差 `+g<sha>`,
|
注意切换格式当天:Debian 把字母排在标点之前,`20260918T1430…` 小于旧格式
|
||||||
Debian 版本比较把哈希当普通文本比,会把新包判成比旧包小。
|
的 `20260918.…`,同一天已上传的旧格式包仍会压住新包,需要在仓库里把那些
|
||||||
|
旧包移除或停用;从第二天起新格式始终更大。
|
||||||
|
|
||||||
### 分支名清洗
|
### 分支名清洗
|
||||||
|
|
||||||
|
|||||||
@@ -102,13 +102,13 @@ bugfix 测试包;同号正式版仍盖住测试。不要写成 `1.4.2~…`(
|
|||||||
`1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范
|
`1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范
|
||||||
版本把 `~` → `-`、`+g` → `.g`。
|
版本把 `~` → `-`、`+g` → `.g`。
|
||||||
|
|
||||||
`<date>`:`YYYYMMDDHHMM`,构建的日期加分钟,中间不加分隔符。加分钟是
|
`<date>`:`YYYYMMDDTHHMM`,构建的日期加分钟。加分钟是为了同一天重复构建
|
||||||
为了同一天重复构建也能排序:只写到日期时,同分支同日的版本前缀完全相
|
也能排序:只写到日期时,同分支同日的版本前缀完全相同,只剩 `+g<sha>` 可
|
||||||
同,只剩 `+g<sha>` 可区分,而 Debian 把哈希当普通文本比较,新构建可能
|
区分,而 Debian 把哈希当普通文本比较,新构建可能排到旧构建下面(
|
||||||
排到旧构建下面(`apt upgrade` 因此看不到最新包)。不能写成
|
`apt upgrade` 因此看不到最新包)。已设 `SOURCE_DATE_EPOCH` 时用该时刻的
|
||||||
`YYYYMMDDTHHMM`:Debian 版本比较里字母排在标点之前,带 `T` 的版本会小于
|
UTC 时间,否则用本机当前时间。正式 `X.Y.Z` 不加日期。切换格式当天注意:
|
||||||
同日旧格式包的 `YYYYMMDD.…`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC
|
Debian 把字母排在标点之前,`YYYYMMDDT…` 小于旧格式的 `YYYYMMDD.…`,同日
|
||||||
时间,否则用本机当前时间。正式 `X.Y.Z` 不加日期。
|
已上传的旧格式包会压住新包,需要把它们在仓库里移除或停用;次日恢复正常。
|
||||||
`<branch>`:`git rev-parse --abbrev-ref HEAD`,detached 时用
|
`<branch>`:`git rev-parse --abbrev-ref HEAD`,detached 时用
|
||||||
`BUILD_BRANCH` / `CI_COMMIT_BRANCH` / `GITHUB_REF_NAME`,再没有则
|
`BUILD_BRANCH` / `CI_COMMIT_BRANCH` / `GITHUB_REF_NAME`,再没有则
|
||||||
`detached`。清洗:小写;`/`、`_` → `-`;去掉非 `[a-z0-9-]`;压缩连续
|
`detached`。清洗:小写;`/`、`_` → `-`;去掉非 `[a-z0-9-]`;压缩连续
|
||||||
|
|||||||
@@ -11,19 +11,20 @@ With --docker, print the Docker tag rendering of that version.
|
|||||||
|
|
||||||
Derivation (builder contract «版本号»):
|
Derivation (builder contract «版本号»):
|
||||||
official HEAD exact-match of vX.Y.Z → X.Y.Z
|
official HEAD exact-match of vX.Y.Z → X.Y.Z
|
||||||
test nearest ancestor stable tag vX.Y.Z → X.Y.(Z+1)~YYYYMMDDHHMM.branch.n+gSHA
|
test nearest ancestor stable tag vX.Y.Z → X.Y.(Z+1)~YYYYMMDDTHHMM.branch.n+gSHA
|
||||||
no tag no stable tag reachable from HEAD → 0.0.0~YYYYMMDDHHMM.branch+gSHA
|
no tag no stable tag reachable from HEAD → 0.0.0~YYYYMMDDTHHMM.branch+gSHA
|
||||||
|
|
||||||
YYYYMMDDHHMM is the build minute (day plus time of day), with no separator:
|
YYYYMMDDTHHMM is the build minute (day plus time of day). SOURCE_DATE_EPOCH,
|
||||||
the digits must stay in one run so Debian compares them numerically. A letter
|
if set, is that instant in UTC; otherwise the local date and time. Official
|
||||||
separator (YYYYMMDDTHHMM) would compare *below* the bare date, because Debian
|
X.Y.Z never gets a date. Minute precision is required for apt: Debian version
|
||||||
orders letters before punctuation — same-day packages built with the old
|
comparison ignores upload order, so same-day rebuilds that differ only in
|
||||||
date-only scheme would outrank the new ones. SOURCE_DATE_EPOCH, if set, is
|
+gSHA compare as arbitrary text (a newer build can sort below an older one).
|
||||||
that instant in UTC; otherwise the local date and time. Official X.Y.Z never
|
|
||||||
gets a date. Minute precision is required for apt: Debian version comparison
|
Switching from the date-only scheme: Debian orders letters below punctuation,
|
||||||
ignores upload order, so same-day rebuilds that differ only in +gSHA compare
|
so 'YYYYMMDDT…' compares *below* the old 'YYYYMMDD.…' form. Packages built on
|
||||||
as arbitrary text (a newer build can sort below an older one). With the minute
|
the switch day therefore rank under that day's date-only packages until those
|
||||||
in the version, later builds always compare greater.
|
are removed or disabled in the repository; from the next day on, the new form
|
||||||
|
always ranks higher.
|
||||||
|
|
||||||
Stable tags match v<major>.<minor>.<patch> only. Baseline is ancestry, not
|
Stable tags match v<major>.<minor>.<patch> only. Baseline is ancestry, not
|
||||||
the highest version in the repository. This script does not fetch tags.
|
the highest version in the repository. This script does not fetch tags.
|
||||||
@@ -120,17 +121,15 @@ short_sha() {
|
|||||||
gitc rev-parse HEAD | cut -c1-7
|
gitc rev-parse HEAD | cut -c1-7
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build-minute YYYYMMDDHHMM. SOURCE_DATE_EPOCH (UTC) wins when set, for
|
# Build-minute YYYYMMDDTHHMM. SOURCE_DATE_EPOCH (UTC) wins when set, for
|
||||||
# reproducible test packages; otherwise the machine's current local time.
|
# reproducible test packages; otherwise the machine's current local time.
|
||||||
# The minute is not decoration: it is what keeps same-day rebuilds ordered in
|
# The minute is not decoration: it is what keeps same-day rebuilds ordered in
|
||||||
# Debian version comparison (0.0.0~202609181036… < 0.0.0~202609181722…). It is
|
# Debian version comparison (0.0.0~20260918T1036… < 0.0.0~20260918T1722…).
|
||||||
# glued to the date with no 'T' so the whole stamp stays one numeric run;
|
|
||||||
# '20260918T1722' would sort below '20260918.dev-…' and lose to old packages.
|
|
||||||
build_stamp() {
|
build_stamp() {
|
||||||
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then
|
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then
|
||||||
date -u -d "@${SOURCE_DATE_EPOCH}" +%Y%m%d%H%M
|
date -u -d "@${SOURCE_DATE_EPOCH}" +%Y%m%dT%H%M
|
||||||
else
|
else
|
||||||
date +%Y%m%d%H%M
|
date +%Y%m%dT%H%M
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ VERSION_MK = SCRIPTS / "version.mk"
|
|||||||
|
|
||||||
# Frozen build instant so test versions are deterministic.
|
# Frozen build instant so test versions are deterministic.
|
||||||
SOURCE_DATE_EPOCH = "1704067200" # 2024-01-01T00:00:00Z
|
SOURCE_DATE_EPOCH = "1704067200" # 2024-01-01T00:00:00Z
|
||||||
BUILD_STAMP = "202401010000"
|
BUILD_STAMP = "20240101T0000"
|
||||||
|
|
||||||
|
|
||||||
def git(cwd: Path, *args: str, env: dict[str, str] | None = None) -> str:
|
def git(cwd: Path, *args: str, env: dict[str, str] | None = None) -> str:
|
||||||
@@ -183,9 +183,11 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
"""同一天重建必须比上一包大,否则 apt 永远拿不到最新构建。
|
"""同一天重建必须比上一包大,否则 apt 永远拿不到最新构建。
|
||||||
|
|
||||||
只写到日期时同分支同日的版本前缀完全相同,只剩 +g<sha> 可区分,
|
只写到日期时同分支同日的版本前缀完全相同,只剩 +g<sha> 可区分,
|
||||||
Debian 版本比较把哈希当普通文本,新包可能被判成比旧包小。带分钟的
|
Debian 版本比较把哈希当普通文本,新包可能被判成比旧包小。
|
||||||
时间必须与日期拼在同一段数字里:字母排在标点之前,YYYYMMDDTHHMM
|
|
||||||
这种带字母分隔的写法会小于同日旧格式包的 YYYYMMDD.…。
|
已知边界(故意断言):切换格式当天,旧格式同日包 `YYYYMMDD.…` 仍
|
||||||
|
大于新格式 `YYYYMMDDT…`,因为 Debian 把字母排在标点之前。那天需要
|
||||||
|
把旧格式包在仓库里移除或停用,或者等次日(次日新格式一定更大)。
|
||||||
"""
|
"""
|
||||||
if shutil.which("dpkg") is None:
|
if shutil.which("dpkg") is None:
|
||||||
self.skipTest("dpkg not available")
|
self.skipTest("dpkg not available")
|
||||||
@@ -200,17 +202,15 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
self.assertEqual(early.returncode, 0, early.stderr)
|
self.assertEqual(early.returncode, 0, early.stderr)
|
||||||
self.assertEqual(later.returncode, 0, later.stderr)
|
self.assertEqual(later.returncode, 0, later.stderr)
|
||||||
first, second = early.stdout.strip(), later.stdout.strip()
|
first, second = early.stdout.strip(), later.stdout.strip()
|
||||||
self.assertEqual(first.rsplit(".", 1)[0].split("~")[1].split(".")[0], "202401010000")
|
|
||||||
self.assertEqual(second.rsplit(".", 1)[0].split("~")[1].split(".")[0], "202401011000")
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
second,
|
second,
|
||||||
f"0.0.0~202401011000.main+g{sha7(repo)}",
|
f"0.0.0~20240101T1000.main+g{sha7(repo)}",
|
||||||
"时间必须与日期拼成一段数字,不能插入字母分隔符",
|
"构建时间精确到分钟,形如 YYYYMMDDTHHMM",
|
||||||
)
|
)
|
||||||
for label, a, b in (
|
for label, a, b in (
|
||||||
("同日重建", second, first),
|
("同日重建", second, first),
|
||||||
("已上传的旧格式同日包", second, "0.0.0~20240101.main+gaaaaaaa"),
|
("旧格式前一日包", second, "0.0.0~20231231.main+gaaaaaaa"),
|
||||||
("次日新格式包", "0.0.0~202401021000.main+gccccccc", second),
|
("次日新格式包", "0.0.0~20240102T1000.main+gccccccc", second),
|
||||||
("下一正式版", "0.1.0", second),
|
("下一正式版", "0.1.0", second),
|
||||||
):
|
):
|
||||||
compared = subprocess.run(
|
compared = subprocess.run(
|
||||||
@@ -220,6 +220,17 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
self.assertEqual(compared.returncode, 0, f"{label}:{a} 应大于 {b}")
|
self.assertEqual(compared.returncode, 0, f"{label}:{a} 应大于 {b}")
|
||||||
|
compared = subprocess.run(
|
||||||
|
["dpkg", "--compare-versions", "0.0.0~20240101.main+gaaaaaaa", "gt", second],
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
compared.returncode,
|
||||||
|
0,
|
||||||
|
"已知边界:旧格式同日包大于 YYYYMMDDT…,切换当天需要移除旧包或等次日",
|
||||||
|
)
|
||||||
|
|
||||||
def test_detached_uses_env_branch(self) -> None:
|
def test_detached_uses_env_branch(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
@@ -250,7 +261,7 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
sha = sha7(repo)
|
sha = sha7(repo)
|
||||||
later = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1725148800"})
|
later = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1725148800"})
|
||||||
self.assertEqual(later.returncode, 0, later.stderr)
|
self.assertEqual(later.returncode, 0, later.stderr)
|
||||||
self.assertEqual(later.stdout.strip(), f"1.0.1~202409010000.main.1+g{sha}")
|
self.assertEqual(later.stdout.strip(), f"1.0.1~20240901T0000.main.1+g{sha}")
|
||||||
|
|
||||||
def test_official_version_ignores_build_date(self) -> None:
|
def test_official_version_ignores_build_date(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
|||||||
Reference in New Issue
Block a user