244 lines
8.5 KiB
Python
244 lines
8.5 KiB
Python
#!/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
|
|
from urllib.parse import quote
|
|
|
|
import httpx
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
|
|
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
|
|
|
|
signature = sig_file.read_text().strip()
|
|
file_size = pkg_file.stat().st_size
|
|
filename = pkg_file.name
|
|
|
|
# 判断文件名是否包含架构标识
|
|
has_arch_marker = any(marker in filename for marker in ["aarch64", "arm64", "x86_64"])
|
|
|
|
if has_arch_marker:
|
|
# 文件名含架构标识,按标识解析
|
|
arch = "aarch64" if "aarch64" in filename or "arm64" in filename else "x86_64"
|
|
archs = [arch]
|
|
else:
|
|
# Universal Binary:同时支持 x86_64 和 aarch64
|
|
archs = ["x86_64", "aarch64"]
|
|
|
|
for arch in archs:
|
|
packages.append({
|
|
"platform": "darwin",
|
|
"architecture": arch,
|
|
"filename": filename,
|
|
"local_path": str(pkg_file),
|
|
"signature": signature,
|
|
"file_size": file_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,
|
|
})
|
|
|
|
# macOS DMG: 给新用户首次安装用(无签名文件)
|
|
for dmg_file in bundle_dir.rglob("*.dmg"):
|
|
filename = dmg_file.name
|
|
|
|
# 判断文件名是否包含架构标识
|
|
has_arch_marker = any(marker in filename for marker in ["aarch64", "arm64", "x86_64"])
|
|
if has_arch_marker:
|
|
arch = "aarch64" if "aarch64" in filename or "arm64" in filename else "x86_64"
|
|
archs = [arch]
|
|
else:
|
|
# Universal DMG:同时支持 x86_64 和 aarch64
|
|
archs = ["x86_64", "aarch64"]
|
|
|
|
for arch in archs:
|
|
packages.append({
|
|
"platform": "darwin",
|
|
"architecture": arch,
|
|
"filename": filename,
|
|
"local_path": str(dmg_file),
|
|
"signature": "", # DMG 无签名(非 updater 包)
|
|
"file_size": dmg_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="https://media.liche.cn/meijiaka-zy/releases", 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="https://media.liche.cn/meijiaka-zy/releases", 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)
|
|
# 按 local_path 去重,同一个文件只上传一次
|
|
uploaded = {} # local_path -> file_url
|
|
for p in packages:
|
|
if args.skip_upload:
|
|
if not args.base_url:
|
|
print("错误: --skip-upload 时必须提供 --base-url")
|
|
sys.exit(1)
|
|
encoded_filename = quote(p['filename'], safe='')
|
|
p["file_url"] = f"{args.base_url.rstrip('/')}/{p['platform']}/{encoded_filename}"
|
|
else:
|
|
local_path = p["local_path"]
|
|
if local_path not in uploaded:
|
|
key = f"meijiaka-zy/releases/{args.version}/{p['platform']}/{p['filename']}"
|
|
print(f"上传 {p['filename']} ({p['platform']}) 到七牛云...")
|
|
uploaded[local_path] = upload_to_qiniu(local_path, key)
|
|
encoded_filename = quote(p['filename'], safe='')
|
|
p["file_url"] = f"{uploaded[local_path].rsplit('/', 1)[0]}/{encoded_filename}"
|
|
|
|
# 删除临时字段
|
|
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()
|