feat: 支持按 Tag 打包,分支管理可添加 Tag
- 分支管理新增「分支 / Tag」类型选择,Tag 存于 config["tags"],与分支不可重名 - 每个 Tag 与分支一样拥有同名独立版本轨道,默认上架轨 - 打包页按「分支」「Tag」分组选择;任务新增 ref_type 字段记录引用类型 - Tag 打包时 fetch --tags --force 后以游离 HEAD 检出 refs/tags/<tag> - 历史记录与下载页按类型显示「分支」或「Tag」 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -108,6 +108,27 @@ MASTER_BRANCH_NAMES = {"master", "main"}
|
||||
RELEASE_BRANCH_NAMES = MASTER_BRANCH_NAMES | {"develop", "dev"}
|
||||
RELEASE_BRANCH_PREFIXES = ("release/", "release-", "hotfix/", "hotfix-")
|
||||
|
||||
# 可打包的代码引用类型:分支在 config["branches"],Tag 在 config["tags"],两者名字不可重复,
|
||||
# 因此任务里只存名字即可反查类型;分支与 Tag 共用 branch_track 映射各自的版本轨道。
|
||||
REF_BRANCH = "branch"
|
||||
REF_TAG = "tag"
|
||||
|
||||
|
||||
def get_tags(config: dict) -> list:
|
||||
"""取可打包的 Tag 列表。"""
|
||||
tags = config.get("tags")
|
||||
return tags if isinstance(tags, list) else []
|
||||
|
||||
|
||||
def get_ref_type(config: dict, name: str) -> str:
|
||||
"""按名字判断是分支还是 Tag。"""
|
||||
return REF_TAG if name in get_tags(config) else REF_BRANCH
|
||||
|
||||
|
||||
def _build_refs(config: dict) -> list:
|
||||
"""所有可打包的引用(分支在前,Tag 在后)。"""
|
||||
return list(config.get("branches", []) or []) + get_tags(config)
|
||||
|
||||
|
||||
def _is_release_branch(branch: str) -> bool:
|
||||
"""按分支名推断是否为上架分支。"""
|
||||
@@ -120,6 +141,13 @@ def _default_kind_for_branch(branch: str) -> str:
|
||||
return KIND_RELEASE if _is_release_branch(branch) else KIND_FEATURE
|
||||
|
||||
|
||||
def _default_kind_for_ref(config: dict, name: str) -> str:
|
||||
"""新建轨道时的缺省类型:Tag 一般用于重打已发布的版本,默认上架轨;分支按名字推断。"""
|
||||
if get_ref_type(config, name) == REF_TAG:
|
||||
return KIND_RELEASE
|
||||
return _default_kind_for_branch(name)
|
||||
|
||||
|
||||
def _default_track_for_branch(branch: str) -> str:
|
||||
"""分支自己的轨道名:master/main 沿用历史名 release,其余即分支名。
|
||||
|
||||
@@ -252,7 +280,7 @@ def _ensure_versions(config: dict) -> bool:
|
||||
branch_track = {}
|
||||
config["branch_track"] = branch_track
|
||||
changed = True
|
||||
for branch in config.get("branches", []) or []:
|
||||
for branch in _build_refs(config):
|
||||
name = branch_track.get(branch)
|
||||
if splitting and name in FIXED_TRACKS:
|
||||
# 旧的共用轨 → 各分支自己的轨道(master/main 仍留在 release 轨)
|
||||
@@ -263,7 +291,7 @@ def _ensure_versions(config: dict) -> bool:
|
||||
branch_track[branch] = name
|
||||
changed = True
|
||||
if name not in tracks:
|
||||
kind = _default_kind_for_branch(branch)
|
||||
kind = _default_kind_for_ref(config, branch)
|
||||
tracks[name] = _seed_track(tracks, kind, legacy[kind])
|
||||
if legacy[kind]:
|
||||
split_kinds.add(kind)
|
||||
@@ -336,15 +364,16 @@ 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):
|
||||
def resolve_track(config: dict, branch: str, track: str, default_kind: str = None):
|
||||
"""把用户传入的轨道值解析为 (轨道名, 轨道类型)。
|
||||
|
||||
传类型(release / feature)时用分支自己的轨道,并把它设成该类型;
|
||||
也可直接传已存在的轨道名,让这个分支与另一个分支共用同一条版本线(类型不变)。
|
||||
不传时类型取 default_kind,缺省按分支名推断。
|
||||
"""
|
||||
value = (track or "").strip()
|
||||
if not value:
|
||||
return _default_track_for_branch(branch), _default_kind_for_branch(branch)
|
||||
return _default_track_for_branch(branch), default_kind or _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 {}):
|
||||
@@ -823,6 +852,8 @@ async def add_branch(data: dict):
|
||||
branches = config.get("branches", ["main"])
|
||||
if name in branches:
|
||||
raise HTTPException(status_code=400, detail="分支已存在")
|
||||
if name in get_tags(config):
|
||||
raise HTTPException(status_code=400, detail="已存在同名 Tag")
|
||||
track, kind = resolve_track(config, name, data.get("track"))
|
||||
branches.append(name)
|
||||
config["branches"] = branches
|
||||
@@ -834,7 +865,7 @@ async def add_branch(data: dict):
|
||||
|
||||
@router.put("/branches/track")
|
||||
async def update_branch_track(data: dict):
|
||||
"""修改分支的版本轨道类型(release=上架,构建号自增;feature=自测,不自增)。
|
||||
"""修改分支或 Tag 的版本轨道类型(release=上架,构建号自增;feature=自测,不自增)。
|
||||
|
||||
每个分支用自己的一条轨道,切换类型只改这条轨道的行为,版本号仍与其它分支互不影响。
|
||||
分支名可能含 /(如 feature/login),因此走请求体而非路径参数。
|
||||
@@ -845,7 +876,7 @@ async def update_branch_track(data: dict):
|
||||
|
||||
async with _config_lock:
|
||||
config = load_config()
|
||||
if branch not in config.get("branches", []):
|
||||
if branch not in _build_refs(config):
|
||||
raise HTTPException(status_code=404, detail="分支不存在")
|
||||
track, kind = resolve_track(config, branch, data.get("track"))
|
||||
config.setdefault("branch_track", {})[branch] = track
|
||||
@@ -869,6 +900,51 @@ async def delete_branch(branch_name: str):
|
||||
return {"message": "分支已删除"}
|
||||
|
||||
|
||||
# Tag 管理 API:打包时检出指定 Tag 的代码,版本轨道与分支一样按名字各自独立
|
||||
@router.get("/tags")
|
||||
async def get_tags_list():
|
||||
"""获取 Tag 列表"""
|
||||
return get_tags(load_config())
|
||||
|
||||
|
||||
@router.post("/tags")
|
||||
async def add_tag(data: dict):
|
||||
"""新增 Tag,可指定版本轨道类型(缺省为上架轨,App_Store 打包构建号自增)。"""
|
||||
name = data.get("name", "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="Tag 名称不能为空")
|
||||
|
||||
async with _config_lock:
|
||||
config = load_config()
|
||||
tags = get_tags(config)
|
||||
if name in tags:
|
||||
raise HTTPException(status_code=400, detail="Tag 已存在")
|
||||
if name in config.get("branches", []):
|
||||
raise HTTPException(status_code=400, detail="已存在同名分支")
|
||||
track, kind = resolve_track(config, name, data.get("track"), default_kind=KIND_RELEASE)
|
||||
tags.append(name)
|
||||
config["tags"] = tags
|
||||
config.setdefault("branch_track", {})[name] = track
|
||||
_ensure_track(config, track, kind)
|
||||
save_config(config)
|
||||
return {"message": "Tag 已添加", "track": track}
|
||||
|
||||
|
||||
@router.delete("/tags/{tag_name:path}")
|
||||
async def delete_tag(tag_name: str):
|
||||
"""删除 Tag"""
|
||||
async with _config_lock:
|
||||
config = load_config()
|
||||
tags = get_tags(config)
|
||||
if tag_name not in tags:
|
||||
raise HTTPException(status_code=404, detail="Tag 不存在")
|
||||
tags.remove(tag_name)
|
||||
config["tags"] = tags
|
||||
config.get("branch_track", {}).pop(tag_name, None)
|
||||
save_config(config)
|
||||
return {"message": "Tag 已删除"}
|
||||
|
||||
|
||||
@router.get("/build")
|
||||
async def get_build_settings(db: Session = Depends(get_db)):
|
||||
"""获取打包设置"""
|
||||
@@ -941,7 +1017,7 @@ async def get_versions():
|
||||
"primary_track": primary,
|
||||
"branch_track": {
|
||||
branch: get_track_name(config, branch)
|
||||
for branch in config.get("branches", []) or []
|
||||
for branch in _build_refs(config)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user