#!/usr/bin/env python3 """ 手动发布版本脚本 ================ 用法: python scripts/publish_release.py \ --version 1.6.0 \ --notes "修复视频导出崩溃\n优化启动速度" \ --bundle-dir ../tauri-app/src-tauri/target/release/bundle 流程: 1. 扫描 bundle 目录,找到 .app.tar.gz / .exe / .AppImage 和对应的 .sig 文件 2. 上传安装包到七牛云 3. 读取 .sig 文件内容 4. 调用后端 API 创建版本发布记录 依赖: pip install qiniu httpx """ import argparse import sys from pathlib import Path import httpx def find_packages(bundle_dir: Path) -> list[dict]: """扫描构建产物目录,找到安装包和对应的签名文件""" packages = [] # macOS: .app.tar.gz + .app.tar.gz.sig for sig_file in bundle_dir.rglob("*.app.tar.gz.sig"): pkg_file = sig_file.with_suffix("") # 去掉 .sig if not pkg_file.exists(): print(f"警告: 签名文件存在但安装包缺失: {pkg_file}") continue # 解析文件名:{name}_{version}_{target}_{arch}.app.tar.gz parts = pkg_file.stem.split("_") # 最后一个部分可能是 arch,需要处理 arch = "aarch64" if "aarch64" in pkg_file.name or "arm64" in pkg_file.name else "x86_64" signature = sig_file.read_text().strip() packages.append({ "platform": "darwin", "architecture": arch, "filename": pkg_file.name, "local_path": str(pkg_file), "signature": signature, "file_size": pkg_file.stat().st_size, }) # Windows: .exe + .exe.sig for sig_file in bundle_dir.rglob("*.exe.sig"): pkg_file = sig_file.with_suffix("") if not pkg_file.exists(): print(f"警告: 签名文件存在但安装包缺失: {pkg_file}") continue arch = "x86_64" # Windows 目前只打 x86_64 signature = sig_file.read_text().strip() packages.append({ "platform": "windows", "architecture": arch, "filename": pkg_file.name, "local_path": str(pkg_file), "signature": signature, "file_size": pkg_file.stat().st_size, }) # Linux: .AppImage + .AppImage.sig for sig_file in bundle_dir.rglob("*.AppImage.sig"): pkg_file = sig_file.with_suffix("") if not pkg_file.exists(): print(f"警告: 签名文件存在但安装包缺失: {pkg_file}") continue arch = "x86_64" # Linux 目前只打 x86_64 signature = sig_file.read_text().strip() packages.append({ "platform": "linux", "architecture": arch, "filename": pkg_file.name, "local_path": str(pkg_file), "signature": signature, "file_size": pkg_file.stat().st_size, }) return packages def upload_to_qiniu(local_path: str, key: str) -> str: """上传文件到七牛云,返回访问 URL""" # 这里使用七牛云 SDK,需要配置环境变量 # 复用现有七牛云密钥和 media bucket import os from qiniu import Auth, put_file access_key = os.environ["QINIU_ACCESS_KEY"] secret_key = os.environ["QINIU_SECRET_KEY"] bucket_name = os.environ["QINIU_VIDEO_BUCKET"] domain = os.environ["QINIU_VIDEO_DOMAIN"] auth = Auth(access_key, secret_key) token = auth.upload_token(bucket_name, key, 3600) ret, info = put_file(token, key, local_path, version="v2") if ret is None: raise RuntimeError(f"上传失败: {info}") return f"https://{domain}/{key}" def create_release(api_base_url: str, version: str, notes: str, mandatory: bool, packages: list[dict]): """调用后端 API 创建版本发布记录""" url = f"{api_base_url}/api/v1/update/releases" payload = { "version": version, "notes": notes, "mandatory": mandatory, "packages": packages, } response = httpx.post(url, json=payload, timeout=30) response.raise_for_status() return response.json() def main(): parser = argparse.ArgumentParser(description="手动发布应用版本") parser.add_argument("--version", required=True, help="版本号,如 1.6.0") parser.add_argument("--notes", default="", help="更新说明(支持 \\n 换行)") parser.add_argument("--mandatory", action="store_true", help="强制更新") parser.add_argument("--bundle-dir", required=True, help="Tauri 构建产物目录") parser.add_argument("--api-url", default="https://dev.tapi.meijiaka.cn", help="后端 API 地址") parser.add_argument("--skip-upload", action="store_true", help="跳过七牛云上传(包已上传)") parser.add_argument("--base-url", default="", help="安装包基础 URL(跳过上传时使用)") args = parser.parse_args() bundle_dir = Path(args.bundle_dir) if not bundle_dir.exists(): print(f"错误: 构建目录不存在: {bundle_dir}") sys.exit(1) # 1. 扫描构建产物 print(f"扫描构建目录: {bundle_dir}") packages = find_packages(bundle_dir) if not packages: print("错误: 未找到任何安装包或签名文件") print("请确认已运行: export TAURI_SIGNING_PRIVATE_KEY=... && npm run tauri build") sys.exit(1) print(f"发现 {len(packages)} 个平台包:") for p in packages: print(f" - {p['platform']}-{p['architecture']}: {p['filename']} ({p['file_size'] / 1024 / 1024:.1f} MB)") # 2. 上传到七牛云(或构造 URL) for p in packages: if args.skip_upload: if not args.base_url: print("错误: --skip-upload 时必须提供 --base-url") sys.exit(1) p["file_url"] = f"{args.base_url.rstrip('/')}/{p['filename']}" else: key = f"releases/{args.version}/{p['filename']}" print(f"上传 {p['filename']} 到七牛云...") p["file_url"] = upload_to_qiniu(p["local_path"], key) # 删除临时字段 del p["local_path"] # 3. 创建版本发布记录 print(f"创建版本发布记录: {args.version}") try: result = create_release( api_base_url=args.api_url, version=args.version, notes=args.notes.replace("\\n", "\n"), mandatory=args.mandatory, packages=packages, ) print(f"发布成功!ID: {result['id']}") print(f"更新接口: {args.api_url}/api/v1/update/check?version={{current_version}}&target={{target}}&arch={{arch}}") except httpx.HTTPStatusError as e: print(f"发布失败: {e.response.status_code} - {e.response.text}") sys.exit(1) if __name__ == "__main__": main()