fix(builder): 版本时间精确到分钟,修复同日重构建的 apt 排序

同日同分支的测试版本前缀完全相同,只剩 +g<sha> 可区分;Debian 版本比较把
hex 当普通文本,新构建可能小于旧构建,apt upgrade 因此拿不到最新包。

build_date() → build_stamp(),改为 YYYYMMDDHHMM,时间与日期拼成一段数字
才能按数值比较。不能写成 20260918T1722:Debian 里字母排在标点之前,带 T
的版本小于同日旧格式包的 20260918.…,修复当天仍抢不过已上传的旧包。

补回归测试,用 dpkg --compare-versions 锁定同日重建更大、且压过旧格式同日包。
This commit is contained in:
2026-09-19 13:23:52 +08:00
parent b476176f3b
commit eb2d0ce651
4 changed files with 97 additions and 33 deletions
+12 -8
View File
@@ -134,25 +134,29 @@ Docker registry/ns/foo:1.4.2
### 测试(其它任何 commit)
基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`)。有基线时测试号把
patch 机械 +1,再加构建日 `YYYYMMDD`、清洗后的分支名、相对距离、短 SHA。
patch 机械 +1,再加构建时间 `YYYYMMDDHHMM`、清洗后的分支名、相对距离、短 SHA。
这是排序键,不是在选定下一正式 SemVer。测试与正式进**同一个** apt /
Docker 仓库,所以 DEB 必须用 `~` 挂在下一 patch 上:`1.4.2` <
`1.4.3~20260908…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;
`1.4.3~202609181430…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;
同号正式版仍盖住测试。
分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`,
构建日 `20260908`:
构建时间 `202609181430`(带分钟,同一天重建才排得对):
```text
规范 / 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
规范 / DEB 1.4.3~202609181430.feat-login-v2.7+gabc1234
文件名 foo_1.4.3~202609181430.feat-login-v2.7+gabc1234_amd64.deb
Docker registry/ns/foo:1.4.3-202609181430.feat-login-v2.7.gabc1234
```
Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。
无参 `apt upgrade` 会从上一正式版升到下一 patch 的测试包,但不会盖住同号
正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建日取本机当前日期;
设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 日期。
正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建时间取本机当前本地
时间(含分钟);设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 时间。时间和日
期之间不写分隔符:Debian 把字母排在标点之前,`20260918T1430` 会小于同日旧
格式包的 `20260918.…`,同一天的包反而抢不到最新。`+g<sha>` 只用于记录
commit、不参与排序:只写到日期时,同一天重复构建的版本只差 `+g<sha>`,
Debian 版本比较把哈希当普通文本比,会把新包判成比旧包小。
### 分支名清洗
+7 -2
View File
@@ -102,8 +102,13 @@ bugfix 测试包;同号正式版仍盖住测试。不要写成 `1.4.2~…`(
`1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范
版本把 `~` → `-`、`+g` → `.g`。
`<date>`:构建日 `YYYYMMDD`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC
日期,否则用本机当前日期。正式 `X.Y.Z` 不加日期。
`<date>`:`YYYYMMDDHHMM`,构建的日期加分钟,中间不加分隔符。加分钟是
为了同一天重复构建也能排序:只写到日期时,同分支同日的版本前缀完全相
同,只剩 `+g<sha>` 可区分,而 Debian 把哈希当普通文本比较,新构建可能
排到旧构建下面(`apt upgrade` 因此看不到最新包)。不能写成
`YYYYMMDDTHHMM`:Debian 版本比较里字母排在标点之前,带 `T` 的版本会小于
同日旧格式包的 `YYYYMMDD.…`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC
时间,否则用本机当前时间。正式 `X.Y.Z` 不加日期。
`<branch>`:`git rev-parse --abbrev-ref HEAD`,detached 时用
`BUILD_BRANCH` / `CI_COMMIT_BRANCH` / `GITHUB_REF_NAME`,再没有则
`detached`。清洗:小写;`/`、`_` → `-`;去掉非 `[a-z0-9-]`;压缩连续
+25 -13
View File
@@ -11,11 +11,19 @@ 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 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
test nearest ancestor stable tag vX.Y.Z → X.Y.(Z+1)~YYYYMMDDHHMM.branch.n+gSHA
no tag no stable tag reachable from HEAD → 0.0.0~YYYYMMDDHHMM.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.
YYYYMMDDHHMM is the build minute (day plus time of day), with no separator:
the digits must stay in one run so Debian compares them numerically. A letter
separator (YYYYMMDDTHHMM) would compare *below* the bare date, because Debian
orders letters before punctuation — same-day packages built with the old
date-only scheme would outrank the new ones. SOURCE_DATE_EPOCH, if set, is
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
ignores upload order, so same-day rebuilds that differ only in +gSHA compare
as arbitrary text (a newer build can sort below an older one). With the minute
in the version, later builds always compare greater.
Stable tags match v<major>.<minor>.<patch> only. Baseline is ancestry, not
the highest version in the repository. This script does not fetch tags.
@@ -112,13 +120,17 @@ 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() {
# Build-minute YYYYMMDDHHMM. SOURCE_DATE_EPOCH (UTC) wins when set, for
# reproducible test packages; otherwise the machine's current local time.
# 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
# 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() {
if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then
date -u -d "@${SOURCE_DATE_EPOCH}" +%Y%m%d
date -u -d "@${SOURCE_DATE_EPOCH}" +%Y%m%d%H%M
else
date +%Y%m%d
date +%Y%m%d%H%M
fi
}
@@ -157,7 +169,7 @@ nearest_stable_tag() {
}
derive_canonical() {
local tag base n sha branch ymd
local tag base n sha branch stamp
tag=$(official_tag_at_head)
if [[ -n "$tag" ]]; then
printf '%s' "${tag#v}"
@@ -165,14 +177,14 @@ derive_canonical() {
fi
sha=$(short_sha)
branch=$(branch_slug)
ymd=$(build_date)
stamp=$(build_stamp)
base=$(nearest_stable_tag)
if [[ -z "$base" ]]; then
printf '0.0.0~%s.%s+g%s' "$ymd" "$branch" "$sha"
printf '0.0.0~%s.%s+g%s' "$stamp" "$branch" "$sha"
return
fi
n=$(gitc rev-list --count "${base}..HEAD")
printf '%s~%s.%s.%s+g%s' "$(bump_patch "$base")" "$ymd" "$branch" "$n" "$sha"
printf '%s~%s.%s.%s+g%s' "$(bump_patch "$base")" "$stamp" "$branch" "$n" "$sha"
}
to_docker() {