feat: 版本号按分支轨道分开管理,版本号配置收归管理员
master/develop 共用 release 轨(App_Store 打包构建号自增),feature 分支走
feature 轨(构建号永不自增,始终使用手填值),两条轨道的 App_Ver 各自独立。
- versions 改为 tracks 结构,旧的单轨/单值配置在加载时自动迁移进 release 轨
- 新增 config["branch_track"] 记录分支归属,缺省按分支名推断,可在分支管理页改
- 新增 PUT /api/config/branches/track;分支名含 / 时走请求体而非路径参数
- 修复 DELETE /api/config/branches/{name} 无法删除含 / 的分支(405)
- 版本号接口不再对普通用户开放,打包设置页整页改为管理员可见
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -201,3 +201,64 @@ def test_update_versions_rejects_invalid_app_version(client, tmp_config):
|
||||
resp = client.put("/api/config/versions", json={"app_ver": "2.196"})
|
||||
assert resp.status_code == 400
|
||||
assert "App_Ver" in resp.json()["detail"]
|
||||
|
||||
|
||||
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
|
||||
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"]["feature"]["app_ver"] == "2.100.0"
|
||||
assert data["tracks"]["feature"]["build_ver"] == "2.100.0.0"
|
||||
# 顶层字段镜像 release 轨,兼容旧客户端
|
||||
assert data["app_ver"] == "2.196.0"
|
||||
|
||||
|
||||
def test_update_versions_rejects_unknown_track(client, tmp_config):
|
||||
resp = client.put("/api/config/versions", json={"track": "beta", "app_ver": "2.196.0"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_legacy_versions_migrate_to_release_track(client, tmp_config):
|
||||
import json
|
||||
with open(tmp_config, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
cfg["versions"] = {"app_ver": "2.195.0", "build_ver": "2.195.0.7"}
|
||||
with open(tmp_config, "w", encoding="utf-8") as f:
|
||||
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},
|
||||
}
|
||||
# feature 轨以同一 App_Ver 初始化,构建号从 .0 起
|
||||
assert data["tracks"]["feature"]["app_ver"] == "2.195.0"
|
||||
assert data["tracks"]["feature"]["build_ver"] == "2.195.0.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"
|
||||
|
||||
# 手动改归属
|
||||
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 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
|
||||
|
||||
|
||||
def test_delete_branch_with_slash_in_name(client, tmp_config):
|
||||
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
||||
resp = client.delete("/api/config/branches/feature%2Flogin")
|
||||
assert resp.status_code == 200
|
||||
assert "feature/login" not in client.get("/api/config/branches").json()
|
||||
|
||||
+86
-13
@@ -315,6 +315,14 @@ 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:
|
||||
return {
|
||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
||||
"versions": {"tracks": {"release": release, "feature": feature}},
|
||||
}
|
||||
|
||||
|
||||
async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||
"""从服务配置读取版本号,不依赖分支源码脚本"""
|
||||
build_dir = tmp_path / "build"
|
||||
@@ -325,12 +333,12 @@ async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||
task.scheme_id = "1"
|
||||
task.build_type = "Ad_Hoc"
|
||||
task.obfuscation = False
|
||||
task.branch = "master"
|
||||
|
||||
mock_config = {
|
||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
||||
"versions": {"app_ver": "3.0.1", "build_ver": "3.0.1.0"},
|
||||
}
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "3.0.1", "build_map": {}},
|
||||
{"app_ver": "9.9.9", "build_map": {}},
|
||||
)
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
@@ -344,26 +352,91 @@ async def test_generate_config_appstore_increments_per_version(tmp_path, log_str
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store", obfuscation=False)
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||
obfuscation=False, branch="master")
|
||||
|
||||
# 2.195.0 已打到 .5,切到 2.196.0 后又回退到 2.195.0
|
||||
mock_config = {
|
||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
||||
"versions": {"app_ver": "2.195.0", "build_map": {"2.195.0": 5, "2.196.0": 3}},
|
||||
}
|
||||
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": {}},
|
||||
)
|
||||
|
||||
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"]
|
||||
assert result["BUILD_VERSION"] == "2.195.0.6"
|
||||
assert mock_config["versions"]["build_map"]["2.195.0"] == 6
|
||||
assert mock_config["versions"]["build_map"]["2.196.0"] == 3
|
||||
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 轨的版本号与构建号序列"""
|
||||
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": {}},
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
async def test_generate_config_feature_branch_never_increments(tmp_path, log_streamer):
|
||||
"""feature 分支用自己的版本号,且构建号永不自增(即使是 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="feature/login")
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5}},
|
||||
{"app_ver": "2.100.0", "build_map": {}},
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
assert result["VERSION"] == "2.100.0"
|
||||
assert result["BUILD_VERSION"] == "2.100.0.0"
|
||||
assert mock_config["versions"]["tracks"]["feature"]["build_map"] == {}
|
||||
save_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_generate_config_branch_track_override(tmp_path, log_streamer):
|
||||
"""branch_track 中的显式配置优先于按分支名推断"""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
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["branch_track"] = {"release/2.196": "feature"}
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
assert result["VERSION"] == "2.100.0"
|
||||
|
||||
|
||||
async def test_generate_config_uses_server_theme_not_source_autopacking(tmp_path, log_streamer):
|
||||
"""Web 打包不读取分支源码中的 AutoPacking 目录。"""
|
||||
build_dir = tmp_path / "build"
|
||||
|
||||
@@ -37,8 +37,8 @@ def test_production_rejects_default_security_settings(monkeypatch):
|
||||
config.validate_production_security()
|
||||
|
||||
|
||||
def test_config_routes_limit_regular_users_to_apps_and_versions(client, tmp_config):
|
||||
"""普通账号可管理 Apps 与版本号,不能读取或修改管理员配置。"""
|
||||
def test_config_routes_limit_regular_users_to_apps(client, tmp_config):
|
||||
"""普通账号只能管理 Apps,版本号等其余配置均为管理员专属。"""
|
||||
response = client.post("/api/users", json={
|
||||
"username": "builder",
|
||||
"password": "builder-password-123",
|
||||
@@ -57,10 +57,13 @@ def test_config_routes_limit_regular_users_to_apps_and_versions(client, tmp_conf
|
||||
assert client.post("/api/config/apps", json={
|
||||
"name": "普通用户 App", "server": "测试环境",
|
||||
}, headers=headers).status_code == 200
|
||||
assert client.get("/api/config/versions", headers=headers).status_code == 200
|
||||
versions = client.put("/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers)
|
||||
assert versions.status_code == 200
|
||||
assert versions.json()["build_ver"] == "2.196.0.0"
|
||||
assert client.get("/api/config/versions", headers=headers).status_code == 403
|
||||
assert client.put(
|
||||
"/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers
|
||||
).status_code == 403
|
||||
assert client.put(
|
||||
"/api/config/branches/track", json={"branch": "master", "track": "feature"}, headers=headers
|
||||
).status_code == 403
|
||||
assert client.get("/api/config", headers=headers).status_code == 403
|
||||
assert client.get("/api/config/servers", headers=headers).status_code == 403
|
||||
assert client.put("/api/config/build", json={"max_concurrent_builds": 1}, headers=headers).status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user