feat: 上架版本号按分支拆分,master / develop 各自独立
原来 master 与 develop 共用一条 release 轨,版本号和构建号绑在一起。 现改为每个上架分支各有一条轨道(轨道名即分支名),自测分支仍共用 feature 轨。 - 轨道记录新增 kind(release/feature),branch_track 存轨道名 - 旧的共用 release 轨及更早的单轨结构,加载时自动按上架分支拆开, 已用构建号原样复制,拆分后各分支从原来的号继续递增 - 构建号递增时跨上架轨取同一 App_Ver 的最大值 +1:App Store 的构建号 唯一性按 MARKETING_VERSION 全局判定,避免两分支同版本号时重号被拒 - 分支管理页轨道选择改为「上架(本分支独立版本号)/ 自测(共用)」, 打包设置页按上架分支分块填写版本号 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJaZ913U5GVdTkYHiRpvDU
This commit is contained in:
+60
-15
@@ -229,15 +229,20 @@ def test_update_versions_rejects_invalid_app_version(client, tmp_config):
|
||||
|
||||
|
||||
def test_versions_tracks_are_independent(client, tmp_config):
|
||||
# 两条轨道的 App_Ver 各自独立,互不影响
|
||||
assert client.put("/api/config/versions", json={"track": "release", "app_ver": "2.196.0"}).status_code == 200
|
||||
# 每个上架分支一条轨道(轨道名即分支名),App_Ver 各自独立,互不影响
|
||||
assert client.put("/api/config/versions", json={"track": "main", "app_ver": "2.196.0"}).status_code == 200
|
||||
assert client.put("/api/config/versions", json={"track": "dev", "app_ver": "2.198.0"}).status_code == 200
|
||||
assert client.put("/api/config/versions", json={"track": "feature", "app_ver": "2.100.0"}).status_code == 200
|
||||
|
||||
data = client.get("/api/config/versions").json()
|
||||
assert data["tracks"]["release"]["app_ver"] == "2.196.0"
|
||||
assert data["tracks"]["main"]["app_ver"] == "2.196.0"
|
||||
assert data["tracks"]["dev"]["app_ver"] == "2.198.0"
|
||||
assert data["tracks"]["feature"]["app_ver"] == "2.100.0"
|
||||
assert data["tracks"]["feature"]["build_ver"] == "2.100.0.0"
|
||||
# 顶层字段镜像 release 轨,兼容旧客户端
|
||||
assert data["tracks"]["main"]["kind"] == "release"
|
||||
assert data["tracks"]["feature"]["kind"] == "feature"
|
||||
# 顶层字段镜像主上架轨(第一个上架分支),兼容旧客户端
|
||||
assert data["primary_track"] == "main"
|
||||
assert data["app_ver"] == "2.196.0"
|
||||
|
||||
|
||||
@@ -246,7 +251,8 @@ def test_update_versions_rejects_unknown_track(client, tmp_config):
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_legacy_versions_migrate_to_release_track(client, tmp_config):
|
||||
def test_legacy_versions_split_per_release_branch(client, tmp_config):
|
||||
"""最旧的单轨配置:拆成每个上架分支一条轨,各自沿用原来的已用构建号"""
|
||||
import json
|
||||
with open(tmp_config, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
@@ -255,28 +261,67 @@ def test_legacy_versions_migrate_to_release_track(client, tmp_config):
|
||||
json.dump(cfg, f)
|
||||
|
||||
data = client.get("/api/config/versions").json()
|
||||
assert data["tracks"]["release"] == {
|
||||
"app_ver": "2.195.0",
|
||||
"build_ver": "2.195.0.7",
|
||||
"build_map": {"2.195.0": 7},
|
||||
}
|
||||
for branch in ("main", "dev"):
|
||||
assert data["branch_track"][branch] == branch
|
||||
assert data["tracks"][branch] == {
|
||||
"kind": "release",
|
||||
"app_ver": "2.195.0",
|
||||
"build_ver": "2.195.0.7",
|
||||
"build_map": {"2.195.0": 7},
|
||||
}
|
||||
# feature 轨以同一 App_Ver 初始化,构建号从 .0 起
|
||||
assert data["tracks"]["feature"]["app_ver"] == "2.195.0"
|
||||
assert data["tracks"]["feature"]["build_ver"] == "2.195.0.0"
|
||||
# 拆分后各轨独立:改 main 不影响 dev
|
||||
assert client.put("/api/config/versions", json={"track": "main", "app_ver": "2.196.0"}).status_code == 200
|
||||
data = client.get("/api/config/versions").json()
|
||||
assert data["tracks"]["main"]["app_ver"] == "2.196.0"
|
||||
assert data["tracks"]["dev"]["app_ver"] == "2.195.0"
|
||||
|
||||
|
||||
def test_legacy_shared_release_track_splits_per_branch(client, tmp_config):
|
||||
"""旧的 master/develop 共用 release 轨:拆成各分支自己的轨,构建号原样带过去"""
|
||||
import json
|
||||
with open(tmp_config, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
cfg["versions"] = {"tracks": {
|
||||
"release": {"app_ver": "2.195.0", "build_map": {"2.195.0": 4}},
|
||||
"feature": {"app_ver": "2.100.0", "build_map": {}},
|
||||
}}
|
||||
cfg["branch_track"] = {"main": "release", "dev": "release"}
|
||||
with open(tmp_config, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f)
|
||||
|
||||
data = client.get("/api/config/versions").json()
|
||||
assert "release" not in data["tracks"]
|
||||
assert data["branch_track"] == {"main": "main", "dev": "dev"}
|
||||
for branch in ("main", "dev"):
|
||||
assert data["tracks"][branch]["app_ver"] == "2.195.0"
|
||||
assert data["tracks"][branch]["build_ver"] == "2.195.0.4"
|
||||
assert data["tracks"]["feature"]["app_ver"] == "2.100.0"
|
||||
|
||||
|
||||
def test_branch_track_defaults_and_override(client, tmp_config):
|
||||
assert client.post("/api/config/branches", json={"name": "develop"}).status_code == 200
|
||||
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
||||
|
||||
tracks = client.get("/api/config/versions").json()["branch_track"]
|
||||
assert tracks["develop"] == "release"
|
||||
assert tracks["feature/login"] == "feature"
|
||||
data = client.get("/api/config/versions").json()
|
||||
# 上架分支各有一条以分支名命名的轨道,自测分支共用 feature 轨
|
||||
assert data["branch_track"]["develop"] == "develop"
|
||||
assert data["branch_track"]["feature/login"] == "feature"
|
||||
assert data["tracks"]["develop"]["kind"] == "release"
|
||||
|
||||
# 手动改归属
|
||||
# 手动改归属:改成上架后拥有自己的轨道
|
||||
resp = client.put("/api/config/branches/track", json={"branch": "feature/login", "track": "release"})
|
||||
assert resp.status_code == 200
|
||||
assert client.get("/api/config/versions").json()["branch_track"]["feature/login"] == "release"
|
||||
assert resp.json()["track"] == "feature/login"
|
||||
data = client.get("/api/config/versions").json()
|
||||
assert data["branch_track"]["feature/login"] == "feature/login"
|
||||
assert data["tracks"]["feature/login"]["kind"] == "release"
|
||||
|
||||
# 改回自测则重新共用 feature 轨
|
||||
assert client.put("/api/config/branches/track", json={"branch": "feature/login", "track": "feature"}).status_code == 200
|
||||
assert client.get("/api/config/versions").json()["branch_track"]["feature/login"] == "feature"
|
||||
|
||||
assert client.put("/api/config/branches/track", json={"branch": "nope", "track": "release"}).status_code == 404
|
||||
assert client.put("/api/config/branches/track", json={"branch": "develop", "track": "beta"}).status_code == 400
|
||||
|
||||
+58
-27
@@ -315,14 +315,23 @@ def test_resolve_provisioning_profile_extracts_name_and_team(tmp_path):
|
||||
assert _resolve_provisioning_profile(str(profile_path)) == ("a4YX061_adHoc", "MG4Z7FU83W")
|
||||
|
||||
|
||||
def _versions_config(release: dict, feature: dict) -> dict:
|
||||
def _versions_config(tracks: dict) -> dict:
|
||||
"""构造带版本轨道的配置;tracks 为 轨道名 -> {kind, app_ver, build_map}。"""
|
||||
return {
|
||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
||||
"versions": {"tracks": {"release": release, "feature": feature}},
|
||||
"versions": {"tracks": tracks},
|
||||
}
|
||||
|
||||
|
||||
def _release(app_ver: str, build_map: dict = None) -> dict:
|
||||
return {"kind": "release", "app_ver": app_ver, "build_map": build_map or {}}
|
||||
|
||||
|
||||
def _feature(app_ver: str, build_map: dict = None) -> dict:
|
||||
return {"kind": "feature", "app_ver": app_ver, "build_map": build_map or {}}
|
||||
|
||||
|
||||
async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||
"""从服务配置读取版本号,不依赖分支源码脚本"""
|
||||
build_dir = tmp_path / "build"
|
||||
@@ -335,10 +344,7 @@ async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||
task.obfuscation = False
|
||||
task.branch = "master"
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "3.0.1", "build_map": {}},
|
||||
{"app_ver": "9.9.9", "build_map": {}},
|
||||
)
|
||||
mock_config = _versions_config({"master": _release("3.0.1"), "feature": _feature("9.9.9")})
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
@@ -356,42 +362,67 @@ async def test_generate_config_appstore_increments_per_version(tmp_path, log_str
|
||||
obfuscation=False, branch="master")
|
||||
|
||||
# 2.195.0 已打到 .5,切到 2.196.0 后又回退到 2.195.0
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5, "2.196.0": 3}},
|
||||
{"app_ver": "2.195.0", "build_map": {}},
|
||||
)
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.195.0", {"2.195.0": 5, "2.196.0": 3}),
|
||||
"feature": _feature("2.195.0"),
|
||||
})
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||
patch("backend.routers.config.save_config") as save_mock:
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
# 从 2.195.0 自身的 5 继续到 6,不受 2.196.0 影响,也不撞已传的 .5
|
||||
release_map = mock_config["versions"]["tracks"]["release"]["build_map"]
|
||||
release_map = mock_config["versions"]["tracks"]["master"]["build_map"]
|
||||
assert result["BUILD_VERSION"] == "2.195.0.6"
|
||||
assert release_map["2.195.0"] == 6
|
||||
assert release_map["2.196.0"] == 3
|
||||
save_mock.assert_called()
|
||||
|
||||
|
||||
async def test_generate_config_develop_shares_release_track(tmp_path, log_streamer):
|
||||
"""develop 与 master 共用 release 轨的版本号与构建号序列"""
|
||||
async def test_generate_config_release_branches_are_independent(tmp_path, log_streamer):
|
||||
"""master 与 develop 各用自己的轨道,版本号与构建号互不影响"""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||
obfuscation=False, branch="develop")
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5}},
|
||||
{"app_ver": "2.100.0", "build_map": {}},
|
||||
)
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.195.0", {"2.195.0": 5}),
|
||||
"develop": _release("2.196.0", {"2.196.0": 1}),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||
patch("backend.routers.config.save_config"):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
assert result["VERSION"] == "2.195.0"
|
||||
assert result["BUILD_VERSION"] == "2.195.0.6"
|
||||
assert result["VERSION"] == "2.196.0"
|
||||
assert result["BUILD_VERSION"] == "2.196.0.2"
|
||||
# master 轨完全不受影响
|
||||
assert mock_config["versions"]["tracks"]["master"]["build_map"] == {"2.195.0": 5}
|
||||
|
||||
|
||||
async def test_generate_config_build_no_skips_other_release_track(tmp_path, log_streamer):
|
||||
"""两个上架分支临时用同一 App_Ver 时,构建号跨轨道取最大值 +1,避免 App Store 重号"""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||
obfuscation=False, branch="develop")
|
||||
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.196.0", {"2.196.0": 7}),
|
||||
"develop": _release("2.196.0", {"2.196.0": 2}),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||
patch("backend.routers.config.save_config"):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
assert result["BUILD_VERSION"] == "2.196.0.8"
|
||||
assert mock_config["versions"]["tracks"]["develop"]["build_map"]["2.196.0"] == 8
|
||||
|
||||
|
||||
async def test_generate_config_feature_branch_never_increments(tmp_path, log_streamer):
|
||||
@@ -402,10 +433,10 @@ async def test_generate_config_feature_branch_never_increments(tmp_path, log_str
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||
obfuscation=False, branch="feature/login")
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5}},
|
||||
{"app_ver": "2.100.0", "build_map": {}},
|
||||
)
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.195.0", {"2.195.0": 5}),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||
patch("backend.routers.config.save_config") as save_mock:
|
||||
@@ -425,10 +456,10 @@ async def test_generate_config_branch_track_override(tmp_path, log_streamer):
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="Ad_Hoc",
|
||||
obfuscation=False, branch="release/2.196")
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.196.0", "build_map": {}},
|
||||
{"app_ver": "2.100.0", "build_map": {}},
|
||||
)
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.196.0"),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
mock_config["branch_track"] = {"release/2.196": "feature"}
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||
|
||||
Reference in New Issue
Block a user