feat: 版本号按分支轨道分开管理,版本号配置收归管理员
master/develop 共用 release 轨(App_Store 打包构建号自增),feature 分支走
feature 轨(构建号永不自增,始终使用手填值),两条轨道的 App_Ver 各自独立。
- versions 改为 tracks 结构,旧的单轨/单值配置在加载时自动迁移进 release 轨
- 新增 config["branch_track"] 记录分支归属,缺省按分支名推断,可在分支管理页改
- 新增 PUT /api/config/branches/track;分支名含 / 时走请求体而非路径参数
- 修复 DELETE /api/config/branches/{name} 无法删除含 / 的分支(405)
- 版本号接口不再对普通用户开放,打包设置页整页改为管理员可见
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,11 +25,12 @@ FastAPI + Vue 3 iOS app auto-packaging service. Supports Ad_Hoc and App_Store bu
|
||||
- `backend/deps.py` — JWT auth dependency used by all protected routes
|
||||
- `backend/config.py` — loads .env, defines all config constants
|
||||
- `frontend/src/views/` — BuildView, HistoryView, ConfigView
|
||||
- `config.json` — runtime config (apps, schemes, servers, upload, branches) 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"]`
|
||||
|
||||
## Conventions
|
||||
|
||||
- All backend routes require JWT auth except `/api/auth/login` and `/api/health`
|
||||
- Admin-only routes: user management (`/api/users/*`)
|
||||
- Admin-only routes: user management (`/api/users/*`) and all of `/api/config/*` except `/api/config/apps*`
|
||||
- Config stored in `config.json` (apps/schemes/servers), `.env` (infra), SQLite `build_config` table (build settings)
|
||||
- Chinese UI and documentation throughout
|
||||
|
||||
@@ -187,10 +187,22 @@ GIT_PASSWORD=<仅 read_repository 权限的 Personal Access Token>
|
||||
| App 管理 | 应用名称、Scheme、服务器环境、upload_key 等 |
|
||||
| Scheme 管理 | 打包 Scheme 配置 |
|
||||
| 服务器环境 | 测试/正式/自定义环境的 API 地址、Universal Link 等 |
|
||||
| 分支管理 | 可打包的代码分支列表 |
|
||||
| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码 |
|
||||
| 分支管理 | 可打包的代码分支列表,以及每个分支所属的版本轨道 |
|
||||
| 版本号 | 服务端统一管理 App_Ver 和 Build_Ver,不修改分支源码;按版本轨道分开管理(见下) |
|
||||
| 上传配置 | 服务端内置 OSS / WebDAV 分发,生成 IPA、manifest、下载页和二维码 |
|
||||
|
||||
### 版本号与版本轨道
|
||||
|
||||
版本号按 **轨道(track)** 分开管理,两条轨道的 App_Ver 各自独立填写、互不影响:
|
||||
|
||||
| 轨道 | 适用分支 | 构建号行为 |
|
||||
|------|----------|-----------|
|
||||
| `release`(上架) | master、develop(共用同一套版本号与构建号序列) | App_Store 打包时按当前 App_Ver 自动 +1 |
|
||||
| `feature`(自测) | feature/* 等其他分支 | 永不自增,始终使用配置页填写的值(默认 `.0`) |
|
||||
|
||||
- 分支归属默认按名字推断(master/main/develop/dev/release-*/hotfix-* → release,其余 → feature),可在「分支管理」页逐个改。
|
||||
- 每条轨道内的构建号按 App_Ver 独立记录(`build_map`),回退到打过包的旧版本号时会从它自己的最大值继续,不会与 App Store Connect 上已传构建号冲突。
|
||||
|
||||
### 服务端自动化资源
|
||||
|
||||
项目内的 `backend/automation/` 是 Web 打包服务唯一使用的自动化资源:默认皮肤和混淆工具都在此处维护。分支源码中的 `AutoPacking/` 不会被复制、执行或修改,仍可供开发者手动打包使用。
|
||||
@@ -277,7 +289,7 @@ logs/
|
||||
| 角色 | 权限 |
|
||||
|------|------|
|
||||
| 管理员 | 所有功能 + 用户管理(增删改查、角色切换) |
|
||||
| 普通用户 | 打包、查看历史、查看配置(不可修改配置、不可管理用户) |
|
||||
| 普通用户 | 打包、查看历史、管理 Apps 配置(不可修改版本号及其余配置、不可管理用户) |
|
||||
|
||||
### 管理用户
|
||||
|
||||
|
||||
+158
-44
@@ -26,10 +26,9 @@ from ..deps import get_current_user, require_admin
|
||||
|
||||
|
||||
def _require_config_permission(request: Request, user: dict = Depends(get_current_user)) -> dict:
|
||||
"""普通用户可管理 Apps 与版本号;其余配置及凭据只允许管理员访问。"""
|
||||
"""普通用户只可管理 Apps;版本号、其余配置及凭据只允许管理员访问。"""
|
||||
path = request.url.path.rstrip("/")
|
||||
if (path == "/api/config/apps" or path.startswith("/api/config/apps/")
|
||||
or path == "/api/config/versions"):
|
||||
if path == "/api/config/apps" or path.startswith("/api/config/apps/"):
|
||||
return user
|
||||
return require_admin(user)
|
||||
|
||||
@@ -90,45 +89,118 @@ DEFAULT_SERVERS = {
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_VERSIONS = {
|
||||
"app_ver": "2.180.0",
|
||||
DEFAULT_APP_VER = "2.180.0"
|
||||
|
||||
# 版本轨道:上架分支(master/develop)共用 release,自测分支(feature/*)用 feature。
|
||||
# 两条轨道的 App_Ver 各自独立填写,互不影响。
|
||||
TRACK_RELEASE = "release"
|
||||
TRACK_FEATURE = "feature"
|
||||
TRACKS = (TRACK_RELEASE, TRACK_FEATURE)
|
||||
|
||||
# 按名字推断轨道的默认规则;config["branch_track"] 中的显式配置优先。
|
||||
RELEASE_BRANCH_NAMES = {"master", "main", "develop", "dev"}
|
||||
RELEASE_BRANCH_PREFIXES = ("release/", "release-", "hotfix/", "hotfix-")
|
||||
|
||||
|
||||
def _default_track_for_branch(branch: str) -> str:
|
||||
"""按分支名推断所属版本轨道。"""
|
||||
name = (branch or "").strip().lower()
|
||||
if 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:
|
||||
# 每个版本号(App_Ver)独立记录已用到的最大构建号第四位。
|
||||
# App Store 的构建号唯一性按 MARKETING_VERSION(即完整 App_Ver) 分别计算,
|
||||
# 因此必须按版本号分别记录,回退到旧版本号时才能从它自己的最大值继续递增。
|
||||
"build_map": {},
|
||||
}
|
||||
return {"app_ver": app_ver, "build_map": {}}
|
||||
|
||||
|
||||
def _ensure_versions(config: dict) -> bool:
|
||||
"""确保 versions 存在,并把旧式单值 build_ver 迁移为按版本号记录的 build_map。"""
|
||||
"""确保 versions 为双轨结构,并迁移旧式单轨/单值 build_ver 配置。
|
||||
|
||||
旧式 ``{"app_ver": ..., "build_map": ...}`` 整体迁入 release 轨;
|
||||
feature 轨用同一个 App_Ver 初始化,构建号从 .0 起且永不自增。
|
||||
"""
|
||||
versions = config.get("versions")
|
||||
changed = False
|
||||
|
||||
if not isinstance(versions, dict):
|
||||
config["versions"] = DEFAULT_VERSIONS.copy()
|
||||
config["versions"]["build_map"] = {}
|
||||
config["versions"] = {"tracks": {TRACK_RELEASE: _new_track(), TRACK_FEATURE: _new_track()}}
|
||||
return True
|
||||
|
||||
changed = False
|
||||
if not versions.get("app_ver"):
|
||||
versions["app_ver"] = DEFAULT_VERSIONS["app_ver"]
|
||||
changed = True
|
||||
|
||||
if "build_map" not in versions:
|
||||
build_map = {}
|
||||
old = versions.pop("build_ver", None)
|
||||
# 旧式形如 "2.195.0.3" → build_map = {"2.195.0": 3}
|
||||
tracks = versions.get("tracks")
|
||||
if not isinstance(tracks, dict):
|
||||
# 旧式单轨结构 → release 轨
|
||||
legacy_app_ver = versions.get("app_ver") or DEFAULT_APP_VER
|
||||
legacy_build_map = versions.get("build_map")
|
||||
if not isinstance(legacy_build_map, dict):
|
||||
legacy_build_map = {}
|
||||
# 更旧的单值形式 "2.195.0.3" → {"2.195.0": 3}
|
||||
old = versions.get("build_ver")
|
||||
if isinstance(old, str):
|
||||
parts = old.split(".")
|
||||
if len(parts) >= 4 and parts[3].isdigit():
|
||||
build_map[".".join(parts[:3])] = int(parts[3])
|
||||
versions["build_map"] = build_map
|
||||
changed = True
|
||||
elif "build_ver" in versions:
|
||||
# build_map 已存在但残留旧字段,清理掉
|
||||
legacy_build_map[".".join(parts[:3])] = int(parts[3])
|
||||
versions.pop("app_ver", None)
|
||||
versions.pop("build_map", None)
|
||||
versions.pop("build_ver", None)
|
||||
tracks = {
|
||||
TRACK_RELEASE: {"app_ver": legacy_app_ver, "build_map": legacy_build_map},
|
||||
TRACK_FEATURE: _new_track(legacy_app_ver),
|
||||
}
|
||||
versions["tracks"] = tracks
|
||||
changed = True
|
||||
|
||||
for name in TRACKS:
|
||||
track = tracks.get(name)
|
||||
if not isinstance(track, dict):
|
||||
tracks[name] = _new_track()
|
||||
changed = True
|
||||
continue
|
||||
if not track.get("app_ver"):
|
||||
track["app_ver"] = DEFAULT_APP_VER
|
||||
changed = True
|
||||
if not isinstance(track.get("build_map"), dict):
|
||||
track["build_map"] = {}
|
||||
changed = True
|
||||
|
||||
# 分支归属:只存显式配置,缺省按名字规则推断
|
||||
branch_track = config.get("branch_track")
|
||||
if not isinstance(branch_track, dict):
|
||||
branch_track = {}
|
||||
config["branch_track"] = branch_track
|
||||
changed = True
|
||||
for branch in config.get("branches", []) or []:
|
||||
if branch_track.get(branch) not in TRACKS:
|
||||
branch_track[branch] = _default_track_for_branch(branch)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def get_track_name(config: dict, branch: str) -> str:
|
||||
"""取分支所属的版本轨道名。"""
|
||||
track = (config.get("branch_track") or {}).get(branch)
|
||||
return track if track in TRACKS else _default_track_for_branch(branch)
|
||||
|
||||
|
||||
def get_track(config: dict, track_name: str) -> dict:
|
||||
"""取某条轨道的版本记录(不存在时返回默认值,不写回配置)。"""
|
||||
tracks = (config.get("versions") or {}).get("tracks") or {}
|
||||
track = tracks.get(track_name)
|
||||
if not isinstance(track, dict):
|
||||
return _new_track()
|
||||
return track
|
||||
|
||||
|
||||
def track_build_ver(track: dict) -> str:
|
||||
"""该轨道当前 App_Ver 已用到的构建号,形如 2.195.0.5(尚未打包则 .0)。"""
|
||||
app_ver = track.get("app_ver") or DEFAULT_APP_VER
|
||||
build_map = track.get("build_map") or {}
|
||||
return f"{app_ver}.{int(build_map.get(app_ver, 0))}"
|
||||
|
||||
# 为已有环境迁移的固定前缀;后续环境从配置中的 next_app_id_prefix 自动分配。
|
||||
LEGACY_SERVER_PREFIXES = {
|
||||
"测试环境": 1,
|
||||
@@ -562,11 +634,15 @@ async def get_branches():
|
||||
|
||||
@router.post("/branches")
|
||||
async def add_branch(data: dict):
|
||||
"""新增分支"""
|
||||
"""新增分支,可指定所属版本轨道(缺省按分支名推断)"""
|
||||
name = data.get("name", "").strip()
|
||||
if not name:
|
||||
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:
|
||||
config = load_config()
|
||||
branches = config.get("branches", ["main"])
|
||||
@@ -574,11 +650,32 @@ async def add_branch(data: dict):
|
||||
raise HTTPException(status_code=400, detail="分支已存在")
|
||||
branches.append(name)
|
||||
config["branches"] = branches
|
||||
config.setdefault("branch_track", {})[name] = track
|
||||
save_config(config)
|
||||
return {"message": "分支已添加"}
|
||||
return {"message": "分支已添加", "track": track}
|
||||
|
||||
|
||||
@router.delete("/branches/{branch_name}")
|
||||
@router.put("/branches/track")
|
||||
async def update_branch_track(data: dict):
|
||||
"""修改分支所属的版本轨道(release=上架,feature=自测)。
|
||||
|
||||
分支名可能含 /(如 feature/login),因此走请求体而非路径参数。
|
||||
"""
|
||||
branch = (data.get("branch") or "").strip()
|
||||
track = (data.get("track") or "").strip()
|
||||
if track not in TRACKS:
|
||||
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track}")
|
||||
|
||||
async with _config_lock:
|
||||
config = load_config()
|
||||
if branch not in config.get("branches", []):
|
||||
raise HTTPException(status_code=404, detail="分支不存在")
|
||||
config.setdefault("branch_track", {})[branch] = track
|
||||
save_config(config)
|
||||
return {"message": "分支版本轨道已更新", "branch": branch, "track": track}
|
||||
|
||||
|
||||
@router.delete("/branches/{branch_name:path}")
|
||||
async def delete_branch(branch_name: str):
|
||||
"""删除分支"""
|
||||
async with _config_lock:
|
||||
@@ -588,6 +685,7 @@ async def delete_branch(branch_name: str):
|
||||
raise HTTPException(status_code=404, detail="分支不存在")
|
||||
branches.remove(branch_name)
|
||||
config["branches"] = branches
|
||||
config.get("branch_track", {}).pop(branch_name, None)
|
||||
save_config(config)
|
||||
return {"message": "分支已删除"}
|
||||
|
||||
@@ -639,33 +737,47 @@ async def update_build_settings(settings: BuildConfigUpdate, db: Session = Depen
|
||||
# 版本号配置 API
|
||||
@router.get("/versions")
|
||||
async def get_versions():
|
||||
"""获取服务端统一管理的 App_Ver 及其当前构建号。
|
||||
"""获取两条版本轨道各自的 App_Ver 及其当前构建号。
|
||||
|
||||
build_ver 为当前 App_Ver 已用到的最大构建号(形如 2.195.0.5,尚未打包则 .0),
|
||||
仅供展示;实际递增在 App_Store 打包时按版本号独立进行。
|
||||
build_ver 为该轨道当前 App_Ver 已用到的最大构建号(形如 2.195.0.5,尚未打包则 .0),
|
||||
仅供展示;实际递增只发生在 release 轨的 App_Store 打包时,feature 轨永不自增。
|
||||
顶层 app_ver / build_ver 为 release 轨的镜像,保留给旧客户端。
|
||||
"""
|
||||
config = load_config()
|
||||
versions = config.get("versions", DEFAULT_VERSIONS.copy())
|
||||
app_ver = versions.get("app_ver", DEFAULT_VERSIONS["app_ver"])
|
||||
build_map = versions.get("build_map", {}) or {}
|
||||
tracks = {}
|
||||
for name in TRACKS:
|
||||
track = get_track(config, name)
|
||||
tracks[name] = {
|
||||
"app_ver": track.get("app_ver") or DEFAULT_APP_VER,
|
||||
"build_ver": track_build_ver(track),
|
||||
"build_map": track.get("build_map") or {},
|
||||
}
|
||||
return {
|
||||
"app_ver": app_ver,
|
||||
"build_ver": f"{app_ver}.{int(build_map.get(app_ver, 0))}",
|
||||
"build_map": build_map,
|
||||
**tracks[TRACK_RELEASE],
|
||||
"tracks": tracks,
|
||||
"branch_track": {
|
||||
branch: get_track_name(config, branch)
|
||||
for branch in config.get("branches", []) or []
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.put("/versions")
|
||||
async def update_versions(data: dict):
|
||||
"""更新 App_Ver,并可选地手动设置该版本号的构建号。
|
||||
"""更新某条轨道的 App_Ver,并可选地手动设置该版本号的构建号。
|
||||
|
||||
每个 App_Ver 的构建号独立保存在 build_map 中各自递增,因此来回切换版本号
|
||||
(包括回退到打过包的旧版本号)都不会重复,不会被 App Store 拒绝。
|
||||
每个 App_Ver 的构建号在所属轨道内独立保存于 build_map 各自递增,因此来回切换
|
||||
版本号(包括回退到打过包的旧版本号)都不会重复,不会被 App Store 拒绝。
|
||||
|
||||
- 不传 build_ver:保留该版本号已记录的构建号,仅更新当前 App_Ver。
|
||||
- track:release(master/develop,上架用)或 feature(自测用),缺省 release。
|
||||
- 不传 build_ver:保留该版本号已记录的构建号,仅更新该轨道的当前 App_Ver。
|
||||
- 传 build_ver:形如 2.196.0.3,前三位须与 app_ver 一致,第四位为构建号;
|
||||
直接写入该版本号的记录(可用于手动对齐 App Store Connect 上的已传构建号)。
|
||||
"""
|
||||
track_name = (data.get("track") or TRACK_RELEASE).strip()
|
||||
if track_name not in TRACKS:
|
||||
raise HTTPException(status_code=400, detail=f"未知的版本轨道: {track_name}")
|
||||
|
||||
app_ver = data.get("app_ver", "").strip()
|
||||
if not re.fullmatch(r"\d+\.\d+\.\d+", app_ver):
|
||||
raise HTTPException(status_code=400, detail="App_Ver 必须为主版本.次版本.修订号,例如 2.196.0")
|
||||
@@ -683,17 +795,19 @@ async def update_versions(data: dict):
|
||||
|
||||
async with _config_lock:
|
||||
config = load_config()
|
||||
versions = config.setdefault("versions", DEFAULT_VERSIONS.copy())
|
||||
versions["app_ver"] = app_ver
|
||||
build_map = versions.setdefault("build_map", {})
|
||||
tracks = config.setdefault("versions", {}).setdefault("tracks", {})
|
||||
track = tracks.setdefault(track_name, _new_track())
|
||||
track["app_ver"] = app_ver
|
||||
build_map = track.setdefault("build_map", {})
|
||||
if build_no is not None:
|
||||
build_map[app_ver] = build_no
|
||||
save_config(config)
|
||||
|
||||
return {
|
||||
"message": "版本号已更新",
|
||||
"track": track_name,
|
||||
"app_ver": app_ver,
|
||||
"build_ver": f"{app_ver}.{int(build_map.get(app_ver, 0))}",
|
||||
"build_ver": track_build_ver(track),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -601,29 +601,40 @@ async def generate_config(task_id: str, task, build_dir: Path) -> dict:
|
||||
"""生成打包配置"""
|
||||
await log_streamer.emit_step(task_id, "生成配置")
|
||||
|
||||
from ..routers.config import load_config, _config_lock
|
||||
from ..routers.config import (
|
||||
load_config, _config_lock, get_track, get_track_name, track_build_ver,
|
||||
TRACK_RELEASE, _new_track,
|
||||
)
|
||||
|
||||
config = load_config()
|
||||
apps = config.get("apps", {})
|
||||
schemes = config.get("schemes", {})
|
||||
versions = config.get("versions", {})
|
||||
|
||||
app = apps.get(task.app_id, {})
|
||||
scheme = schemes.get(task.scheme_id, {})
|
||||
|
||||
# 版本号由当前服务配置统一管理,不读取分支源码中的手动打包脚本。
|
||||
app_ver = versions.get("app_ver", "2.180.0")
|
||||
# 按分支所属轨道取版本号:master/develop 走 release 轨(上架用),feature 走 feature 轨(自测用),
|
||||
# 两条轨道的 App_Ver 各自独立。
|
||||
track_name = get_track_name(config, task.branch)
|
||||
track = get_track(config, track_name)
|
||||
app_ver = track.get("app_ver") or "2.180.0"
|
||||
|
||||
# 构建号按版本号独立记录:build_map[app_ver] 为该版本号已用到的最大第四位。
|
||||
build_map = versions.get("build_map", {}) or {}
|
||||
build_ver = f"{app_ver}.{int(build_map.get(app_ver, 0))}"
|
||||
build_ver = track_build_ver(track)
|
||||
await log_streamer.emit(
|
||||
task_id,
|
||||
f"版本轨道: {track_name}({'上架,App_Store 打包自增' if track_name == TRACK_RELEASE else '自测,构建号不自增'})",
|
||||
)
|
||||
|
||||
# App_Store 打包时,在该版本号自己的构建号上 +1(回退到旧版本号也从它自己的最大值继续)
|
||||
if task.build_type == "App_Store":
|
||||
# release 轨的 App_Store 打包,在该版本号自己的构建号上 +1
|
||||
# (回退到旧版本号也从它自己的最大值继续);feature 轨始终使用手动设定的构建号。
|
||||
if track_name == TRACK_RELEASE and task.build_type == "App_Store":
|
||||
async with _config_lock:
|
||||
config = load_config()
|
||||
versions = config.setdefault("versions", {"app_ver": app_ver, "build_map": {}})
|
||||
build_map = versions.setdefault("build_map", {})
|
||||
tracks = config.setdefault("versions", {}).setdefault("tracks", {})
|
||||
track = tracks.setdefault(track_name, _new_track(app_ver))
|
||||
build_map = track.setdefault("build_map", {})
|
||||
n = int(build_map.get(app_ver, 0)) + 1
|
||||
build_map[app_ver] = n
|
||||
build_ver = f"{app_ver}.{n}"
|
||||
|
||||
+5
-2
@@ -70,7 +70,10 @@
|
||||
}
|
||||
},
|
||||
"versions": {
|
||||
"app_ver": "2.180.0",
|
||||
"build_map": {}
|
||||
"tracks": {
|
||||
"release": { "app_ver": "2.180.0", "build_map": {} },
|
||||
"feature": { "app_ver": "2.180.0", "build_map": {} }
|
||||
}
|
||||
},
|
||||
"branch_track": { "master": "release", "develop": "release" }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,15 @@ function mockFetch(url) {
|
||||
'/api/config/servers': { '测试环境': { api: 'https://test.com', assDom: '', universalLink: '' } },
|
||||
'/api/config/branches': ['main', 'dev'],
|
||||
'/api/config/build': { max_concurrent_builds: 2, build_dir_retention_hours: 24, build_base_dir: '/tmp' },
|
||||
'/api/config/versions': { app_ver: '2.195.0', build_ver: '2.195.0.0' },
|
||||
'/api/config/versions': {
|
||||
app_ver: '2.195.0',
|
||||
build_ver: '2.195.0.0',
|
||||
tracks: {
|
||||
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: {} },
|
||||
},
|
||||
branch_track: { main: 'release', dev: 'release' },
|
||||
},
|
||||
'/api/config': { apps: {}, schemes: {}, branches: ['main'] },
|
||||
}
|
||||
return Promise.resolve({
|
||||
@@ -168,20 +176,76 @@ describe('ConfigView.vue', () => {
|
||||
expect(window.alert).toHaveBeenCalledWith('设置已保存')
|
||||
})
|
||||
|
||||
it('普通账号可修改版本号但看不到打包参数', async () => {
|
||||
it('普通账号只能管理 Apps,看不到打包设置与版本号', async () => {
|
||||
const wrapper = mountConfigView({ admin: false })
|
||||
await flushPromises()
|
||||
|
||||
const menuTexts = wrapper.findAll('.sidebar-menu li').map(li => li.text())
|
||||
expect(menuTexts).toEqual(['Apps 配置', '打包设置'])
|
||||
expect(menuTexts).toEqual(['Apps 配置'])
|
||||
|
||||
expect(wrapper.text()).not.toContain('上架版本号')
|
||||
expect(wrapper.text()).not.toContain('自测版本号')
|
||||
expect(wrapper.text()).not.toContain('打包参数')
|
||||
// 版本号接口为管理员专属,普通账号不再请求
|
||||
expect(fetch).not.toHaveBeenCalledWith('/api/config/versions', expect.anything())
|
||||
expect(fetch.mock.calls.map(c => c[0])).toEqual(['/api/config/apps'])
|
||||
})
|
||||
|
||||
it('两条版本轨道各自独立展示与保存', async () => {
|
||||
const calls = []
|
||||
fetch.mockImplementation((url, opts) => {
|
||||
if (url === '/api/config/versions' && opts?.method === 'PUT') {
|
||||
calls.push(JSON.parse(opts.body))
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ app_ver: '2.101.0', build_ver: '2.101.0.0' }),
|
||||
})
|
||||
}
|
||||
return mockFetch(url)
|
||||
})
|
||||
window.alert = vi.fn()
|
||||
|
||||
const wrapper = mountConfigView()
|
||||
await flushPromises()
|
||||
|
||||
const buildMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '打包设置')
|
||||
await buildMenu.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('应用版本号')
|
||||
expect(wrapper.text()).not.toContain('打包参数')
|
||||
// 非管理员在打包设置页可见 App_Ver 与 Build_Ver 两个输入框
|
||||
expect(wrapper.findAll('input').length).toBe(2)
|
||||
const inputs = wrapper.findAll('input[type="text"]')
|
||||
expect(inputs[0].element.value).toBe('2.195.0') // release App_Ver
|
||||
expect(inputs[2].element.value).toBe('2.100.0') // feature App_Ver
|
||||
|
||||
await inputs[2].setValue('2.101.0')
|
||||
const saveBtn = wrapper.findAll('.btn-primary').find(b => b.text() === '保存自测版本号')
|
||||
await saveBtn.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(calls).toEqual([{ track: 'feature', app_ver: '2.101.0', build_ver: '2.101.0.0' }])
|
||||
})
|
||||
|
||||
it('分支管理可切换版本轨道', async () => {
|
||||
const calls = []
|
||||
fetch.mockImplementation((url, opts) => {
|
||||
if (url === '/api/config/branches/track' && opts?.method === 'PUT') {
|
||||
calls.push(JSON.parse(opts.body))
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({ message: 'ok' }) })
|
||||
}
|
||||
return mockFetch(url)
|
||||
})
|
||||
|
||||
const wrapper = mountConfigView()
|
||||
await flushPromises()
|
||||
|
||||
const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理')
|
||||
await branchesMenu.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const select = wrapper.find('.config-table select')
|
||||
expect(select.element.value).toBe('release')
|
||||
await select.setValue('feature')
|
||||
await flushPromises()
|
||||
|
||||
expect(calls).toEqual([{ branch: 'main', track: 'feature' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'schemes' }" @click="tab = 'schemes'">Schemes 配置</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'branches' }" @click="tab = 'branches'">分支管理</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'upload' }" @click="tab = 'upload'">上传配置</li>
|
||||
<li :class="{ active: tab === 'build' }" @click="tab = 'build'">打包设置</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'build' }" @click="tab = 'build'">打包设置</li>
|
||||
<li v-if="isAdmin" :class="{ active: tab === 'json' }" @click="tab = 'json'">JSON 编辑</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -151,7 +151,10 @@
|
||||
<!-- 分支管理 -->
|
||||
<div v-if="tab === 'branches'">
|
||||
<h2>分支管理</h2>
|
||||
<p style="color: #666; margin-bottom: 16px; font-size: 14px;">管理可打包的代码分支,打包时从对应分支目录获取源码</p>
|
||||
<p style="color: #666; margin-bottom: 16px; font-size: 14px;">
|
||||
管理可打包的代码分支,打包时从对应分支目录获取源码。
|
||||
版本轨道决定该分支使用哪一套版本号:上架(master/develop 共用,App_Store 打包构建号自增)、自测(feature 用,构建号不自增)。
|
||||
</p>
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
|
||||
<input v-model="newBranch" type="text" placeholder="输入分支名称" style="flex: 1; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 14px;">
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 24px;" @click="addBranch">添加分支</button>
|
||||
@@ -160,18 +163,25 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分支名称</th>
|
||||
<th>版本轨道</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="b in branches" :key="b">
|
||||
<td><strong>{{ b }}</strong></td>
|
||||
<td>
|
||||
<select :value="branchTrack[b] || 'feature'" style="width: auto; padding: 4px 8px;" @change="setBranchTrack(b, $event.target.value)">
|
||||
<option value="release">上架(master/develop)</option>
|
||||
<option value="feature">自测(feature)</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="action-btns">
|
||||
<button class="action-btn delete" @click="deleteBranch(b)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!branches.length">
|
||||
<td colspan="2" style="text-align: center; color: #999;">暂无分支配置</td>
|
||||
<td colspan="3" style="text-align: center; color: #999;">暂无分支配置</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -269,21 +279,36 @@
|
||||
<div v-if="tab === 'build'">
|
||||
<h2>打包设置</h2>
|
||||
<div style="max-width: 500px;">
|
||||
<h4 class="section-title">应用版本号</h4>
|
||||
<h4 class="section-title">上架版本号(master / develop)</h4>
|
||||
<div class="form-group">
|
||||
<label>App_Ver(应用版本)</label>
|
||||
<input type="text" v-model="versions.app_ver" placeholder="2.180.0" @input="onAppVerInput">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号</div>
|
||||
<input type="text" v-model="versions.release.app_ver" placeholder="2.180.0" @input="onAppVerInput('release')">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">格式:主版本.次版本.修订号。master 与 develop 共用这一套版本号。</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Build_Ver(构建号)</label>
|
||||
<input type="text" v-model="versions.build_ver" placeholder="2.180.0.0">
|
||||
<input type="text" v-model="versions.release.build_ver" placeholder="2.180.0.0">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">
|
||||
四段数字,前三位须与 App_Ver 一致。修改 App_Ver 会自动切换为该版本号已记录的构建号;
|
||||
每个版本号的构建号独立递增,App_Store 打包时自动 +1。一般无需手动修改。
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions">保存版本号</button>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions('release')">保存上架版本号</button>
|
||||
|
||||
<h4 class="section-title">自测版本号(feature 分支)</h4>
|
||||
<div class="form-group">
|
||||
<label>App_Ver(应用版本)</label>
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<label>Build_Ver(构建号)</label>
|
||||
<input type="text" v-model="versions.feature.build_ver" placeholder="2.180.0.0">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">
|
||||
仅供自测,打包时<strong>不会自动递增</strong>,始终使用这里填写的值(默认 .0)。需要区分时请手动修改。
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 32px; margin-bottom: 24px;" @click="saveVersions('feature')">保存自测版本号</button>
|
||||
|
||||
<template v-if="isAdmin">
|
||||
<h4 class="section-title">打包参数</h4>
|
||||
@@ -613,20 +638,30 @@ const servers = ref({})
|
||||
const branches = ref([])
|
||||
const newBranch = ref('')
|
||||
const buildSettings = ref({})
|
||||
const versions = ref({ app_ver: '', build_ver: '' })
|
||||
const buildMap = ref({})
|
||||
// 两条版本轨道:release(master/develop,上架用)与 feature(自测用),各自独立
|
||||
const versions = ref({
|
||||
release: { app_ver: '', build_ver: '' },
|
||||
feature: { app_ver: '', build_ver: '' },
|
||||
})
|
||||
const buildMap = ref({ release: {}, feature: {} })
|
||||
const branchTrack = ref({})
|
||||
|
||||
const applyVersions = (data) => {
|
||||
buildMap.value = data.build_map || {}
|
||||
versions.value = { app_ver: data.app_ver || '', build_ver: data.build_ver || '' }
|
||||
const tracks = data.tracks || {}
|
||||
for (const track of ['release', 'feature']) {
|
||||
const item = tracks[track] || {}
|
||||
buildMap.value[track] = item.build_map || {}
|
||||
versions.value[track] = { app_ver: item.app_ver || '', build_ver: item.build_ver || '' }
|
||||
}
|
||||
branchTrack.value = data.branch_track || {}
|
||||
}
|
||||
|
||||
// 修改 App_Ver 时,自动把 Build_Ver 切换为该版本号已记录的构建号(新版本号则为 .0)
|
||||
const onAppVerInput = () => {
|
||||
const appVer = (versions.value.app_ver || '').trim()
|
||||
const onAppVerInput = (track) => {
|
||||
const appVer = (versions.value[track].app_ver || '').trim()
|
||||
if (/^\d+\.\d+\.\d+$/.test(appVer)) {
|
||||
const n = Number(buildMap.value[appVer] || 0)
|
||||
versions.value.build_ver = `${appVer}.${n}`
|
||||
const n = Number((buildMap.value[track] || {})[appVer] || 0)
|
||||
versions.value[track].build_ver = `${appVer}.${n}`
|
||||
}
|
||||
}
|
||||
const uploadConfig = ref({ mode: 'oss', oss: {}, webdav: {}, dingtalk: {} })
|
||||
@@ -659,12 +694,9 @@ onMounted(async () => {
|
||||
|
||||
const loadData = async () => {
|
||||
if (!isAdmin.value) {
|
||||
const [appsRes, versionsRes] = await Promise.all([
|
||||
authFetch('/api/config/apps'),
|
||||
authFetch('/api/config/versions'),
|
||||
])
|
||||
// 普通用户只能管理 Apps,版本号等配置均为管理员专属
|
||||
const appsRes = await authFetch('/api/config/apps')
|
||||
if (appsRes.ok) apps.value = await appsRes.json()
|
||||
if (versionsRes.ok) applyVersions(await versionsRes.json())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1111,6 +1143,24 @@ const addBranch = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const setBranchTrack = async (name, track) => {
|
||||
try {
|
||||
const res = await authFetch('/api/config/branches/track', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ branch: name, track }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || '修改失败')
|
||||
}
|
||||
branchTrack.value = { ...branchTrack.value, [name]: track }
|
||||
} catch (e) {
|
||||
alert('修改失败: ' + e.message)
|
||||
await loadData()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBranch = async (name) => {
|
||||
if (!confirm(`确定删除分支「${name}」?`)) return
|
||||
try {
|
||||
@@ -1132,21 +1182,25 @@ const saveBuildSettings = async () => {
|
||||
alert('设置已保存')
|
||||
}
|
||||
|
||||
const saveVersions = async () => {
|
||||
const saveVersions = async (track = 'release') => {
|
||||
try {
|
||||
const res = await authFetch('/api/config/versions', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
app_ver: versions.value.app_ver,
|
||||
build_ver: versions.value.build_ver,
|
||||
track,
|
||||
app_ver: versions.value[track].app_ver,
|
||||
build_ver: versions.value[track].build_ver,
|
||||
}),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
// 回写该版本号最新的构建号,保持本地 build_map 与服务端一致
|
||||
buildMap.value = { ...buildMap.value, [data.app_ver]: Number((data.build_ver.split('.')[3]) || 0) }
|
||||
versions.value = { app_ver: data.app_ver, build_ver: data.build_ver }
|
||||
buildMap.value[track] = {
|
||||
...(buildMap.value[track] || {}),
|
||||
[data.app_ver]: Number((data.build_ver.split('.')[3]) || 0),
|
||||
}
|
||||
versions.value[track] = { app_ver: data.app_ver, build_ver: data.build_ver }
|
||||
alert('版本号已保存')
|
||||
} else {
|
||||
const err = await res.json()
|
||||
|
||||
@@ -201,3 +201,64 @@ def test_update_versions_rejects_invalid_app_version(client, tmp_config):
|
||||
resp = client.put("/api/config/versions", json={"app_ver": "2.196"})
|
||||
assert resp.status_code == 400
|
||||
assert "App_Ver" in resp.json()["detail"]
|
||||
|
||||
|
||||
def test_versions_tracks_are_independent(client, tmp_config):
|
||||
# 两条轨道的 App_Ver 各自独立,互不影响
|
||||
assert client.put("/api/config/versions", json={"track": "release", "app_ver": "2.196.0"}).status_code == 200
|
||||
assert client.put("/api/config/versions", json={"track": "feature", "app_ver": "2.100.0"}).status_code == 200
|
||||
|
||||
data = client.get("/api/config/versions").json()
|
||||
assert data["tracks"]["release"]["app_ver"] == "2.196.0"
|
||||
assert data["tracks"]["feature"]["app_ver"] == "2.100.0"
|
||||
assert data["tracks"]["feature"]["build_ver"] == "2.100.0.0"
|
||||
# 顶层字段镜像 release 轨,兼容旧客户端
|
||||
assert data["app_ver"] == "2.196.0"
|
||||
|
||||
|
||||
def test_update_versions_rejects_unknown_track(client, tmp_config):
|
||||
resp = client.put("/api/config/versions", json={"track": "beta", "app_ver": "2.196.0"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_legacy_versions_migrate_to_release_track(client, tmp_config):
|
||||
import json
|
||||
with open(tmp_config, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
cfg["versions"] = {"app_ver": "2.195.0", "build_ver": "2.195.0.7"}
|
||||
with open(tmp_config, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f)
|
||||
|
||||
data = client.get("/api/config/versions").json()
|
||||
assert data["tracks"]["release"] == {
|
||||
"app_ver": "2.195.0",
|
||||
"build_ver": "2.195.0.7",
|
||||
"build_map": {"2.195.0": 7},
|
||||
}
|
||||
# feature 轨以同一 App_Ver 初始化,构建号从 .0 起
|
||||
assert data["tracks"]["feature"]["app_ver"] == "2.195.0"
|
||||
assert data["tracks"]["feature"]["build_ver"] == "2.195.0.0"
|
||||
|
||||
|
||||
def test_branch_track_defaults_and_override(client, tmp_config):
|
||||
assert client.post("/api/config/branches", json={"name": "develop"}).status_code == 200
|
||||
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
||||
|
||||
tracks = client.get("/api/config/versions").json()["branch_track"]
|
||||
assert tracks["develop"] == "release"
|
||||
assert tracks["feature/login"] == "feature"
|
||||
|
||||
# 手动改归属
|
||||
resp = client.put("/api/config/branches/track", json={"branch": "feature/login", "track": "release"})
|
||||
assert resp.status_code == 200
|
||||
assert client.get("/api/config/versions").json()["branch_track"]["feature/login"] == "release"
|
||||
|
||||
assert client.put("/api/config/branches/track", json={"branch": "nope", "track": "release"}).status_code == 404
|
||||
assert client.put("/api/config/branches/track", json={"branch": "develop", "track": "beta"}).status_code == 400
|
||||
|
||||
|
||||
def test_delete_branch_with_slash_in_name(client, tmp_config):
|
||||
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
||||
resp = client.delete("/api/config/branches/feature%2Flogin")
|
||||
assert resp.status_code == 200
|
||||
assert "feature/login" not in client.get("/api/config/branches").json()
|
||||
|
||||
+86
-13
@@ -315,6 +315,14 @@ def test_resolve_provisioning_profile_extracts_name_and_team(tmp_path):
|
||||
assert _resolve_provisioning_profile(str(profile_path)) == ("a4YX061_adHoc", "MG4Z7FU83W")
|
||||
|
||||
|
||||
def _versions_config(release: dict, feature: dict) -> dict:
|
||||
return {
|
||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
||||
"versions": {"tracks": {"release": release, "feature": feature}},
|
||||
}
|
||||
|
||||
|
||||
async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||
"""从服务配置读取版本号,不依赖分支源码脚本"""
|
||||
build_dir = tmp_path / "build"
|
||||
@@ -325,12 +333,12 @@ async def test_generate_config_uses_saved_version(tmp_path, log_streamer):
|
||||
task.scheme_id = "1"
|
||||
task.build_type = "Ad_Hoc"
|
||||
task.obfuscation = False
|
||||
task.branch = "master"
|
||||
|
||||
mock_config = {
|
||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
||||
"versions": {"app_ver": "3.0.1", "build_ver": "3.0.1.0"},
|
||||
}
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "3.0.1", "build_map": {}},
|
||||
{"app_ver": "9.9.9", "build_map": {}},
|
||||
)
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
@@ -344,26 +352,91 @@ async def test_generate_config_appstore_increments_per_version(tmp_path, log_str
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store", obfuscation=False)
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||
obfuscation=False, branch="master")
|
||||
|
||||
# 2.195.0 已打到 .5,切到 2.196.0 后又回退到 2.195.0
|
||||
mock_config = {
|
||||
"apps": {"1": {"name": "App", "certificates": {}}},
|
||||
"schemes": {"1": {"name": "sch", "ossFloder": "f"}},
|
||||
"versions": {"app_ver": "2.195.0", "build_map": {"2.195.0": 5, "2.196.0": 3}},
|
||||
}
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5, "2.196.0": 3}},
|
||||
{"app_ver": "2.195.0", "build_map": {}},
|
||||
)
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||
patch("backend.routers.config.save_config") as save_mock:
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
# 从 2.195.0 自身的 5 继续到 6,不受 2.196.0 影响,也不撞已传的 .5
|
||||
release_map = mock_config["versions"]["tracks"]["release"]["build_map"]
|
||||
assert result["BUILD_VERSION"] == "2.195.0.6"
|
||||
assert mock_config["versions"]["build_map"]["2.195.0"] == 6
|
||||
assert mock_config["versions"]["build_map"]["2.196.0"] == 3
|
||||
assert release_map["2.195.0"] == 6
|
||||
assert release_map["2.196.0"] == 3
|
||||
save_mock.assert_called()
|
||||
|
||||
|
||||
async def test_generate_config_develop_shares_release_track(tmp_path, log_streamer):
|
||||
"""develop 与 master 共用 release 轨的版本号与构建号序列"""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||
obfuscation=False, branch="develop")
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5}},
|
||||
{"app_ver": "2.100.0", "build_map": {}},
|
||||
)
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||
patch("backend.routers.config.save_config"):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
assert result["VERSION"] == "2.195.0"
|
||||
assert result["BUILD_VERSION"] == "2.195.0.6"
|
||||
|
||||
|
||||
async def test_generate_config_feature_branch_never_increments(tmp_path, log_streamer):
|
||||
"""feature 分支用自己的版本号,且构建号永不自增(即使是 App_Store 打包)"""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="App_Store",
|
||||
obfuscation=False, branch="feature/login")
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.195.0", "build_map": {"2.195.0": 5}},
|
||||
{"app_ver": "2.100.0", "build_map": {}},
|
||||
)
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config), \
|
||||
patch("backend.routers.config.save_config") as save_mock:
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
assert result["VERSION"] == "2.100.0"
|
||||
assert result["BUILD_VERSION"] == "2.100.0.0"
|
||||
assert mock_config["versions"]["tracks"]["feature"]["build_map"] == {}
|
||||
save_mock.assert_not_called()
|
||||
|
||||
|
||||
async def test_generate_config_branch_track_override(tmp_path, log_streamer):
|
||||
"""branch_track 中的显式配置优先于按分支名推断"""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
|
||||
task = MagicMock(app_id="1", scheme_id="1", build_type="Ad_Hoc",
|
||||
obfuscation=False, branch="release/2.196")
|
||||
|
||||
mock_config = _versions_config(
|
||||
{"app_ver": "2.196.0", "build_map": {}},
|
||||
{"app_ver": "2.100.0", "build_map": {}},
|
||||
)
|
||||
mock_config["branch_track"] = {"release/2.196": "feature"}
|
||||
|
||||
with patch("backend.routers.config.load_config", return_value=mock_config):
|
||||
result = await generate_config("t1", task, build_dir)
|
||||
|
||||
assert result["VERSION"] == "2.100.0"
|
||||
|
||||
|
||||
async def test_generate_config_uses_server_theme_not_source_autopacking(tmp_path, log_streamer):
|
||||
"""Web 打包不读取分支源码中的 AutoPacking 目录。"""
|
||||
build_dir = tmp_path / "build"
|
||||
|
||||
@@ -37,8 +37,8 @@ def test_production_rejects_default_security_settings(monkeypatch):
|
||||
config.validate_production_security()
|
||||
|
||||
|
||||
def test_config_routes_limit_regular_users_to_apps_and_versions(client, tmp_config):
|
||||
"""普通账号可管理 Apps 与版本号,不能读取或修改管理员配置。"""
|
||||
def test_config_routes_limit_regular_users_to_apps(client, tmp_config):
|
||||
"""普通账号只能管理 Apps,版本号等其余配置均为管理员专属。"""
|
||||
response = client.post("/api/users", json={
|
||||
"username": "builder",
|
||||
"password": "builder-password-123",
|
||||
@@ -57,10 +57,13 @@ def test_config_routes_limit_regular_users_to_apps_and_versions(client, tmp_conf
|
||||
assert client.post("/api/config/apps", json={
|
||||
"name": "普通用户 App", "server": "测试环境",
|
||||
}, headers=headers).status_code == 200
|
||||
assert client.get("/api/config/versions", headers=headers).status_code == 200
|
||||
versions = client.put("/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers)
|
||||
assert versions.status_code == 200
|
||||
assert versions.json()["build_ver"] == "2.196.0.0"
|
||||
assert client.get("/api/config/versions", headers=headers).status_code == 403
|
||||
assert client.put(
|
||||
"/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers
|
||||
).status_code == 403
|
||||
assert client.put(
|
||||
"/api/config/branches/track", json={"branch": "master", "track": "feature"}, headers=headers
|
||||
).status_code == 403
|
||||
assert client.get("/api/config", headers=headers).status_code == 403
|
||||
assert client.get("/api/config/servers", headers=headers).status_code == 403
|
||||
assert client.put("/api/config/build", json={"max_concurrent_builds": 1}, headers=headers).status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user