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:
@@ -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
|
- `backend/config.py` — loads .env, defines all config constants
|
||||||
- `frontend/src/views/` — BuildView, HistoryView, ConfigView
|
- `frontend/src/views/` — BuildView, HistoryView, ConfigView
|
||||||
- `config.json` — runtime config (apps, schemes, servers, upload, branches, versions) managed via web UI
|
- `config.json` — runtime config (apps, schemes, servers, upload, branches, versions) managed via web UI
|
||||||
- Versions are per-track: `release` (master/develop, build number auto-increments on App_Store) and `feature` (self-test, never increments); each branch's track lives in `config["branch_track"]`
|
- 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)
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
@@ -187,21 +187,24 @@ GIT_PASSWORD=<仅 read_repository 权限的 Personal Access Token>
|
|||||||
| App 管理 | 应用名称、Scheme、服务器环境、upload_key 等 |
|
| App 管理 | 应用名称、Scheme、服务器环境、upload_key 等 |
|
||||||
| Scheme 管理 | 打包 Scheme 配置 |
|
| Scheme 管理 | 打包 Scheme 配置 |
|
||||||
| 服务器环境 | 测试/正式/自定义环境的 API 地址、Universal Link 等 |
|
| 服务器环境 | 测试/正式/自定义环境的 API 地址、Universal Link 等 |
|
||||||
| 分支管理 | 可打包的代码分支列表,以及每个分支所属的版本轨道 |
|
| 分支管理 | 可打包的代码分支列表,以及每个分支所属的版本轨道(上架分支各自独立) |
|
||||||
| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码;按版本轨道分开管理(见下) |
|
| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码;按版本轨道分开管理(见下) |
|
||||||
| 上传配置 | 服务端内置 OSS / WebDAV 分发,生成 IPA、manifest、下载页和二维码 |
|
| 上传配置 | 服务端内置 OSS / WebDAV 分发,生成 IPA、manifest、下载页和二维码 |
|
||||||
|
|
||||||
### 版本号与版本轨道
|
### 版本号与版本轨道
|
||||||
|
|
||||||
版本号按 **轨道(track)** 分开管理,两条轨道的 App_Ver 各自独立填写、互不影响:
|
版本号按 **轨道(track)** 分开管理,各轨道的 App_Ver 各自独立填写、互不影响:
|
||||||
|
|
||||||
| 轨道 | 适用分支 | 构建号行为 |
|
| 轨道 | 适用分支 | 构建号行为 |
|
||||||
|------|----------|-----------|
|
|------|----------|-----------|
|
||||||
| `release`(上架) | master、develop(共用同一套版本号与构建号序列) | App_Store 打包时按当前 App_Ver 自动 +1 |
|
| 上架轨(轨道名即分支名,如 `master`、`develop`) | 每个上架分支各一条,版本号彼此独立 | App_Store 打包时按当前 App_Ver 自动 +1 |
|
||||||
| `feature`(自测) | feature/* 等其他分支 | 永不自增,始终使用配置页填写的值(默认 `.0`) |
|
| `feature`(自测) | feature/* 等其他分支,共用一条 | 永不自增,始终使用配置页填写的值(默认 `.0`) |
|
||||||
|
|
||||||
- 分支归属默认按名字推断(master/main/develop/dev/release-*/hotfix-* → release,其余 → feature),可在「分支管理」页逐个改。
|
- 分支归属默认按名字推断(master/main/develop/dev/release-*/hotfix-* → 自己的上架轨,其余 → feature),可在「分支管理」页逐个改;改成「上架」即为该分支新建一条以分支名命名的轨道。
|
||||||
|
- 「打包设置」页按上架分支分块填写版本号,最后一块是共用的自测版本号。
|
||||||
- 每条轨道内的构建号按 App_Ver 独立记录(`build_map`),回退到打过包的旧版本号时会从它自己的最大值继续,不会与 App Store Connect 上已传构建号冲突。
|
- 每条轨道内的构建号按 App_Ver 独立记录(`build_map`),回退到打过包的旧版本号时会从它自己的最大值继续,不会与 App Store Connect 上已传构建号冲突。
|
||||||
|
- App Store 的构建号唯一性按 App_Ver 全局判定,因此两个上架分支若临时用了同一个 App_Ver,递增时会跨轨道取最大值 +1,避免重号被拒。
|
||||||
|
- 旧配置(master/develop 共用的 `release` 轨)首次启动时自动拆分为各分支自己的轨道,已用构建号原样复制过去。
|
||||||
|
|
||||||
### 服务端自动化资源
|
### 服务端自动化资源
|
||||||
|
|
||||||
|
|||||||
+203
-70
@@ -93,74 +93,128 @@ DEFAULT_SERVERS = {
|
|||||||
|
|
||||||
DEFAULT_APP_VER = "2.180.0"
|
DEFAULT_APP_VER = "2.180.0"
|
||||||
|
|
||||||
# 版本轨道:上架分支(master/develop)共用 release,自测分支(feature/*)用 feature。
|
# 版本轨道:每个上架分支(master / develop / release-* 等)各有一条独立轨道,
|
||||||
# 两条轨道的 App_Ver 各自独立填写,互不影响。
|
# 轨道名即分支名,App_Store 打包时构建号自增;所有自测分支共用一条 feature 轨,
|
||||||
TRACK_RELEASE = "release"
|
# 构建号永不自增。各轨道的 App_Ver 互不影响。
|
||||||
TRACK_FEATURE = "feature"
|
KIND_RELEASE = "release"
|
||||||
TRACKS = (TRACK_RELEASE, TRACK_FEATURE)
|
KIND_FEATURE = "feature"
|
||||||
|
TRACK_FEATURE = "feature" # 自测轨的固定轨道名
|
||||||
|
LEGACY_RELEASE_TRACK = "release" # 旧版 master/develop 共用的上架轨,迁移时按分支拆开
|
||||||
|
|
||||||
# 按名字推断轨道的默认规则;config["branch_track"] 中的显式配置优先。
|
# 按名字推断轨道的默认规则;config["branch_track"] 中的显式配置优先。
|
||||||
RELEASE_BRANCH_NAMES = {"master", "main", "develop", "dev"}
|
RELEASE_BRANCH_NAMES = {"master", "main", "develop", "dev"}
|
||||||
RELEASE_BRANCH_PREFIXES = ("release/", "release-", "hotfix/", "hotfix-")
|
RELEASE_BRANCH_PREFIXES = ("release/", "release-", "hotfix/", "hotfix-")
|
||||||
|
|
||||||
|
|
||||||
def _default_track_for_branch(branch: str) -> str:
|
def _is_release_branch(branch: str) -> bool:
|
||||||
"""按分支名推断所属版本轨道。"""
|
"""按分支名推断是否为上架分支。"""
|
||||||
name = (branch or "").strip().lower()
|
name = (branch or "").strip().lower()
|
||||||
if name in RELEASE_BRANCH_NAMES or name.startswith(RELEASE_BRANCH_PREFIXES):
|
return name in RELEASE_BRANCH_NAMES or name.startswith(RELEASE_BRANCH_PREFIXES)
|
||||||
return TRACK_RELEASE
|
|
||||||
return TRACK_FEATURE
|
|
||||||
|
|
||||||
|
|
||||||
def _new_track(app_ver: str = DEFAULT_APP_VER) -> dict:
|
def _release_track_name(branch: str) -> str:
|
||||||
|
"""上架轨道名即分支名;与固定的自测轨重名时加前缀避让。"""
|
||||||
|
return f"release:{branch}" if branch == TRACK_FEATURE else branch
|
||||||
|
|
||||||
|
|
||||||
|
def _default_track_for_branch(branch: str) -> str:
|
||||||
|
"""按分支名推断所属版本轨道名。"""
|
||||||
|
return _release_track_name(branch) if _is_release_branch(branch) else TRACK_FEATURE
|
||||||
|
|
||||||
|
|
||||||
|
def _new_track(app_ver: str = DEFAULT_APP_VER, kind: str = KIND_RELEASE) -> dict:
|
||||||
# 每个版本号(App_Ver)独立记录已用到的最大构建号第四位。
|
# 每个版本号(App_Ver)独立记录已用到的最大构建号第四位。
|
||||||
# App Store 的构建号唯一性按 MARKETING_VERSION(即完整 App_Ver) 分别计算,
|
# App Store 的构建号唯一性按 MARKETING_VERSION(即完整 App_Ver) 分别计算,
|
||||||
# 因此必须按版本号分别记录,回退到旧版本号时才能从它自己的最大值继续递增。
|
# 因此必须按版本号分别记录,回退到旧版本号时才能从它自己的最大值继续递增。
|
||||||
return {"app_ver": app_ver, "build_map": {}}
|
return {"kind": kind, "app_ver": app_ver, "build_map": {}}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_versions(config: dict) -> bool:
|
def _seed_release_track(tracks: dict, legacy: dict = None) -> dict:
|
||||||
"""确保 versions 为双轨结构,并迁移旧式单轨/单值 build_ver 配置。
|
"""新建上架轨的初值:优先沿用旧共用轨(含已用构建号),否则沿用任一上架轨的 App_Ver。"""
|
||||||
|
if legacy:
|
||||||
|
return {"kind": KIND_RELEASE, "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()
|
||||||
|
|
||||||
旧式 ``{"app_ver": ..., "build_map": ...}`` 整体迁入 release 轨;
|
|
||||||
feature 轨用同一个 App_Ver 初始化,构建号从 .0 起且永不自增。
|
|
||||||
"""
|
|
||||||
versions = config.get("versions")
|
|
||||||
changed = False
|
|
||||||
|
|
||||||
if not isinstance(versions, dict):
|
def _ensure_track(config: dict, track_name: str) -> dict:
|
||||||
config["versions"] = {"tracks": {TRACK_RELEASE: _new_track(), TRACK_FEATURE: _new_track()}}
|
"""确保轨道存在;新建的上架轨沿用已有上架轨的 App_Ver,构建号从 .0 起。"""
|
||||||
return True
|
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)
|
||||||
|
)
|
||||||
|
return track
|
||||||
|
|
||||||
tracks = versions.get("tracks")
|
|
||||||
if not isinstance(tracks, dict):
|
def _legacy_single_track(versions: dict) -> dict:
|
||||||
# 旧式单轨结构 → release 轨
|
"""把最旧的单轨结构读成 {app_ver, build_map}。"""
|
||||||
legacy_app_ver = versions.get("app_ver") or DEFAULT_APP_VER
|
app_ver = versions.get("app_ver") or DEFAULT_APP_VER
|
||||||
legacy_build_map = versions.get("build_map")
|
build_map = versions.get("build_map")
|
||||||
if not isinstance(legacy_build_map, dict):
|
if not isinstance(build_map, dict):
|
||||||
legacy_build_map = {}
|
build_map = {}
|
||||||
# 更旧的单值形式 "2.195.0.3" → {"2.195.0": 3}
|
# 更旧的单值形式 "2.195.0.3" → {"2.195.0": 3}
|
||||||
old = versions.get("build_ver")
|
old = versions.get("build_ver")
|
||||||
if isinstance(old, str):
|
if isinstance(old, str):
|
||||||
parts = old.split(".")
|
parts = old.split(".")
|
||||||
if len(parts) >= 4 and parts[3].isdigit():
|
if len(parts) >= 4 and parts[3].isdigit():
|
||||||
legacy_build_map[".".join(parts[:3])] = int(parts[3])
|
build_map[".".join(parts[:3])] = int(parts[3])
|
||||||
versions.pop("app_ver", None)
|
versions.pop("app_ver", None)
|
||||||
versions.pop("build_map", None)
|
versions.pop("build_map", None)
|
||||||
versions.pop("build_ver", None)
|
versions.pop("build_ver", None)
|
||||||
tracks = {
|
return {"app_ver": app_ver, "build_map": build_map}
|
||||||
TRACK_RELEASE: {"app_ver": legacy_app_ver, "build_map": legacy_build_map},
|
|
||||||
TRACK_FEATURE: _new_track(legacy_app_ver),
|
|
||||||
}
|
def _ensure_versions(config: dict) -> bool:
|
||||||
versions["tracks"] = tracks
|
"""确保 versions 为「每个上架分支一条轨 + 一条 feature 轨」结构,并迁移旧配置。
|
||||||
|
|
||||||
|
旧式单轨 ``{"app_ver": ..., "build_map": ...}`` 和旧的 master/develop 共用
|
||||||
|
``release`` 轨,都会按上架分支拆成各自独立的轨道,并复制已用构建号,
|
||||||
|
使拆分后每个分支都从原来的构建号继续递增。
|
||||||
|
"""
|
||||||
|
versions = config.get("versions")
|
||||||
|
changed = False
|
||||||
|
if not isinstance(versions, dict):
|
||||||
|
versions = {}
|
||||||
|
config["versions"] = versions
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
for name in TRACKS:
|
legacy = None # 需要拆分到各上架分支的旧共用轨
|
||||||
track = tracks.get(name)
|
tracks = versions.get("tracks")
|
||||||
|
if not isinstance(tracks, dict):
|
||||||
|
legacy = _legacy_single_track(versions)
|
||||||
|
tracks = {}
|
||||||
|
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")
|
||||||
|
legacy = {
|
||||||
|
"app_ver": old.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):
|
||||||
|
tracks[TRACK_FEATURE] = _new_track((legacy or {}).get("app_ver") or DEFAULT_APP_VER, KIND_FEATURE)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
for name, track in tracks.items():
|
||||||
if not isinstance(track, dict):
|
if not isinstance(track, dict):
|
||||||
tracks[name] = _new_track()
|
track = tracks[name] = _new_track()
|
||||||
|
changed = True
|
||||||
|
kind = KIND_FEATURE if name == TRACK_FEATURE else KIND_RELEASE
|
||||||
|
if track.get("kind") != kind:
|
||||||
|
track["kind"] = kind
|
||||||
changed = True
|
changed = True
|
||||||
continue
|
|
||||||
if not track.get("app_ver"):
|
if not track.get("app_ver"):
|
||||||
track["app_ver"] = DEFAULT_APP_VER
|
track["app_ver"] = DEFAULT_APP_VER
|
||||||
changed = True
|
changed = True
|
||||||
@@ -168,33 +222,101 @@ def _ensure_versions(config: dict) -> bool:
|
|||||||
track["build_map"] = {}
|
track["build_map"] = {}
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
# 分支归属:只存显式配置,缺省按名字规则推断
|
# 分支归属:值为轨道名,缺省按分支名规则推断
|
||||||
branch_track = config.get("branch_track")
|
branch_track = config.get("branch_track")
|
||||||
if not isinstance(branch_track, dict):
|
if not isinstance(branch_track, dict):
|
||||||
branch_track = {}
|
branch_track = {}
|
||||||
config["branch_track"] = branch_track
|
config["branch_track"] = branch_track
|
||||||
changed = True
|
changed = True
|
||||||
for branch in config.get("branches", []) or []:
|
for branch in config.get("branches", []) or []:
|
||||||
if branch_track.get(branch) not in TRACKS:
|
name = branch_track.get(branch)
|
||||||
branch_track[branch] = _default_track_for_branch(branch)
|
if name == LEGACY_RELEASE_TRACK and LEGACY_RELEASE_TRACK not in tracks:
|
||||||
|
# 旧的共用上架轨 → 该分支自己的轨
|
||||||
|
name = _release_track_name(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)
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def _primary_track_name(config: dict) -> str:
|
||||||
|
"""主上架轨:第一个上架分支所属的轨道;没有上架分支时退回 feature 轨。
|
||||||
|
|
||||||
|
仅用于顶层 app_ver/build_ver 的兼容字段和未指定轨道时的缺省值。
|
||||||
|
"""
|
||||||
|
for branch in config.get("branches", []) or []:
|
||||||
|
name = get_track_name(config, branch)
|
||||||
|
if name != TRACK_FEATURE:
|
||||||
|
return name
|
||||||
|
return TRACK_FEATURE
|
||||||
|
|
||||||
|
|
||||||
def get_track_name(config: dict, branch: str) -> str:
|
def get_track_name(config: dict, branch: str) -> str:
|
||||||
"""取分支所属的版本轨道名。"""
|
"""取分支所属的版本轨道名。"""
|
||||||
track = (config.get("branch_track") or {}).get(branch)
|
name = (config.get("branch_track") or {}).get(branch)
|
||||||
return track if track in TRACKS else _default_track_for_branch(branch)
|
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)
|
||||||
|
|
||||||
|
|
||||||
def get_track(config: dict, track_name: str) -> dict:
|
def get_track(config: dict, track_name: str) -> dict:
|
||||||
"""取某条轨道的版本记录(不存在时返回默认值,不写回配置)。"""
|
"""取某条轨道的版本记录副本(不存在时返回默认值;只读,不写回配置)。
|
||||||
|
|
||||||
|
未迁移的旧记录没有 kind,按轨道名补上:只有固定的 feature 轨是自测轨。
|
||||||
|
"""
|
||||||
tracks = (config.get("versions") or {}).get("tracks") or {}
|
tracks = (config.get("versions") or {}).get("tracks") or {}
|
||||||
|
kind = KIND_FEATURE if track_name == TRACK_FEATURE else KIND_RELEASE
|
||||||
track = tracks.get(track_name)
|
track = tracks.get(track_name)
|
||||||
if not isinstance(track, dict):
|
if not isinstance(track, dict):
|
||||||
return _new_track()
|
return _new_track(kind=kind)
|
||||||
return track
|
return {**track, "kind": track.get("kind") or kind}
|
||||||
|
|
||||||
|
|
||||||
|
def is_release_track(track: dict) -> bool:
|
||||||
|
"""该轨道是否为上架轨(App_Store 打包时构建号自增)。"""
|
||||||
|
return track.get("kind", KIND_RELEASE) == KIND_RELEASE
|
||||||
|
|
||||||
|
|
||||||
|
def next_build_no(tracks: dict, app_ver: str) -> int:
|
||||||
|
"""该 App_Ver 下一个可用的构建号。
|
||||||
|
|
||||||
|
构建号唯一性由 App Store 按 MARKETING_VERSION 全局判定,与分支无关,
|
||||||
|
因此取所有上架轨在这个版本号上已用到的最大值再 +1,避免两个上架分支
|
||||||
|
临时使用同一 App_Ver 时产生重复构建号被拒。
|
||||||
|
"""
|
||||||
|
used = [
|
||||||
|
int((track.get("build_map") or {}).get(app_ver, 0))
|
||||||
|
for track in tracks.values()
|
||||||
|
if isinstance(track, dict) and is_release_track(track)
|
||||||
|
]
|
||||||
|
return max(used, default=0) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_track(config: dict, branch: str, track: str) -> str:
|
||||||
|
"""把用户传入的轨道值解析为轨道名。
|
||||||
|
|
||||||
|
兼容按类型指定:``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)
|
||||||
|
if value in ((config.get("versions") or {}).get("tracks") or {}):
|
||||||
|
return value
|
||||||
|
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track}")
|
||||||
|
|
||||||
|
|
||||||
def track_build_ver(track: dict) -> str:
|
def track_build_ver(track: dict) -> str:
|
||||||
@@ -203,6 +325,7 @@ def track_build_ver(track: dict) -> str:
|
|||||||
build_map = track.get("build_map") or {}
|
build_map = track.get("build_map") or {}
|
||||||
return f"{app_ver}.{int(build_map.get(app_ver, 0))}"
|
return f"{app_ver}.{int(build_map.get(app_ver, 0))}"
|
||||||
|
|
||||||
|
|
||||||
# 为已有环境迁移的固定前缀;后续环境从配置中的 next_app_id_prefix 自动分配。
|
# 为已有环境迁移的固定前缀;后续环境从配置中的 next_app_id_prefix 自动分配。
|
||||||
LEGACY_SERVER_PREFIXES = {
|
LEGACY_SERVER_PREFIXES = {
|
||||||
"测试环境": 1,
|
"测试环境": 1,
|
||||||
@@ -652,43 +775,47 @@ async def get_branches():
|
|||||||
|
|
||||||
@router.post("/branches")
|
@router.post("/branches")
|
||||||
async def add_branch(data: dict):
|
async def add_branch(data: dict):
|
||||||
"""新增分支,可指定所属版本轨道(缺省按分支名推断)"""
|
"""新增分支,可指定所属版本轨道(缺省按分支名推断)。
|
||||||
|
|
||||||
|
track 传 release 表示这个分支用自己独立的上架版本号,feature 表示共用自测版本号;
|
||||||
|
也可直接传已有轨道名,让该分支与另一个分支共用同一条版本线。
|
||||||
|
"""
|
||||||
name = data.get("name", "").strip()
|
name = data.get("name", "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
raise HTTPException(status_code=400, detail="分支名称不能为空")
|
raise HTTPException(status_code=400, detail="分支名称不能为空")
|
||||||
|
|
||||||
track = (data.get("track") or "").strip() or _default_track_for_branch(name)
|
|
||||||
if track not in TRACKS:
|
|
||||||
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track}")
|
|
||||||
|
|
||||||
async with _config_lock:
|
async with _config_lock:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
branches = config.get("branches", ["main"])
|
branches = config.get("branches", ["main"])
|
||||||
if name in branches:
|
if name in branches:
|
||||||
raise HTTPException(status_code=400, detail="分支已存在")
|
raise HTTPException(status_code=400, detail="分支已存在")
|
||||||
|
track = resolve_track(config, name, data.get("track"))
|
||||||
branches.append(name)
|
branches.append(name)
|
||||||
config["branches"] = branches
|
config["branches"] = branches
|
||||||
config.setdefault("branch_track", {})[name] = track
|
config.setdefault("branch_track", {})[name] = track
|
||||||
|
_ensure_track(config, track)
|
||||||
save_config(config)
|
save_config(config)
|
||||||
return {"message": "分支已添加", "track": track}
|
return {"message": "分支已添加", "track": track}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/branches/track")
|
@router.put("/branches/track")
|
||||||
async def update_branch_track(data: dict):
|
async def update_branch_track(data: dict):
|
||||||
"""修改分支所属的版本轨道(release=上架,feature=自测)。
|
"""修改分支所属的版本轨道(release=该分支独立的上架版本号,feature=共用自测版本号)。
|
||||||
|
|
||||||
分支名可能含 /(如 feature/login),因此走请求体而非路径参数。
|
改成 release 时会为这个分支建一条以分支名命名的轨道(初值沿用已有上架轨的 App_Ver),
|
||||||
|
此后它的版本号与其它分支互不影响。分支名可能含 /(如 feature/login),因此走请求体而非路径参数。
|
||||||
"""
|
"""
|
||||||
branch = (data.get("branch") or "").strip()
|
branch = (data.get("branch") or "").strip()
|
||||||
track = (data.get("track") or "").strip()
|
if not (data.get("track") or "").strip():
|
||||||
if track not in TRACKS:
|
raise HTTPException(status_code=400, detail="未指定版本轨道")
|
||||||
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track}")
|
|
||||||
|
|
||||||
async with _config_lock:
|
async with _config_lock:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
if branch not in config.get("branches", []):
|
if branch not in config.get("branches", []):
|
||||||
raise HTTPException(status_code=404, detail="分支不存在")
|
raise HTTPException(status_code=404, detail="分支不存在")
|
||||||
|
track = resolve_track(config, branch, data.get("track"))
|
||||||
config.setdefault("branch_track", {})[branch] = track
|
config.setdefault("branch_track", {})[branch] = track
|
||||||
|
_ensure_track(config, track)
|
||||||
save_config(config)
|
save_config(config)
|
||||||
return {"message": "分支版本轨道已更新", "branch": branch, "track": track}
|
return {"message": "分支版本轨道已更新", "branch": branch, "track": track}
|
||||||
|
|
||||||
@@ -755,24 +882,29 @@ async def update_build_settings(settings: BuildConfigUpdate, db: Session = Depen
|
|||||||
# 版本号配置 API
|
# 版本号配置 API
|
||||||
@router.get("/versions")
|
@router.get("/versions")
|
||||||
async def get_versions():
|
async def get_versions():
|
||||||
"""获取两条版本轨道各自的 App_Ver 及其当前构建号。
|
"""获取各条版本轨道的 App_Ver 及其当前构建号。
|
||||||
|
|
||||||
|
每个上架分支(master / develop 等)一条独立轨道,轨道名即分支名;自测分支共用 feature 轨。
|
||||||
build_ver 为该轨道当前 App_Ver 已用到的最大构建号(形如 2.195.0.5,尚未打包则 .0),
|
build_ver 为该轨道当前 App_Ver 已用到的最大构建号(形如 2.195.0.5,尚未打包则 .0),
|
||||||
仅供展示;实际递增只发生在 release 轨的 App_Store 打包时,feature 轨永不自增。
|
仅供展示;实际递增只发生在上架轨的 App_Store 打包时,feature 轨永不自增。
|
||||||
顶层 app_ver / build_ver 为 release 轨的镜像,保留给旧客户端。
|
顶层 app_ver / build_ver 为主上架轨的镜像,保留给旧客户端。
|
||||||
"""
|
"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
tracks = {}
|
tracks = {
|
||||||
for name in TRACKS:
|
name: {
|
||||||
track = get_track(config, name)
|
"kind": KIND_FEATURE if name == TRACK_FEATURE else KIND_RELEASE,
|
||||||
tracks[name] = {
|
|
||||||
"app_ver": track.get("app_ver") or DEFAULT_APP_VER,
|
"app_ver": track.get("app_ver") or DEFAULT_APP_VER,
|
||||||
"build_ver": track_build_ver(track),
|
"build_ver": track_build_ver(track),
|
||||||
"build_map": track.get("build_map") or {},
|
"build_map": track.get("build_map") or {},
|
||||||
}
|
}
|
||||||
|
for name, track in ((config.get("versions") or {}).get("tracks") or {}).items()
|
||||||
|
if isinstance(track, dict)
|
||||||
|
}
|
||||||
|
primary = _primary_track_name(config)
|
||||||
return {
|
return {
|
||||||
**tracks[TRACK_RELEASE],
|
**{k: v for k, v in tracks.get(primary, {}).items() if k != "kind"},
|
||||||
"tracks": tracks,
|
"tracks": tracks,
|
||||||
|
"primary_track": primary,
|
||||||
"branch_track": {
|
"branch_track": {
|
||||||
branch: get_track_name(config, branch)
|
branch: get_track_name(config, branch)
|
||||||
for branch in config.get("branches", []) or []
|
for branch in config.get("branches", []) or []
|
||||||
@@ -787,14 +919,12 @@ async def update_versions(data: dict):
|
|||||||
每个 App_Ver 的构建号在所属轨道内独立保存于 build_map 各自递增,因此来回切换
|
每个 App_Ver 的构建号在所属轨道内独立保存于 build_map 各自递增,因此来回切换
|
||||||
版本号(包括回退到打过包的旧版本号)都不会重复,不会被 App Store 拒绝。
|
版本号(包括回退到打过包的旧版本号)都不会重复,不会被 App Store 拒绝。
|
||||||
|
|
||||||
- track:release(master/develop,上架用)或 feature(自测用),缺省 release。
|
- track:轨道名,上架分支为分支名(如 master、develop),自测为 feature;缺省为主上架轨。
|
||||||
- 不传 build_ver:保留该版本号已记录的构建号,仅更新该轨道的当前 App_Ver。
|
- 不传 build_ver:保留该版本号已记录的构建号,仅更新该轨道的当前 App_Ver。
|
||||||
- 传 build_ver:形如 2.196.0.3,前三位须与 app_ver 一致,第四位为构建号;
|
- 传 build_ver:形如 2.196.0.3,前三位须与 app_ver 一致,第四位为构建号;
|
||||||
直接写入该版本号的记录(可用于手动对齐 App Store Connect 上的已传构建号)。
|
直接写入该版本号的记录(可用于手动对齐 App Store Connect 上的已传构建号)。
|
||||||
"""
|
"""
|
||||||
track_name = (data.get("track") or TRACK_RELEASE).strip()
|
track_name = (data.get("track") or "").strip()
|
||||||
if track_name not in TRACKS:
|
|
||||||
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track_name}")
|
|
||||||
|
|
||||||
app_ver = data.get("app_ver", "").strip()
|
app_ver = data.get("app_ver", "").strip()
|
||||||
if not re.fullmatch(r"\d+\.\d+\.\d+", app_ver):
|
if not re.fullmatch(r"\d+\.\d+\.\d+", app_ver):
|
||||||
@@ -814,7 +944,10 @@ async def update_versions(data: dict):
|
|||||||
async with _config_lock:
|
async with _config_lock:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
tracks = config.setdefault("versions", {}).setdefault("tracks", {})
|
tracks = config.setdefault("versions", {}).setdefault("tracks", {})
|
||||||
track = tracks.setdefault(track_name, _new_track())
|
track_name = track_name or _primary_track_name(config)
|
||||||
|
if track_name not in tracks:
|
||||||
|
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track_name}")
|
||||||
|
track = tracks[track_name]
|
||||||
track["app_ver"] = app_ver
|
track["app_ver"] = app_ver
|
||||||
build_map = track.setdefault("build_map", {})
|
build_map = track.setdefault("build_map", {})
|
||||||
if build_no is not None:
|
if build_no is not None:
|
||||||
|
|||||||
@@ -610,7 +610,7 @@ async def generate_config(task_id: str, task, build_dir: Path) -> dict:
|
|||||||
|
|
||||||
from ..routers.config import (
|
from ..routers.config import (
|
||||||
load_config, _config_lock, get_track, get_track_name, track_build_ver,
|
load_config, _config_lock, get_track, get_track_name, track_build_ver,
|
||||||
TRACK_RELEASE, _new_track,
|
is_release_track, next_build_no, _ensure_track,
|
||||||
)
|
)
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
@@ -621,29 +621,29 @@ async def generate_config(task_id: str, task, build_dir: Path) -> dict:
|
|||||||
scheme = schemes.get(task.scheme_id, {})
|
scheme = schemes.get(task.scheme_id, {})
|
||||||
|
|
||||||
# 版本号由当前服务配置统一管理,不读取分支源码中的手动打包脚本。
|
# 版本号由当前服务配置统一管理,不读取分支源码中的手动打包脚本。
|
||||||
# 按分支所属轨道取版本号:master/develop 走 release 轨(上架用),feature 走 feature 轨(自测用),
|
# 按分支所属轨道取版本号:每个上架分支(master、develop 等)一条独立轨道,
|
||||||
# 两条轨道的 App_Ver 各自独立。
|
# 自测分支共用 feature 轨,各轨道的 App_Ver 互不影响。
|
||||||
track_name = get_track_name(config, task.branch)
|
track_name = get_track_name(config, task.branch)
|
||||||
track = get_track(config, track_name)
|
track = get_track(config, track_name)
|
||||||
app_ver = track.get("app_ver") or "2.180.0"
|
app_ver = track.get("app_ver") or "2.180.0"
|
||||||
|
|
||||||
# 构建号按版本号独立记录:build_map[app_ver] 为该版本号已用到的最大第四位。
|
# 构建号按版本号独立记录:build_map[app_ver] 为该版本号已用到的最大第四位。
|
||||||
build_ver = track_build_ver(track)
|
build_ver = track_build_ver(track)
|
||||||
|
is_release = is_release_track(track)
|
||||||
await log_streamer.emit(
|
await log_streamer.emit(
|
||||||
task_id,
|
task_id,
|
||||||
f"版本轨道: {track_name}({'上架,App_Store 打包自增' if track_name == TRACK_RELEASE else '自测,构建号不自增'})",
|
f"版本轨道: {track_name}({'上架,App_Store 打包自增' if is_release else '自测,构建号不自增'})",
|
||||||
)
|
)
|
||||||
|
|
||||||
# release 轨的 App_Store 打包,在该版本号自己的构建号上 +1
|
# 上架轨的 App_Store 打包,在该版本号的构建号上 +1(回退到旧版本号也从它自己的最大值继续);
|
||||||
# (回退到旧版本号也从它自己的最大值继续);feature 轨始终使用手动设定的构建号。
|
# feature 轨始终使用手动设定的构建号。
|
||||||
if track_name == TRACK_RELEASE and task.build_type == "App_Store":
|
if is_release and task.build_type == "App_Store":
|
||||||
async with _config_lock:
|
async with _config_lock:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
tracks = config.setdefault("versions", {}).setdefault("tracks", {})
|
track = _ensure_track(config, track_name)
|
||||||
track = tracks.setdefault(track_name, _new_track(app_ver))
|
track.setdefault("app_ver", app_ver)
|
||||||
build_map = track.setdefault("build_map", {})
|
n = next_build_no(config["versions"]["tracks"], app_ver)
|
||||||
n = int(build_map.get(app_ver, 0)) + 1
|
track.setdefault("build_map", {})[app_ver] = n
|
||||||
build_map[app_ver] = n
|
|
||||||
build_ver = f"{app_ver}.{n}"
|
build_ver = f"{app_ver}.{n}"
|
||||||
|
|
||||||
from ..routers.config import save_config
|
from ..routers.config import save_config
|
||||||
|
|||||||
+4
-3
@@ -74,9 +74,10 @@
|
|||||||
},
|
},
|
||||||
"versions": {
|
"versions": {
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"release": { "app_ver": "2.180.0", "build_map": {} },
|
"master": { "kind": "release", "app_ver": "2.180.0", "build_map": {} },
|
||||||
"feature": { "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": "release", "develop": "release" }
|
"branch_track": { "master": "master", "develop": "develop" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ function mockFetch(url) {
|
|||||||
'/api/config/versions': {
|
'/api/config/versions': {
|
||||||
app_ver: '2.195.0',
|
app_ver: '2.195.0',
|
||||||
build_ver: '2.195.0.0',
|
build_ver: '2.195.0.0',
|
||||||
|
primary_track: 'main',
|
||||||
tracks: {
|
tracks: {
|
||||||
release: { app_ver: '2.195.0', build_ver: '2.195.0.0', build_map: {} },
|
main: { kind: 'release', app_ver: '2.195.0', build_ver: '2.195.0.0', build_map: {} },
|
||||||
feature: { app_ver: '2.100.0', build_ver: '2.100.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: 'release', dev: 'release' },
|
branch_track: { main: 'main', dev: 'dev' },
|
||||||
},
|
},
|
||||||
'/api/config': { apps: {}, schemes: {}, branches: ['main'] },
|
'/api/config': { apps: {}, schemes: {}, branches: ['main'] },
|
||||||
}
|
}
|
||||||
@@ -191,7 +193,7 @@ describe('ConfigView.vue', () => {
|
|||||||
expect(fetch.mock.calls.map(c => c[0])).toEqual(['/api/config/apps'])
|
expect(fetch.mock.calls.map(c => c[0])).toEqual(['/api/config/apps'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('两条版本轨道各自独立展示与保存', async () => {
|
it('每个上架分支与自测轨各自独立展示与保存', async () => {
|
||||||
const calls = []
|
const calls = []
|
||||||
fetch.mockImplementation((url, opts) => {
|
fetch.mockImplementation((url, opts) => {
|
||||||
if (url === '/api/config/versions' && opts?.method === 'PUT') {
|
if (url === '/api/config/versions' && opts?.method === 'PUT') {
|
||||||
@@ -213,10 +215,11 @@ describe('ConfigView.vue', () => {
|
|||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
const inputs = wrapper.findAll('input[type="text"]')
|
const inputs = wrapper.findAll('input[type="text"]')
|
||||||
expect(inputs[0].element.value).toBe('2.195.0') // release App_Ver
|
expect(inputs[0].element.value).toBe('2.195.0') // main(上架)App_Ver
|
||||||
expect(inputs[2].element.value).toBe('2.100.0') // feature App_Ver
|
expect(inputs[2].element.value).toBe('2.196.0') // dev(上架)App_Ver
|
||||||
|
expect(inputs[4].element.value).toBe('2.100.0') // feature App_Ver
|
||||||
|
|
||||||
await inputs[2].setValue('2.101.0')
|
await inputs[4].setValue('2.101.0')
|
||||||
const saveBtn = wrapper.findAll('.btn-primary').find(b => b.text() === '保存自测版本号')
|
const saveBtn = wrapper.findAll('.btn-primary').find(b => b.text() === '保存自测版本号')
|
||||||
await saveBtn.trigger('click')
|
await saveBtn.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
@@ -171,9 +171,9 @@
|
|||||||
<tr v-for="b in branches" :key="b">
|
<tr v-for="b in branches" :key="b">
|
||||||
<td><strong>{{ b }}</strong></td>
|
<td><strong>{{ b }}</strong></td>
|
||||||
<td>
|
<td>
|
||||||
<select :value="branchTrack[b] || 'feature'" style="width: auto; padding: 4px 8px;" @change="setBranchTrack(b, $event.target.value)">
|
<select :value="branchKind(b)" style="width: auto; padding: 4px 8px;" @change="setBranchTrack(b, $event.target.value)">
|
||||||
<option value="release">上架(master/develop)</option>
|
<option value="release">上架(本分支独立版本号)</option>
|
||||||
<option value="feature">自测(feature)</option>
|
<option value="feature">自测(共用 feature 版本号)</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td class="action-btns">
|
<td class="action-btns">
|
||||||
@@ -289,27 +289,30 @@
|
|||||||
<div v-if="tab === 'build'">
|
<div v-if="tab === 'build'">
|
||||||
<h2>打包设置</h2>
|
<h2>打包设置</h2>
|
||||||
<div style="max-width: 500px;">
|
<div style="max-width: 500px;">
|
||||||
<h4 class="section-title">上架版本号(master / develop)</h4>
|
<template v-for="t in releaseTracks" :key="t.name">
|
||||||
|
<h4 class="section-title">上架版本号({{ t.branches.join(' / ') }})</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>App_Ver(应用版本)</label>
|
<label>App_Ver(应用版本)</label>
|
||||||
<input type="text" v-model="versions.release.app_ver" placeholder="2.180.0" @input="onAppVerInput('release')">
|
<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;">格式:主版本.次版本.修订号。master 与 develop 共用这一套版本号。</div>
|
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号。该版本号只用于 {{ t.branches.join(' / ') }} 分支,与其它分支互不影响。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Build_Ver(构建号)</label>
|
<label>Build_Ver(构建号)</label>
|
||||||
<input type="text" v-model="versions.release.build_ver" placeholder="2.180.0.0">
|
<input type="text" v-model="versions[t.name].build_ver" placeholder="2.180.0.0">
|
||||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">
|
<div style="font-size: 12px; color: #999; margin-top: 4px;">
|
||||||
四段数字,前三位须与 App_Ver 一致。修改 App_Ver 会自动切换为该版本号已记录的构建号;
|
四段数字,前三位须与 App_Ver 一致。修改 App_Ver 会自动切换为该版本号已记录的构建号;
|
||||||
每个版本号的构建号独立递增,App_Store 打包时自动 +1。一般无需手动修改。
|
每个版本号的构建号独立递增,App_Store 打包时自动 +1。一般无需手动修改。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions('release')">保存上架版本号</button>
|
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions(t.name)">保存 {{ t.name }} 版本号</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="versions.feature">
|
||||||
<h4 class="section-title">自测版本号(feature 分支)</h4>
|
<h4 class="section-title">自测版本号(feature 分支)</h4>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>App_Ver(应用版本)</label>
|
<label>App_Ver(应用版本)</label>
|
||||||
<input type="text" v-model="versions.feature.app_ver" placeholder="2.180.0" @input="onAppVerInput('feature')">
|
<input type="text" v-model="versions.feature.app_ver" placeholder="2.180.0" @input="onAppVerInput('feature')">
|
||||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">与上架版本号各自独立,互不影响。</div>
|
<div style="font-size: 12px; color: #999; margin-top: 4px;">所有自测分支共用,与上架版本号各自独立,互不影响。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Build_Ver(构建号)</label>
|
<label>Build_Ver(构建号)</label>
|
||||||
@@ -319,6 +322,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions('feature')">保存自测版本号</button>
|
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions('feature')">保存自测版本号</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-if="isAdmin">
|
<template v-if="isAdmin">
|
||||||
<h4 class="section-title">打包参数</h4>
|
<h4 class="section-title">打包参数</h4>
|
||||||
@@ -654,27 +658,47 @@ const servers = ref({})
|
|||||||
const branches = ref([])
|
const branches = ref([])
|
||||||
const newBranch = ref('')
|
const newBranch = ref('')
|
||||||
const buildSettings = ref({})
|
const buildSettings = ref({})
|
||||||
// 两条版本轨道:release(master/develop,上架用)与 feature(自测用),各自独立
|
// 版本轨道:每个上架分支(master、develop 等)一条独立轨道,轨道名即分支名;
|
||||||
const versions = ref({
|
// 自测分支共用固定的 feature 轨。各轨道的版本号互不影响。
|
||||||
release: { app_ver: '', build_ver: '' },
|
const versions = ref({}) // 轨道名 -> { app_ver, build_ver }
|
||||||
feature: { app_ver: '', build_ver: '' },
|
const buildMap = ref({}) // 轨道名 -> { App_Ver: 已用构建号 }
|
||||||
})
|
const trackKind = ref({}) // 轨道名 -> release | feature
|
||||||
const buildMap = ref({ release: {}, feature: {} })
|
const branchTrack = ref({}) // 分支名 -> 轨道名
|
||||||
const branchTrack = ref({})
|
|
||||||
|
|
||||||
const applyVersions = (data) => {
|
const applyVersions = (data) => {
|
||||||
const tracks = data.tracks || {}
|
const tracks = data.tracks || {}
|
||||||
for (const track of ['release', 'feature']) {
|
const nextVersions = {}
|
||||||
const item = tracks[track] || {}
|
const nextMap = {}
|
||||||
buildMap.value[track] = item.build_map || {}
|
const nextKind = {}
|
||||||
versions.value[track] = { app_ver: item.app_ver || '', build_ver: item.build_ver || '' }
|
for (const [name, item] of Object.entries(tracks)) {
|
||||||
|
nextVersions[name] = { app_ver: item.app_ver || '', build_ver: item.build_ver || '' }
|
||||||
|
nextMap[name] = item.build_map || {}
|
||||||
|
nextKind[name] = item.kind || 'release'
|
||||||
}
|
}
|
||||||
|
versions.value = nextVersions
|
||||||
|
buildMap.value = nextMap
|
||||||
|
trackKind.value = nextKind
|
||||||
branchTrack.value = data.branch_track || {}
|
branchTrack.value = data.branch_track || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分支所属轨道的类型(供分支管理里的下拉框显示)
|
||||||
|
const branchKind = (branch) => trackKind.value[branchTrack.value[branch]] || 'feature'
|
||||||
|
|
||||||
|
// 打包设置里按上架轨道分块展示;多个分支共用一条轨道时只显示一块
|
||||||
|
const releaseTracks = computed(() => {
|
||||||
|
const seen = new Set()
|
||||||
|
return branches.value
|
||||||
|
.map(b => branchTrack.value[b])
|
||||||
|
.filter(name => name && trackKind.value[name] === 'release' && !seen.has(name) && seen.add(name))
|
||||||
|
.map(name => ({
|
||||||
|
name,
|
||||||
|
branches: branches.value.filter(b => branchTrack.value[b] === name),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
// 修改 App_Ver 时,自动把 Build_Ver 切换为该版本号已记录的构建号(新版本号则为 .0)
|
// 修改 App_Ver 时,自动把 Build_Ver 切换为该版本号已记录的构建号(新版本号则为 .0)
|
||||||
const onAppVerInput = (track) => {
|
const onAppVerInput = (track) => {
|
||||||
const appVer = (versions.value[track].app_ver || '').trim()
|
const appVer = (versions.value[track]?.app_ver || '').trim()
|
||||||
if (/^\d+\.\d+\.\d+$/.test(appVer)) {
|
if (/^\d+\.\d+\.\d+$/.test(appVer)) {
|
||||||
const n = Number((buildMap.value[track] || {})[appVer] || 0)
|
const n = Number((buildMap.value[track] || {})[appVer] || 0)
|
||||||
versions.value[track].build_ver = `${appVer}.${n}`
|
versions.value[track].build_ver = `${appVer}.${n}`
|
||||||
@@ -1194,7 +1218,8 @@ const setBranchTrack = async (name, track) => {
|
|||||||
const err = await res.json()
|
const err = await res.json()
|
||||||
throw new Error(err.detail || '修改失败')
|
throw new Error(err.detail || '修改失败')
|
||||||
}
|
}
|
||||||
branchTrack.value = { ...branchTrack.value, [name]: track }
|
// 切到上架轨可能新建了轨道,重新拉取版本号
|
||||||
|
applyVersions(await (await authFetch('/api/config/versions')).json())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('修改失败: ' + e.message)
|
alert('修改失败: ' + e.message)
|
||||||
await loadData()
|
await loadData()
|
||||||
@@ -1222,7 +1247,7 @@ const saveBuildSettings = async () => {
|
|||||||
alert('设置已保存')
|
alert('设置已保存')
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveVersions = async (track = 'release') => {
|
const saveVersions = async (track) => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/config/versions', {
|
const res = await authFetch('/api/config/versions', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
+56
-11
@@ -229,15 +229,20 @@ def test_update_versions_rejects_invalid_app_version(client, tmp_config):
|
|||||||
|
|
||||||
|
|
||||||
def test_versions_tracks_are_independent(client, tmp_config):
|
def test_versions_tracks_are_independent(client, tmp_config):
|
||||||
# 两条轨道的 App_Ver 各自独立,互不影响
|
# 每个上架分支一条轨道(轨道名即分支名),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": "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
|
assert client.put("/api/config/versions", json={"track": "feature", "app_ver": "2.100.0"}).status_code == 200
|
||||||
|
|
||||||
data = client.get("/api/config/versions").json()
|
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"]["app_ver"] == "2.100.0"
|
||||||
assert data["tracks"]["feature"]["build_ver"] == "2.100.0.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"
|
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
|
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
|
import json
|
||||||
with open(tmp_config, "r", encoding="utf-8") as f:
|
with open(tmp_config, "r", encoding="utf-8") as f:
|
||||||
cfg = json.load(f)
|
cfg = json.load(f)
|
||||||
@@ -255,7 +261,10 @@ def test_legacy_versions_migrate_to_release_track(client, tmp_config):
|
|||||||
json.dump(cfg, f)
|
json.dump(cfg, f)
|
||||||
|
|
||||||
data = client.get("/api/config/versions").json()
|
data = client.get("/api/config/versions").json()
|
||||||
assert data["tracks"]["release"] == {
|
for branch in ("main", "dev"):
|
||||||
|
assert data["branch_track"][branch] == branch
|
||||||
|
assert data["tracks"][branch] == {
|
||||||
|
"kind": "release",
|
||||||
"app_ver": "2.195.0",
|
"app_ver": "2.195.0",
|
||||||
"build_ver": "2.195.0.7",
|
"build_ver": "2.195.0.7",
|
||||||
"build_map": {"2.195.0": 7},
|
"build_map": {"2.195.0": 7},
|
||||||
@@ -263,20 +272,56 @@ def test_legacy_versions_migrate_to_release_track(client, tmp_config):
|
|||||||
# feature 轨以同一 App_Ver 初始化,构建号从 .0 起
|
# feature 轨以同一 App_Ver 初始化,构建号从 .0 起
|
||||||
assert data["tracks"]["feature"]["app_ver"] == "2.195.0"
|
assert data["tracks"]["feature"]["app_ver"] == "2.195.0"
|
||||||
assert data["tracks"]["feature"]["build_ver"] == "2.195.0.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):
|
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": "develop"}).status_code == 200
|
||||||
assert client.post("/api/config/branches", json={"name": "feature/login"}).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"]
|
data = client.get("/api/config/versions").json()
|
||||||
assert tracks["develop"] == "release"
|
# 上架分支各有一条以分支名命名的轨道,自测分支共用 feature 轨
|
||||||
assert tracks["feature/login"] == "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"})
|
resp = client.put("/api/config/branches/track", json={"branch": "feature/login", "track": "release"})
|
||||||
assert resp.status_code == 200
|
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": "nope", "track": "release"}).status_code == 404
|
||||||
assert client.put("/api/config/branches/track", json={"branch": "develop", "track": "beta"}).status_code == 400
|
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")
|
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 {
|
return {
|
||||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
"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):
|
async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||||
"""从服务配置读取版本号,不依赖分支源码脚本"""
|
"""从服务配置读取版本号,不依赖分支源码脚本"""
|
||||||
build_dir = tmp_path / "build"
|
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.obfuscation = False
|
||||||
task.branch = "master"
|
task.branch = "master"
|
||||||
|
|
||||||
mock_config = _versions_config(
|
mock_config = _versions_config({"master": _release("3.0.1"), "feature": _feature("9.9.9")})
|
||||||
{"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):
|
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||||
result = await generate_config("t1", task, build_dir)
|
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")
|
obfuscation=False, branch="master")
|
||||||
|
|
||||||
# 2.195.0 已打到 .5,切到 2.196.0 后又回退到 2.195.0
|
# 2.195.0 已打到 .5,切到 2.196.0 后又回退到 2.195.0
|
||||||
mock_config = _versions_config(
|
mock_config = _versions_config({
|
||||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5, "2.196.0": 3}},
|
"master": _release("2.195.0", {"2.195.0": 5, "2.196.0": 3}),
|
||||||
{"app_ver": "2.195.0", "build_map": {}},
|
"feature": _feature("2.195.0"),
|
||||||
)
|
})
|
||||||
|
|
||||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||||
patch("backend.routers.config.save_config") as save_mock:
|
patch("backend.routers.config.save_config") as save_mock:
|
||||||
result = await generate_config("t1", task, build_dir)
|
result = await generate_config("t1", task, build_dir)
|
||||||
|
|
||||||
# 从 2.195.0 自身的 5 继续到 6,不受 2.196.0 影响,也不撞已传的 .5
|
# 从 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 result["BUILD_VERSION"] == "2.195.0.6"
|
||||||
assert release_map["2.195.0"] == 6
|
assert release_map["2.195.0"] == 6
|
||||||
assert release_map["2.196.0"] == 3
|
assert release_map["2.196.0"] == 3
|
||||||
save_mock.assert_called()
|
save_mock.assert_called()
|
||||||
|
|
||||||
|
|
||||||
async def test_generate_config_develop_shares_release_track(tmp_path, log_streamer):
|
async def test_generate_config_release_branches_are_independent(tmp_path, log_streamer):
|
||||||
"""develop 与 master 共用 release 轨的版本号与构建号序列"""
|
"""master 与 develop 各用自己的轨道,版本号与构建号互不影响"""
|
||||||
build_dir = tmp_path / "build"
|
build_dir = tmp_path / "build"
|
||||||
build_dir.mkdir()
|
build_dir.mkdir()
|
||||||
|
|
||||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||||
obfuscation=False, branch="develop")
|
obfuscation=False, branch="develop")
|
||||||
|
|
||||||
mock_config = _versions_config(
|
mock_config = _versions_config({
|
||||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5}},
|
"master": _release("2.195.0", {"2.195.0": 5}),
|
||||||
{"app_ver": "2.100.0", "build_map": {}},
|
"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), \
|
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||||
patch("backend.routers.config.save_config"):
|
patch("backend.routers.config.save_config"):
|
||||||
result = await generate_config("t1", task, build_dir)
|
result = await generate_config("t1", task, build_dir)
|
||||||
|
|
||||||
assert result["VERSION"] == "2.195.0"
|
assert result["VERSION"] == "2.196.0"
|
||||||
assert result["BUILD_VERSION"] == "2.195.0.6"
|
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):
|
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",
|
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||||
obfuscation=False, branch="feature/login")
|
obfuscation=False, branch="feature/login")
|
||||||
|
|
||||||
mock_config = _versions_config(
|
mock_config = _versions_config({
|
||||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5}},
|
"master": _release("2.195.0", {"2.195.0": 5}),
|
||||||
{"app_ver": "2.100.0", "build_map": {}},
|
"feature": _feature("2.100.0"),
|
||||||
)
|
})
|
||||||
|
|
||||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||||
patch("backend.routers.config.save_config") as save_mock:
|
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",
|
task = MagicMock(app_id="1", scheme_id="1", build_type="Ad_Hoc",
|
||||||
obfuscation=False, branch="release/2.196")
|
obfuscation=False, branch="release/2.196")
|
||||||
|
|
||||||
mock_config = _versions_config(
|
mock_config = _versions_config({
|
||||||
{"app_ver": "2.196.0", "build_map": {}},
|
"master": _release("2.196.0"),
|
||||||
{"app_ver": "2.100.0", "build_map": {}},
|
"feature": _feature("2.100.0"),
|
||||||
)
|
})
|
||||||
mock_config["branch_track"] = {"release/2.196": "feature"}
|
mock_config["branch_track"] = {"release/2.196": "feature"}
|
||||||
|
|
||||||
with patch("backend.routers.config.load_config", return_value=mock_config):
|
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||||
|
|||||||
Reference in New Issue
Block a user