feat: 版本轨道按分支彻底分开,自测分支不再共用 feature 轨
每个分支各有一条独立轨道(轨道名即分支名,master/main 沿用历史名 release), 轨道类型 kind 只决定构建号行为:release 上架自增、feature 自测不自增。 - branch_track 映射分支 → 轨道名,新增分支自动建同名轨道 - 迁移:旧的共用 release / feature 轨按分支拆开并复制已用构建号, 拆完没有分支引用的旧共用轨直接删除;无自测分支时保留 feature 轨作模板 - 分支管理页的下拉只改轨道类型(是否自增),不再影响版本号归属 - 打包设置页按分支分块填写版本号 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJaZ913U5GVdTkYHiRpvDU
This commit is contained in:
+114
-82
@@ -93,16 +93,17 @@ DEFAULT_SERVERS = {
|
||||
|
||||
DEFAULT_APP_VER = "2.180.0"
|
||||
|
||||
# 版本轨道:每个上架分支各有一条独立轨道,App_Store 打包时构建号自增;
|
||||
# 所有自测分支共用一条 feature 轨,构建号永不自增。各轨道的 App_Ver 互不影响。
|
||||
# 轨道名:master/main 沿用历史名称 release,其余上架分支以分支名命名(develop、hotfix-x…)。
|
||||
# 版本轨道:每个分支各有一条独立轨道,轨道名即分支名(master/main 沿用历史名称 release),
|
||||
# 各分支的 App_Ver 与构建号互不影响。轨道类型(kind)决定构建号行为:
|
||||
# release(上架)— App_Store 打包时自增;feature(自测)— 永不自增。
|
||||
KIND_RELEASE = "release"
|
||||
KIND_FEATURE = "feature"
|
||||
TRACK_RELEASE = "release" # master/main 的上架轨(沿用旧名,历史构建号原地保留)
|
||||
TRACK_FEATURE = "feature" # 自测轨的固定轨道名
|
||||
KINDS = (KIND_RELEASE, KIND_FEATURE)
|
||||
TRACK_RELEASE = "release" # master/main 的轨道名(沿用旧名,历史构建号原地保留)
|
||||
TRACK_FEATURE = "feature" # 旧版所有自测分支共用的轨道名,迁移时按分支拆开
|
||||
FIXED_TRACKS = (TRACK_RELEASE, TRACK_FEATURE)
|
||||
|
||||
# 按名字推断轨道的默认规则;config["branch_track"] 中的显式配置优先。
|
||||
# 按名字推断轨道类型的默认规则;轨道记录里的 kind 为准,此处只用于新建轨道。
|
||||
MASTER_BRANCH_NAMES = {"master", "main"}
|
||||
RELEASE_BRANCH_NAMES = MASTER_BRANCH_NAMES | {"develop", "dev"}
|
||||
RELEASE_BRANCH_PREFIXES = ("release/", "release-", "hotfix/", "hotfix-")
|
||||
@@ -114,16 +115,19 @@ def _is_release_branch(branch: str) -> bool:
|
||||
return name in RELEASE_BRANCH_NAMES or name.startswith(RELEASE_BRANCH_PREFIXES)
|
||||
|
||||
|
||||
def _release_track_name(branch: str) -> str:
|
||||
"""上架分支的轨道名: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_kind_for_branch(branch: str) -> str:
|
||||
"""新建轨道时按分支名推断轨道类型。"""
|
||||
return KIND_RELEASE if _is_release_branch(branch) else KIND_FEATURE
|
||||
|
||||
|
||||
def _default_track_for_branch(branch: str) -> str:
|
||||
"""按分支名推断所属版本轨道名。"""
|
||||
return _release_track_name(branch) if _is_release_branch(branch) else TRACK_FEATURE
|
||||
"""分支自己的轨道名:master/main 沿用历史名 release,其余即分支名。
|
||||
|
||||
分支恰好叫 release / feature(且不是 master)时加前缀避让,免得撞上固定轨道名。
|
||||
"""
|
||||
if (branch or "").strip().lower() in MASTER_BRANCH_NAMES:
|
||||
return TRACK_RELEASE
|
||||
return f"branch:{branch}" if branch in FIXED_TRACKS else branch
|
||||
|
||||
|
||||
def _new_track(app_ver: str = DEFAULT_APP_VER, kind: str = KIND_RELEASE) -> dict:
|
||||
@@ -133,28 +137,40 @@ def _new_track(app_ver: str = DEFAULT_APP_VER, kind: str = KIND_RELEASE) -> dict
|
||||
return {"kind": kind, "app_ver": app_ver, "build_map": {}}
|
||||
|
||||
|
||||
def _seed_release_track(tracks: dict, legacy: dict = None) -> dict:
|
||||
"""新建上架轨的初值:优先沿用旧共用轨(含已用构建号),否则沿用任一上架轨的 App_Ver。"""
|
||||
def _seed_track(tracks: dict, kind: str, legacy: dict = None) -> dict:
|
||||
"""新建轨道的初值:优先沿用被拆分的旧共用轨(含已用构建号),
|
||||
否则沿用任一同类型轨道的 App_Ver,都没有则用默认版本号。"""
|
||||
if legacy:
|
||||
return {"kind": KIND_RELEASE, "app_ver": legacy["app_ver"], "build_map": dict(legacy["build_map"])}
|
||||
return {"kind": kind, "app_ver": legacy["app_ver"], "build_map": dict(legacy["build_map"])}
|
||||
for track in tracks.values():
|
||||
if track.get("kind") == KIND_RELEASE:
|
||||
return _new_track(track.get("app_ver") or DEFAULT_APP_VER)
|
||||
return _new_track()
|
||||
if isinstance(track, dict) and track.get("kind") == kind:
|
||||
return _new_track(track.get("app_ver") or DEFAULT_APP_VER, kind)
|
||||
return _new_track(kind=kind)
|
||||
|
||||
|
||||
def _ensure_track(config: dict, track_name: str) -> dict:
|
||||
"""确保轨道存在;新建的上架轨沿用已有上架轨的 App_Ver,构建号从 .0 起。"""
|
||||
def _ensure_track(config: dict, track_name: str, kind: str = None) -> dict:
|
||||
"""确保轨道存在(新建时沿用同类型轨道的 App_Ver,构建号从 .0 起);
|
||||
显式传 kind 时同时把轨道类型改成它。"""
|
||||
tracks = config.setdefault("versions", {}).setdefault("tracks", {})
|
||||
track = tracks.get(track_name)
|
||||
if not isinstance(track, dict):
|
||||
track = tracks[track_name] = (
|
||||
_new_track(kind=KIND_FEATURE) if track_name == TRACK_FEATURE
|
||||
else _seed_release_track(tracks)
|
||||
track = tracks[track_name] = _seed_track(
|
||||
tracks, kind or _default_kind_for_track(track_name)
|
||||
)
|
||||
elif kind:
|
||||
track["kind"] = kind
|
||||
return track
|
||||
|
||||
|
||||
def _default_kind_for_track(track_name: str) -> str:
|
||||
"""轨道记录缺 kind 时按轨道名推断(轨道名即分支名)。"""
|
||||
if track_name == TRACK_RELEASE:
|
||||
return KIND_RELEASE
|
||||
if track_name == TRACK_FEATURE:
|
||||
return KIND_FEATURE
|
||||
return _default_kind_for_branch(track_name)
|
||||
|
||||
|
||||
def _legacy_single_track(versions: dict) -> dict:
|
||||
"""把最旧的单轨结构读成 {app_ver, build_map}。"""
|
||||
app_ver = versions.get("app_ver") or DEFAULT_APP_VER
|
||||
@@ -173,13 +189,25 @@ def _legacy_single_track(versions: dict) -> dict:
|
||||
return {"app_ver": app_ver, "build_map": build_map}
|
||||
|
||||
|
||||
def _ensure_versions(config: dict) -> bool:
|
||||
"""确保 versions 为「每个上架分支一条轨 + 一条 feature 轨」结构,并迁移旧配置。
|
||||
def _legacy_shared_track(tracks: dict, name: str) -> dict:
|
||||
"""取出待拆分的旧共用轨(迁移后的轨道都带 kind,没有 kind 的即旧共用轨)。"""
|
||||
track = tracks.get(name)
|
||||
if not isinstance(track, dict) or "kind" in track:
|
||||
return None
|
||||
build_map = track.get("build_map")
|
||||
return {
|
||||
"app_ver": track.get("app_ver") or DEFAULT_APP_VER,
|
||||
"build_map": build_map if isinstance(build_map, dict) else {},
|
||||
}
|
||||
|
||||
旧式单轨 ``{"app_ver": ..., "build_map": ...}`` 和旧的 master/develop 共用
|
||||
``release`` 轨,都会按上架分支拆开:master/main 留在 release 轨(历史构建号原地
|
||||
保留),develop 等其它上架分支新建以分支名命名的轨道,并复制一份已用构建号,
|
||||
使拆分后每个分支都从原来的构建号继续递增。
|
||||
|
||||
def _ensure_versions(config: dict) -> bool:
|
||||
"""确保 versions 为「每个分支一条轨道」结构,并迁移旧配置。
|
||||
|
||||
旧式单轨 ``{"app_ver": ..., "build_map": ...}``、旧的 master/develop 共用
|
||||
``release`` 轨和所有自测分支共用的 ``feature`` 轨,都会按分支拆开:
|
||||
master/main 留在 release 轨(历史构建号原地保留),其余分支新建以分支名命名的
|
||||
轨道并复制一份已用构建号,使拆分后每个分支都从原来的构建号继续。
|
||||
"""
|
||||
versions = config.get("versions")
|
||||
changed = False
|
||||
@@ -188,36 +216,28 @@ def _ensure_versions(config: dict) -> bool:
|
||||
config["versions"] = versions
|
||||
changed = True
|
||||
|
||||
legacy = None # 需要拆分到各上架分支的旧共用轨
|
||||
# 待拆分到各分支的旧共用轨(按类型各一条)
|
||||
legacy = {KIND_RELEASE: None, KIND_FEATURE: None}
|
||||
tracks = versions.get("tracks")
|
||||
if not isinstance(tracks, dict):
|
||||
legacy = _legacy_single_track(versions)
|
||||
legacy[KIND_RELEASE] = _legacy_single_track(versions)
|
||||
legacy[KIND_FEATURE] = dict(legacy[KIND_RELEASE], build_map={})
|
||||
tracks = {}
|
||||
versions["tracks"] = tracks
|
||||
changed = True
|
||||
else:
|
||||
# 迁移后的轨道都带 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": shared.get("app_ver") or DEFAULT_APP_VER,
|
||||
"build_map": build_map if isinstance(build_map, dict) else {},
|
||||
}
|
||||
|
||||
feature = tracks.get(TRACK_FEATURE)
|
||||
if not isinstance(feature, dict):
|
||||
tracks[TRACK_FEATURE] = _new_track((legacy or {}).get("app_ver") or DEFAULT_APP_VER, KIND_FEATURE)
|
||||
changed = True
|
||||
# release 轨拆分后继续作为 master/main 的轨道,feature 轨拆分后成为无人引用的模板
|
||||
legacy[KIND_RELEASE] = _legacy_shared_track(tracks, TRACK_RELEASE)
|
||||
legacy[KIND_FEATURE] = _legacy_shared_track(tracks, TRACK_FEATURE)
|
||||
splitting = any(legacy.values())
|
||||
split_kinds = set() # 实际被拆分出去的旧共用轨类型
|
||||
|
||||
for name, track in tracks.items():
|
||||
if not isinstance(track, dict):
|
||||
track = tracks[name] = _new_track()
|
||||
track = tracks[name] = _new_track(kind=_default_kind_for_track(name))
|
||||
changed = True
|
||||
kind = KIND_FEATURE if name == TRACK_FEATURE else KIND_RELEASE
|
||||
if track.get("kind") != kind:
|
||||
track["kind"] = kind
|
||||
if track.get("kind") not in KINDS:
|
||||
track["kind"] = _default_kind_for_track(name)
|
||||
changed = True
|
||||
if not track.get("app_ver"):
|
||||
track["app_ver"] = DEFAULT_APP_VER
|
||||
@@ -226,7 +246,7 @@ def _ensure_versions(config: dict) -> bool:
|
||||
track["build_map"] = {}
|
||||
changed = True
|
||||
|
||||
# 分支归属:值为轨道名,缺省按分支名规则推断
|
||||
# 分支归属:值为轨道名,缺省用分支自己的轨道
|
||||
branch_track = config.get("branch_track")
|
||||
if not isinstance(branch_track, dict):
|
||||
branch_track = {}
|
||||
@@ -234,31 +254,44 @@ def _ensure_versions(config: dict) -> bool:
|
||||
changed = True
|
||||
for branch in config.get("branches", []) or []:
|
||||
name = branch_track.get(branch)
|
||||
if name == TRACK_RELEASE and legacy is not None:
|
||||
# 旧的共用上架轨 → master/main 留在 release 轨,其余分支拆到自己的轨
|
||||
name = _release_track_name(branch)
|
||||
if splitting and name in FIXED_TRACKS:
|
||||
# 旧的共用轨 → 各分支自己的轨道(master/main 仍留在 release 轨)
|
||||
name = _default_track_for_branch(branch)
|
||||
if not isinstance(name, str) or not name:
|
||||
name = _default_track_for_branch(branch)
|
||||
if branch_track.get(branch) != name:
|
||||
branch_track[branch] = name
|
||||
changed = True
|
||||
if name not in tracks:
|
||||
tracks[name] = _seed_release_track(tracks, legacy)
|
||||
kind = _default_kind_for_branch(branch)
|
||||
tracks[name] = _seed_track(tracks, kind, legacy[kind])
|
||||
if legacy[kind]:
|
||||
split_kinds.add(kind)
|
||||
changed = True
|
||||
|
||||
# 内容已复制到各分支轨道、又没有分支引用的旧共用轨,直接删掉,免得配置里留下无用条目
|
||||
for name in FIXED_TRACKS:
|
||||
kind = _default_kind_for_track(name)
|
||||
if kind in split_kinds and name in tracks and name not in branch_track.values():
|
||||
tracks.pop(name)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def _primary_track_name(config: dict) -> str:
|
||||
"""主上架轨:第一个上架分支所属的轨道;没有上架分支时退回 feature 轨。
|
||||
"""主上架轨:第一个上架分支(kind=release)所属的轨道;都没有则取第一个分支的轨道。
|
||||
|
||||
仅用于顶层 app_ver/build_ver 的兼容字段和未指定轨道时的缺省值。
|
||||
"""
|
||||
for branch in config.get("branches", []) or []:
|
||||
fallback = TRACK_RELEASE
|
||||
for index, branch in enumerate(config.get("branches", []) or []):
|
||||
name = get_track_name(config, branch)
|
||||
if name != TRACK_FEATURE:
|
||||
if index == 0:
|
||||
fallback = name
|
||||
if is_release_track(get_track(config, name)):
|
||||
return name
|
||||
return TRACK_FEATURE
|
||||
return fallback
|
||||
|
||||
|
||||
def get_track_name(config: dict, branch: str) -> str:
|
||||
@@ -273,14 +306,14 @@ def get_track_name(config: dict, branch: str) -> str:
|
||||
def get_track(config: dict, track_name: str) -> dict:
|
||||
"""取某条轨道的版本记录副本(不存在时返回默认值;只读,不写回配置)。
|
||||
|
||||
未迁移的旧记录没有 kind,按轨道名补上:只有固定的 feature 轨是自测轨。
|
||||
未迁移的旧记录没有 kind,按轨道名(即分支名)推断补上。
|
||||
"""
|
||||
tracks = (config.get("versions") or {}).get("tracks") or {}
|
||||
kind = KIND_FEATURE if track_name == TRACK_FEATURE else KIND_RELEASE
|
||||
track = tracks.get(track_name)
|
||||
kind = _default_kind_for_track(track_name)
|
||||
if not isinstance(track, dict):
|
||||
return _new_track(kind=kind)
|
||||
return {**track, "kind": track.get("kind") or kind}
|
||||
return {**track, "kind": track.get("kind") if track.get("kind") in KINDS else kind}
|
||||
|
||||
|
||||
def is_release_track(track: dict) -> bool:
|
||||
@@ -303,21 +336,19 @@ def next_build_no(tracks: dict, app_ver: str) -> int:
|
||||
return max(used, default=0) + 1
|
||||
|
||||
|
||||
def resolve_track(config: dict, branch: str, track: str) -> str:
|
||||
"""把用户传入的轨道值解析为轨道名。
|
||||
def resolve_track(config: dict, branch: str, track: str):
|
||||
"""把用户传入的轨道值解析为 (轨道名, 轨道类型)。
|
||||
|
||||
兼容按类型指定:``release`` 表示该分支自己的上架轨,``feature`` 表示共用自测轨;
|
||||
也可直接传已存在的轨道名(多个分支共用同一条版本线)。
|
||||
传类型(release / feature)时用分支自己的轨道,并把它设成该类型;
|
||||
也可直接传已存在的轨道名,让这个分支与另一个分支共用同一条版本线(类型不变)。
|
||||
"""
|
||||
value = (track or "").strip()
|
||||
if not value:
|
||||
return _default_track_for_branch(branch)
|
||||
if value == KIND_FEATURE:
|
||||
return TRACK_FEATURE
|
||||
if value == KIND_RELEASE:
|
||||
return _release_track_name(branch)
|
||||
return _default_track_for_branch(branch), _default_kind_for_branch(branch)
|
||||
if value in KINDS:
|
||||
return _default_track_for_branch(branch), value
|
||||
if value in ((config.get("versions") or {}).get("tracks") or {}):
|
||||
return value
|
||||
return value, None
|
||||
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track}")
|
||||
|
||||
|
||||
@@ -777,9 +808,10 @@ async def get_branches():
|
||||
|
||||
@router.post("/branches")
|
||||
async def add_branch(data: dict):
|
||||
"""新增分支,可指定所属版本轨道(缺省按分支名推断)。
|
||||
"""新增分支,可指定版本轨道类型(缺省按分支名推断)。
|
||||
|
||||
track 传 release 表示这个分支用自己独立的上架版本号,feature 表示共用自测版本号;
|
||||
每个分支都有自己独立的一条轨道(轨道名即分支名):track 传 release 表示上架用、
|
||||
App_Store 打包时构建号自增,feature 表示自测用、构建号不自增;
|
||||
也可直接传已有轨道名,让该分支与另一个分支共用同一条版本线。
|
||||
"""
|
||||
name = data.get("name", "").strip()
|
||||
@@ -791,21 +823,21 @@ async def add_branch(data: dict):
|
||||
branches = config.get("branches", ["main"])
|
||||
if name in branches:
|
||||
raise HTTPException(status_code=400, detail="分支已存在")
|
||||
track = resolve_track(config, name, data.get("track"))
|
||||
track, kind = resolve_track(config, name, data.get("track"))
|
||||
branches.append(name)
|
||||
config["branches"] = branches
|
||||
config.setdefault("branch_track", {})[name] = track
|
||||
_ensure_track(config, track)
|
||||
_ensure_track(config, track, kind)
|
||||
save_config(config)
|
||||
return {"message": "分支已添加", "track": track}
|
||||
|
||||
|
||||
@router.put("/branches/track")
|
||||
async def update_branch_track(data: dict):
|
||||
"""修改分支所属的版本轨道(release=该分支独立的上架版本号,feature=共用自测版本号)。
|
||||
"""修改分支的版本轨道类型(release=上架,构建号自增;feature=自测,不自增)。
|
||||
|
||||
改成 release 时会为这个分支建一条以分支名命名的轨道(初值沿用已有上架轨的 App_Ver),
|
||||
此后它的版本号与其它分支互不影响。分支名可能含 /(如 feature/login),因此走请求体而非路径参数。
|
||||
每个分支用自己的一条轨道,切换类型只改这条轨道的行为,版本号仍与其它分支互不影响。
|
||||
分支名可能含 /(如 feature/login),因此走请求体而非路径参数。
|
||||
"""
|
||||
branch = (data.get("branch") or "").strip()
|
||||
if not (data.get("track") or "").strip():
|
||||
@@ -815,9 +847,9 @@ async def update_branch_track(data: dict):
|
||||
config = load_config()
|
||||
if branch not in config.get("branches", []):
|
||||
raise HTTPException(status_code=404, detail="分支不存在")
|
||||
track = resolve_track(config, branch, data.get("track"))
|
||||
track, kind = resolve_track(config, branch, data.get("track"))
|
||||
config.setdefault("branch_track", {})[branch] = track
|
||||
_ensure_track(config, track)
|
||||
_ensure_track(config, track, kind)
|
||||
save_config(config)
|
||||
return {"message": "分支版本轨道已更新", "branch": branch, "track": track}
|
||||
|
||||
@@ -894,7 +926,7 @@ async def get_versions():
|
||||
config = load_config()
|
||||
tracks = {
|
||||
name: {
|
||||
"kind": KIND_FEATURE if name == TRACK_FEATURE else KIND_RELEASE,
|
||||
"kind": track.get("kind") if track.get("kind") in KINDS else _default_kind_for_track(name),
|
||||
"app_ver": track.get("app_ver") or DEFAULT_APP_VER,
|
||||
"build_ver": track_build_ver(track),
|
||||
"build_map": track.get("build_map") or {},
|
||||
|
||||
Reference in New Issue
Block a user