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).
This commit is contained in:
+13
-10
@@ -84,23 +84,26 @@ Docker registry/ns/foo:1.4.2
|
|||||||
|
|
||||||
### 测试(其它任何 commit)
|
### 测试(其它任何 commit)
|
||||||
|
|
||||||
基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`),再加上构建日
|
基线 = 祖先上最近的那颗稳定 tag(没有则为 `0.0.0`)。有基线时测试号把
|
||||||
`YYYYMMDD`、清洗后的分支名、相对距离、短 SHA。测试与正式进**同一个**
|
patch 机械 +1,再加构建日 `YYYYMMDD`、清洗后的分支名、相对距离、短 SHA。
|
||||||
apt / Docker 仓库,所以 DEB 必须用 `~`,保证测试包不会 `apt upgrade`
|
这是排序键,不是在选定下一正式 SemVer。测试与正式进**同一个** apt /
|
||||||
盖住正式包。日期写在 `~` 后面,这样 `1.4.2~20260908…` 仍小于 `1.4.2`。
|
Docker 仓库,所以 DEB 必须用 `~` 挂在下一 patch 上:`1.4.2` <
|
||||||
|
`1.4.3~20260908…` < `1.4.3`。已装上一正式版的机器可以升到 bugfix 测试包;
|
||||||
|
同号正式版仍盖住测试。
|
||||||
|
|
||||||
分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`,
|
分支 `feat/login-v2`,相对 `v1.4.2` 第 7 个 commit,SHA `abc1234`,
|
||||||
构建日 `20260908`:
|
构建日 `20260908`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
规范 / DEB 1.4.2~20260908.feat-login-v2.7+gabc1234
|
规范 / DEB 1.4.3~20260908.feat-login-v2.7+gabc1234
|
||||||
文件名 foo_1.4.2~20260908.feat-login-v2.7+gabc1234_amd64.deb
|
文件名 foo_1.4.3~20260908.feat-login-v2.7+gabc1234_amd64.deb
|
||||||
Docker registry/ns/foo:1.4.2-20260908.feat-login-v2.7.gabc1234
|
Docker registry/ns/foo:1.4.3-20260908.feat-login-v2.7.gabc1234
|
||||||
```
|
```
|
||||||
|
|
||||||
Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。
|
Docker tag 由规范版本映射:`~` → `-`,`+g` → `.g`(Docker 不允许 `~`)。
|
||||||
安装测试包必须显式指定版本或完整 tag,不能靠无参 `apt upgrade`。
|
无参 `apt upgrade` 会从上一正式版升到下一 patch 的测试包,但不会盖住同号
|
||||||
构建日取本机当前日期;设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 日期。
|
正式包。生产若不能自动吃到测试包,应拆仓库/组件。构建日取本机当前日期;
|
||||||
|
设了 `SOURCE_DATE_EPOCH` 则用该时刻的 UTC 日期。
|
||||||
|
|
||||||
### 分支名清洗
|
### 分支名清洗
|
||||||
|
|
||||||
@@ -132,7 +135,7 @@ Docker 只有你点名镜像时才走。token 不会写进输出、提交或回
|
|||||||
- 初始化:`check.py --ready` 的构建项 PASS。只缺发布键算部分完成,仍可构建、不能上传。
|
- 初始化:`check.py --ready` 的构建项 PASS。只缺发布键算部分完成,仍可构建、不能上传。
|
||||||
- 构建:产物路径、规范版本、架构与 `make -f makefile.builder version` 一致。
|
- 构建:产物路径、规范版本、架构与 `make -f makefile.builder version` 一致。
|
||||||
- 发布:HTTP 成功或远端 digest 对得上;回复里有包名或镜像引用、SHA-256 / digest、源 commit。
|
- 发布:HTTP 成功或远端 digest 对得上;回复里有包名或镜像引用、SHA-256 / digest、源 commit。
|
||||||
- 测试包不会通过无参 `apt upgrade` 盖住正式包。
|
- 测试包不会通过无参 `apt upgrade` 盖住同号正式包;会作为下一 patch 候选盖住上一正式包。
|
||||||
|
|
||||||
## 脚本一览
|
## 脚本一览
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ make -f makefile.builder <target>
|
|||||||
`make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的
|
`make -f makefile.builder version` 输出一行规范版本(无 `v` 前缀)。DEB 的
|
||||||
`Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh`
|
`Version` 与文件名直接用它;Docker tag 由它渲染。推导入口是 `scripts/version.sh`
|
||||||
(`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
|
(`makefile.builder` 通过 `scripts/version.mk` 引用);Builder 只读取 Git 状态,不调用
|
||||||
manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release 事先打好。
|
manage-release 选定下一正式 SemVer。测试号把祖先基线的 patch 机械 +1 作为排序键
|
||||||
`version.sh` 不执行 `git fetch`。
|
(`v1.4.2` 之后 → `1.4.3~…`),不是在宣布下一正式号就是 1.4.3。正式 tag 由
|
||||||
|
manage-release 事先打好。`version.sh` 不执行 `git fetch`。
|
||||||
|
|
||||||
推导前 `git fetch --tags`(本地 linked worktree 共享 tags,不必再 fetch 才
|
推导前 `git fetch --tags`(本地 linked worktree 共享 tags,不必再 fetch 才
|
||||||
能看见其它 worktree 新打的 tag)。基线是 **HEAD 祖先上最近的稳定 tag**,
|
能看见其它 worktree 新打的 tag)。基线是 **HEAD 祖先上最近的稳定 tag**,
|
||||||
@@ -74,13 +75,15 @@ manage-release,不猜测下一个正式 SemVer。正式 tag 由 manage-release
|
|||||||
| 判定 | 规范版本 | DEB 文件 | Docker tag |
|
| 判定 | 规范版本 | DEB 文件 | Docker tag |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| HEAD exact-match 稳定 tag `v1.4.2` | `1.4.2` | `name_1.4.2_<arch>.deb` | `1.4.2` |
|
| HEAD exact-match 稳定 tag `v1.4.2` | `1.4.2` | `name_1.4.2_<arch>.deb` | `1.4.2` |
|
||||||
| 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.2~<date>.<branch>.<n>+g<sha>` | `name_1.4.2~<date>.<branch>.<n>+g<sha>_<arch>.deb` | `1.4.2-<date>.<branch>.<n>.g<sha>` |
|
| 其它 commit;祖先最近稳定 tag `v1.4.2` | `1.4.3~<date>.<branch>.<n>+g<sha>` | `name_1.4.3~<date>.<branch>.<n>+g<sha>_<arch>.deb` | `1.4.3-<date>.<branch>.<n>.g<sha>` |
|
||||||
| 祖先中没有稳定 tag | `0.0.0~<date>.<branch>+g<sha>` | 同上替换规范版本 | 同上映射 |
|
| 祖先中没有稳定 tag | `0.0.0~<date>.<branch>+g<sha>` | 同上替换规范版本 | 同上映射 |
|
||||||
|
|
||||||
测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~`,使
|
测试与正式进入同一 apt / Docker 仓库。测试 DEB 必须用 `~` 挂在基线的下一
|
||||||
`1.4.2~…` < `1.4.2`,`apt upgrade` 不会装上测试包。日期必须写在 `~`
|
patch 上:`1.4.2` < `1.4.3~…` < `1.4.3`。已装上一正式版的机器可以升到
|
||||||
之后,不能写成 `1.4.2.<date>` / `1.4.2+<date>`,否则测试包会大于正式
|
bugfix 测试包;同号正式版仍盖住测试。不要写成 `1.4.2~…`(小于已发布的
|
||||||
包。Docker tag 不得含 `~`、`/`、`:`,由规范版本把 `~` → `-`、`+g` → `.g`。
|
`1.4.2`),不要写成 `1.4.2+<date>` / `1.4.3-beta`(Debian 里 `-beta` 大于
|
||||||
|
`1.4.3`)。日期必须写在 `~` 之后。Docker tag 不得含 `~`、`/`、`:`,由规范
|
||||||
|
版本把 `~` → `-`、`+g` → `.g`。
|
||||||
|
|
||||||
`<date>`:构建日 `YYYYMMDD`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC
|
`<date>`:构建日 `YYYYMMDD`。已设 `SOURCE_DATE_EPOCH` 时用该时刻的 UTC
|
||||||
日期,否则用本机当前日期。正式 `X.Y.Z` 不加日期。
|
日期,否则用本机当前日期。正式 `X.Y.Z` 不加日期。
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
## Tag 策略
|
## Tag 策略
|
||||||
|
|
||||||
- 未显式给出 `--tag` / `IMAGE_TAG` 时,使用契约「版本号」渲染出的 tag
|
- 未显式给出 `--tag` / `IMAGE_TAG` 时,使用契约「版本号」渲染出的 tag
|
||||||
(正式 `1.4.2`,测试 `1.4.2-feat-login.7.gabc1234`),不要退回
|
(正式 `1.4.2`,测试 `1.4.3-<date>.feat-login.7.gabc1234`),不要退回
|
||||||
`git describe`,不要询问后另选一套。
|
`git describe`,不要询问后另选一套。
|
||||||
- 正式产物 tag 与 Git tag `vX.Y.Z` 对应,但镜像 tag **不含** `v`;默认视为不可变。
|
- 正式产物 tag 与 Git tag `vX.Y.Z` 对应,但镜像 tag **不含** `v`;默认视为不可变。
|
||||||
- `latest`、`stable`、裸分支名等浮动 tag 只有在用户明确要求时才额外发布,
|
- `latest`、`stable`、裸分支名等浮动 tag 只有在用户明确要求时才额外发布,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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 → 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
|
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
|
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]+$ ]]
|
[[ "$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 one leading v if present.
|
||||||
strip_v() {
|
strip_v() {
|
||||||
local v=$1
|
local v=$1
|
||||||
@@ -163,7 +172,7 @@ derive_canonical() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
n=$(gitc rev-list --count "${base}..HEAD")
|
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() {
|
to_docker() {
|
||||||
|
|||||||
@@ -91,6 +91,29 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
self.assertEqual(docker.returncode, 0, docker.stderr)
|
self.assertEqual(docker.returncode, 0, docker.stderr)
|
||||||
self.assertEqual(docker.stdout, "1.4.2\n")
|
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:
|
def test_test_version_uses_ancestor_not_global_max(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
root = Path(temp)
|
root = Path(temp)
|
||||||
@@ -118,12 +141,12 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result.stdout.strip(),
|
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)
|
docker = version_sh("--docker", cwd=repo)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
docker.stdout.strip(),
|
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:
|
def test_ignores_prerelease_and_app_tags(self) -> None:
|
||||||
@@ -141,7 +164,7 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
sha = sha7(repo)
|
sha = sha7(repo)
|
||||||
result = version_sh(cwd=repo)
|
result = version_sh(cwd=repo)
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
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:
|
def test_no_stable_tag(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
@@ -169,7 +192,7 @@ class BuilderVersionTests(unittest.TestCase):
|
|||||||
git(repo, "checkout", "--detach", "HEAD")
|
git(repo, "checkout", "--detach", "HEAD")
|
||||||
result = version_sh(cwd=repo, env={"BUILD_BRANCH": "ci_job"})
|
result = version_sh(cwd=repo, env={"BUILD_BRANCH": "ci_job"})
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
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:
|
def test_source_date_epoch_changes_test_date(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp:
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
@@ -184,7 +207,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.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:
|
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