refactor: master 沿用 release 轨道名,develop 用 develop,feature 用 feature
上一版把 master 的轨道也改名成 master,会把历史构建号搬到新键上。 改为 master/main 继续用现有的 release 轨(build_map 原地保留), 只有 develop 等其它上架分支新建以分支名命名的轨道并复制一份构建号。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJaZ913U5GVdTkYHiRpvDU
This commit is contained in:
@@ -26,7 +26,7 @@ FastAPI + Vue 3 iOS app auto-packaging service. Supports Ad_Hoc and App_Store bu
|
||||
- `backend/config.py` — loads .env, defines all config constants
|
||||
- `frontend/src/views/` — BuildView, HistoryView, ConfigView
|
||||
- `config.json` — runtime config (apps, schemes, servers, upload, branches, versions) managed via web UI
|
||||
- Versions are per-track: each release branch (master, develop, …) has its own track named after the branch (build number auto-increments on App_Store), and all self-test branches share the `feature` track (never increments); each branch's track name lives in `config["branch_track"]`, track records in `config["versions"]["tracks"]` carry `kind` (release/feature)
|
||||
- Versions are per-track: each release branch has its own track (master/main keeps the historical `release` track, others are named after the branch, e.g. `develop`; build number auto-increments on App_Store), and all self-test branches share the `feature` track (never increments); each branch's track name lives in `config["branch_track"]`, track records in `config["versions"]["tracks"]` carry `kind` (release/feature)
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -197,14 +197,15 @@ GIT_PASSWORD=<仅 read_repository 权限的 Personal Access Token>
|
||||
|
||||
| 轨道 | 适用分支 | 构建号行为 |
|
||||
|------|----------|-----------|
|
||||
| 上架轨(轨道名即分支名,如 `master`、`develop`) | 每个上架分支各一条,版本号彼此独立 | App_Store 打包时按当前 App_Ver 自动 +1 |
|
||||
| `release`(上架) | master / main | App_Store 打包时按当前 App_Ver 自动 +1 |
|
||||
| `develop` 等以分支名命名的上架轨 | develop、release-*、hotfix-* 等其它上架分支,每个分支一条 | 同上,与 `release` 轨互不影响 |
|
||||
| `feature`(自测) | feature/* 等其他分支,共用一条 | 永不自增,始终使用配置页填写的值(默认 `.0`) |
|
||||
|
||||
- 分支归属默认按名字推断(master/main/develop/dev/release-*/hotfix-* → 自己的上架轨,其余 → feature),可在「分支管理」页逐个改;改成「上架」即为该分支新建一条以分支名命名的轨道。
|
||||
- 分支归属默认按名字推断(master/main → `release` 轨,develop/dev/release-*/hotfix-* → 以分支名命名的轨,其余 → `feature`),可在「分支管理」页逐个改;改成「上架」即为该分支建一条独立轨道。
|
||||
- 「打包设置」页按上架分支分块填写版本号,最后一块是共用的自测版本号。
|
||||
- 每条轨道内的构建号按 App_Ver 独立记录(`build_map`),回退到打过包的旧版本号时会从它自己的最大值继续,不会与 App Store Connect 上已传构建号冲突。
|
||||
- App Store 的构建号唯一性按 App_Ver 全局判定,因此两个上架分支若临时用了同一个 App_Ver,递增时会跨轨道取最大值 +1,避免重号被拒。
|
||||
- 旧配置(master/develop 共用的 `release` 轨)首次启动时自动拆分为各分支自己的轨道,已用构建号原样复制过去。
|
||||
- 旧配置(master/develop 共用的 `release` 轨)首次启动时自动拆开:master 留在 `release` 轨,develop 新建 `develop` 轨并复制一份已用构建号。
|
||||
|
||||
### 服务端自动化资源
|
||||
|
||||
|
||||
+21
-19
@@ -93,16 +93,18 @@ DEFAULT_SERVERS = {
|
||||
|
||||
DEFAULT_APP_VER = "2.180.0"
|
||||
|
||||
# 版本轨道:每个上架分支(master / develop / release-* 等)各有一条独立轨道,
|
||||
# 轨道名即分支名,App_Store 打包时构建号自增;所有自测分支共用一条 feature 轨,
|
||||
# 构建号永不自增。各轨道的 App_Ver 互不影响。
|
||||
# 版本轨道:每个上架分支各有一条独立轨道,App_Store 打包时构建号自增;
|
||||
# 所有自测分支共用一条 feature 轨,构建号永不自增。各轨道的 App_Ver 互不影响。
|
||||
# 轨道名:master/main 沿用历史名称 release,其余上架分支以分支名命名(develop、hotfix-x…)。
|
||||
KIND_RELEASE = "release"
|
||||
KIND_FEATURE = "feature"
|
||||
TRACK_RELEASE = "release" # master/main 的上架轨(沿用旧名,历史构建号原地保留)
|
||||
TRACK_FEATURE = "feature" # 自测轨的固定轨道名
|
||||
LEGACY_RELEASE_TRACK = "release" # 旧版 master/develop 共用的上架轨,迁移时按分支拆开
|
||||
FIXED_TRACKS = (TRACK_RELEASE, TRACK_FEATURE)
|
||||
|
||||
# 按名字推断轨道的默认规则;config["branch_track"] 中的显式配置优先。
|
||||
RELEASE_BRANCH_NAMES = {"master", "main", "develop", "dev"}
|
||||
MASTER_BRANCH_NAMES = {"master", "main"}
|
||||
RELEASE_BRANCH_NAMES = MASTER_BRANCH_NAMES | {"develop", "dev"}
|
||||
RELEASE_BRANCH_PREFIXES = ("release/", "release-", "hotfix/", "hotfix-")
|
||||
|
||||
|
||||
@@ -113,8 +115,10 @@ def _is_release_branch(branch: str) -> bool:
|
||||
|
||||
|
||||
def _release_track_name(branch: str) -> str:
|
||||
"""上架轨道名即分支名;与固定的自测轨重名时加前缀避让。"""
|
||||
return f"release:{branch}" if branch == TRACK_FEATURE else branch
|
||||
"""上架分支的轨道名:master/main 为 release,其余即分支名(与固定轨重名时避让)。"""
|
||||
if (branch or "").strip().lower() in MASTER_BRANCH_NAMES:
|
||||
return TRACK_RELEASE
|
||||
return f"release:{branch}" if branch in FIXED_TRACKS else branch
|
||||
|
||||
|
||||
def _default_track_for_branch(branch: str) -> str:
|
||||
@@ -173,7 +177,8 @@ def _ensure_versions(config: dict) -> bool:
|
||||
"""确保 versions 为「每个上架分支一条轨 + 一条 feature 轨」结构,并迁移旧配置。
|
||||
|
||||
旧式单轨 ``{"app_ver": ..., "build_map": ...}`` 和旧的 master/develop 共用
|
||||
``release`` 轨,都会按上架分支拆成各自独立的轨道,并复制已用构建号,
|
||||
``release`` 轨,都会按上架分支拆开:master/main 留在 release 轨(历史构建号原地
|
||||
保留),develop 等其它上架分支新建以分支名命名的轨道,并复制一份已用构建号,
|
||||
使拆分后每个分支都从原来的构建号继续递增。
|
||||
"""
|
||||
versions = config.get("versions")
|
||||
@@ -191,16 +196,15 @@ def _ensure_versions(config: dict) -> bool:
|
||||
versions["tracks"] = tracks
|
||||
changed = True
|
||||
else:
|
||||
# 迁移后的轨道都带 kind;没有 kind 的 release 轨即旧的共用上架轨
|
||||
old = tracks.get(LEGACY_RELEASE_TRACK)
|
||||
if isinstance(old, dict) and "kind" not in old:
|
||||
build_map = old.get("build_map")
|
||||
# 迁移后的轨道都带 kind;没有 kind 的 release 轨即旧的 master/develop 共用轨,
|
||||
# 它继续作为 master/main 的轨道,同时作为其它上架分支新轨的初值。
|
||||
shared = tracks.get(TRACK_RELEASE)
|
||||
if isinstance(shared, dict) and "kind" not in shared:
|
||||
build_map = shared.get("build_map")
|
||||
legacy = {
|
||||
"app_ver": old.get("app_ver") or DEFAULT_APP_VER,
|
||||
"app_ver": shared.get("app_ver") or DEFAULT_APP_VER,
|
||||
"build_map": build_map if isinstance(build_map, dict) else {},
|
||||
}
|
||||
tracks.pop(LEGACY_RELEASE_TRACK)
|
||||
changed = True
|
||||
|
||||
feature = tracks.get(TRACK_FEATURE)
|
||||
if not isinstance(feature, dict):
|
||||
@@ -230,8 +234,8 @@ def _ensure_versions(config: dict) -> bool:
|
||||
changed = True
|
||||
for branch in config.get("branches", []) or []:
|
||||
name = branch_track.get(branch)
|
||||
if name == LEGACY_RELEASE_TRACK and LEGACY_RELEASE_TRACK not in tracks:
|
||||
# 旧的共用上架轨 → 该分支自己的轨
|
||||
if name == TRACK_RELEASE and legacy is not None:
|
||||
# 旧的共用上架轨 → master/main 留在 release 轨,其余分支拆到自己的轨
|
||||
name = _release_track_name(branch)
|
||||
if not isinstance(name, str) or not name:
|
||||
name = _default_track_for_branch(branch)
|
||||
@@ -263,8 +267,6 @@ def get_track_name(config: dict, branch: str) -> str:
|
||||
tracks = (config.get("versions") or {}).get("tracks") or {}
|
||||
if isinstance(name, str) and name in tracks:
|
||||
return name
|
||||
if name == LEGACY_RELEASE_TRACK: # 尚未迁移的旧配置
|
||||
return _release_track_name(branch)
|
||||
return _default_track_for_branch(branch)
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -74,10 +74,10 @@
|
||||
},
|
||||
"versions": {
|
||||
"tracks": {
|
||||
"master": { "kind": "release", "app_ver": "2.180.0", "build_map": {} },
|
||||
"release": { "kind": "release", "app_ver": "2.180.0", "build_map": {} },
|
||||
"develop": { "kind": "release", "app_ver": "2.180.0", "build_map": {} },
|
||||
"feature": { "kind": "feature", "app_ver": "2.180.0", "build_map": {} }
|
||||
}
|
||||
},
|
||||
"branch_track": { "master": "master", "develop": "develop" }
|
||||
"branch_track": { "master": "release", "develop": "develop" }
|
||||
}
|
||||
|
||||
@@ -27,13 +27,13 @@ function mockFetch(url) {
|
||||
'/api/config/versions': {
|
||||
app_ver: '2.195.0',
|
||||
build_ver: '2.195.0.0',
|
||||
primary_track: 'main',
|
||||
primary_track: 'release',
|
||||
tracks: {
|
||||
main: { kind: 'release', app_ver: '2.195.0', build_ver: '2.195.0.0', build_map: {} },
|
||||
release: { kind: 'release', app_ver: '2.195.0', build_ver: '2.195.0.0', build_map: {} },
|
||||
dev: { kind: 'release', app_ver: '2.196.0', build_ver: '2.196.0.0', build_map: {} },
|
||||
feature: { kind: 'feature', app_ver: '2.100.0', build_ver: '2.100.0.0', build_map: {} },
|
||||
},
|
||||
branch_track: { main: 'main', dev: 'dev' },
|
||||
branch_track: { main: 'release', dev: 'dev' },
|
||||
},
|
||||
'/api/config': { apps: {}, schemes: {}, branches: ['main'] },
|
||||
}
|
||||
@@ -215,8 +215,8 @@ describe('ConfigView.vue', () => {
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const inputs = wrapper.findAll('input[type="text"]')
|
||||
expect(inputs[0].element.value).toBe('2.195.0') // main(上架)App_Ver
|
||||
expect(inputs[2].element.value).toBe('2.196.0') // dev(上架)App_Ver
|
||||
expect(inputs[0].element.value).toBe('2.195.0') // main 分支(release 轨)App_Ver
|
||||
expect(inputs[2].element.value).toBe('2.196.0') // dev 分支(dev 轨)App_Ver
|
||||
expect(inputs[4].element.value).toBe('2.100.0') // feature App_Ver
|
||||
|
||||
await inputs[4].setValue('2.101.0')
|
||||
|
||||
@@ -294,7 +294,7 @@
|
||||
<div class="form-group">
|
||||
<label>App_Ver(应用版本)</label>
|
||||
<input type="text" v-model="versions[t.name].app_ver" placeholder="2.180.0" @input="onAppVerInput(t.name)">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号。该版本号只用于 {{ t.branches.join(' / ') }} 分支,与其它分支互不影响。</div>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号。该版本号只用于 {{ t.branches.join(' / ') }} 分支(轨道 {{ t.name }}),与其它分支互不影响。</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Build_Ver(构建号)</label>
|
||||
@@ -304,7 +304,7 @@
|
||||
每个版本号的构建号独立递增,App_Store 打包时自动 +1。一般无需手动修改。
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions(t.name)">保存 {{ t.name }} 版本号</button>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions(t.name)">保存 {{ t.branches.join(' / ') }} 版本号</button>
|
||||
</template>
|
||||
|
||||
<template v-if="versions.feature">
|
||||
|
||||
+20
-18
@@ -229,20 +229,21 @@ 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": "main", "app_ver": "2.196.0"}).status_code == 200
|
||||
# 每个上架分支一条轨道(main/master 为 release,其余即分支名),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": "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"]["main"]["app_ver"] == "2.196.0"
|
||||
assert data["branch_track"] == {"main": "release", "dev": "dev"}
|
||||
assert data["tracks"]["release"]["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"
|
||||
assert data["tracks"]["main"]["kind"] == "release"
|
||||
assert data["tracks"]["release"]["kind"] == "release"
|
||||
assert data["tracks"]["feature"]["kind"] == "feature"
|
||||
# 顶层字段镜像主上架轨(第一个上架分支),兼容旧客户端
|
||||
assert data["primary_track"] == "main"
|
||||
assert data["primary_track"] == "release"
|
||||
assert data["app_ver"] == "2.196.0"
|
||||
|
||||
|
||||
@@ -261,9 +262,10 @@ def test_legacy_versions_split_per_release_branch(client, tmp_config):
|
||||
json.dump(cfg, f)
|
||||
|
||||
data = client.get("/api/config/versions").json()
|
||||
for branch in ("main", "dev"):
|
||||
assert data["branch_track"][branch] == branch
|
||||
assert data["tracks"][branch] == {
|
||||
# main 留在 release 轨,dev 拆出自己的轨,两者都带着原来的已用构建号
|
||||
assert data["branch_track"] == {"main": "release", "dev": "dev"}
|
||||
for track in ("release", "dev"):
|
||||
assert data["tracks"][track] == {
|
||||
"kind": "release",
|
||||
"app_ver": "2.195.0",
|
||||
"build_ver": "2.195.0.7",
|
||||
@@ -272,15 +274,15 @@ def test_legacy_versions_split_per_release_branch(client, tmp_config):
|
||||
# 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
|
||||
# 拆分后各轨独立:改 release 不影响 dev
|
||||
assert client.put("/api/config/versions", json={"track": "release", "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"]["release"]["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 轨:拆成各分支自己的轨,构建号原样带过去"""
|
||||
"""旧的 master/develop 共用 release 轨:master 留在 release 轨,其余分支拆出自己的轨"""
|
||||
import json
|
||||
with open(tmp_config, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
@@ -293,11 +295,11 @@ def test_legacy_shared_release_track_splits_per_branch(client, tmp_config):
|
||||
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["branch_track"] == {"main": "release", "dev": "dev"}
|
||||
for track in ("release", "dev"):
|
||||
assert data["tracks"][track]["kind"] == "release"
|
||||
assert data["tracks"][track]["app_ver"] == "2.195.0"
|
||||
assert data["tracks"][track]["build_ver"] == "2.195.0.4"
|
||||
assert data["tracks"]["feature"]["app_ver"] == "2.100.0"
|
||||
|
||||
|
||||
@@ -306,7 +308,7 @@ def test_branch_track_defaults_and_override(client, tmp_config):
|
||||
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
||||
|
||||
data = client.get("/api/config/versions").json()
|
||||
# 上架分支各有一条以分支名命名的轨道,自测分支共用 feature 轨
|
||||
# 上架分支各有一条独立轨道(main/master 为 release,其余即分支名),自测分支共用 feature 轨
|
||||
assert data["branch_track"]["develop"] == "develop"
|
||||
assert data["branch_track"]["feature/login"] == "feature"
|
||||
assert data["tracks"]["develop"]["kind"] == "release"
|
||||
|
||||
@@ -344,7 +344,7 @@ async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||
task.obfuscation = False
|
||||
task.branch = "master"
|
||||
|
||||
mock_config = _versions_config({"master": _release("3.0.1"), "feature": _feature("9.9.9")})
|
||||
mock_config = _versions_config({"release": _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)
|
||||
@@ -363,7 +363,7 @@ async def test_generate_config_appstore_increments_per_version(tmp_path, log_str
|
||||
|
||||
# 2.195.0 已打到 .5,切到 2.196.0 后又回退到 2.195.0
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.195.0", {"2.195.0": 5, "2.196.0": 3}),
|
||||
"release": _release("2.195.0", {"2.195.0": 5, "2.196.0": 3}),
|
||||
"feature": _feature("2.195.0"),
|
||||
})
|
||||
|
||||
@@ -372,7 +372,7 @@ async def test_generate_config_appstore_increments_per_version(tmp_path, log_str
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
# 从 2.195.0 自身的 5 继续到 6,不受 2.196.0 影响,也不撞已传的 .5
|
||||
release_map = mock_config["versions"]["tracks"]["master"]["build_map"]
|
||||
release_map = mock_config["versions"]["tracks"]["release"]["build_map"]
|
||||
assert result["BUILD_VERSION"] == "2.195.0.6"
|
||||
assert release_map["2.195.0"] == 6
|
||||
assert release_map["2.196.0"] == 3
|
||||
@@ -388,7 +388,7 @@ async def test_generate_config_release_branches_are_independent(tmp_path, log_st
|
||||
obfuscation=False, branch="develop")
|
||||
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.195.0", {"2.195.0": 5}),
|
||||
"release": _release("2.195.0", {"2.195.0": 5}), # master
|
||||
"develop": _release("2.196.0", {"2.196.0": 1}),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
@@ -399,8 +399,8 @@ async def test_generate_config_release_branches_are_independent(tmp_path, log_st
|
||||
|
||||
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}
|
||||
# master 的 release 轨完全不受影响
|
||||
assert mock_config["versions"]["tracks"]["release"]["build_map"] == {"2.195.0": 5}
|
||||
|
||||
|
||||
async def test_generate_config_build_no_skips_other_release_track(tmp_path, log_streamer):
|
||||
@@ -412,7 +412,7 @@ async def test_generate_config_build_no_skips_other_release_track(tmp_path, log_
|
||||
obfuscation=False, branch="develop")
|
||||
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.196.0", {"2.196.0": 7}),
|
||||
"release": _release("2.196.0", {"2.196.0": 7}), # master
|
||||
"develop": _release("2.196.0", {"2.196.0": 2}),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
@@ -434,7 +434,7 @@ async def test_generate_config_feature_branch_never_increments(tmp_path, log_str
|
||||
obfuscation=False, branch="feature/login")
|
||||
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.195.0", {"2.195.0": 5}),
|
||||
"release": _release("2.195.0", {"2.195.0": 5}),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
|
||||
@@ -457,7 +457,7 @@ async def test_generate_config_branch_track_override(tmp_path, log_streamer):
|
||||
obfuscation=False, branch="release/2.196")
|
||||
|
||||
mock_config = _versions_config({
|
||||
"master": _release("2.196.0"),
|
||||
"release": _release("2.196.0"),
|
||||
"feature": _feature("2.100.0"),
|
||||
})
|
||||
mock_config["branch_track"] = {"release/2.196": "feature"}
|
||||
|
||||
Reference in New Issue
Block a user