Files
2025-11-10 10:00:46 +08:00

402 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
import jwt
import time
import requests
import os
import sys
import datetime
from pathlib import Path
# ==============================================
# 重试配置参数(可按需修改)
# ==============================================
MAX_RETRIES = 10 # 查找构建的最大重试次数
RETRY_DELAY = 30 # 查找构建的重试间隔(秒)
SUBMIT_INIT_WAIT = 60 # 提交前初始等待时间(秒)
SUBMIT_MAX_RETRIES = 20 # 提交审核的最大重试次数
SUBMIT_BASE_DELAY = 30 # 提交审核的初始重试间隔(秒)
SUBMIT_DELAY_INCREMENT = 10 # 每次重试间隔递增(秒)
# ==============================================
# 其他配置参数
# ==============================================
def get_chinese_date_time():
"""
获取当前时间,以中文格式返回(如:8月5日 上午8点24分)
包含月、日、上午/下午、时、分,不包含秒数
"""
now = datetime.datetime.now()
month = now.month
day = now.day
hour_24 = now.hour
minute = now.minute
if hour_24 < 12:
period = "上午"
hour_12 = hour_24 if hour_24 != 0 else 12
else:
period = "下午"
hour_12 = hour_24 if hour_24 == 12 else hour_24 - 12
return f"{month}{day}{period}{hour_12}{minute}分"
# 获取参数
if len(sys.argv) >= 2 and sys.argv[1]:
env_name = sys.argv[1]
else:
env_name = 'develop'
if len(sys.argv) >= 3 and sys.argv[2]:
build_number_str = sys.argv[2]
else:
build_number_str = '68'
# 验证第一个参数(枚举值)
valid_envs = ['develop', 'sit', 'uat']
if env_name not in valid_envs:
print(f"错误:环境名称 '{env_name}' 无效")
print(f"有效环境名称:{', '.join(valid_envs)}")
sys.exit(1)
# 验证第二个参数(数字字符串)
if not build_number_str.isdigit():
print(f"错误:构建号 '{build_number_str}' 必须是纯数字字符串")
sys.exit(1)
# 参数验证通过,继续处理
TARGET_BUILD_NUMBER = build_number_str
if env_name == 'develop':
BETA_GROUP_NAME = "外部自动化测试"
BETA_GROUP_NAME = "UAT"
TARGET_VERSION = "0.3.1"
elif env_name == 'sit':
BETA_GROUP_NAME = "SIT"
TARGET_VERSION = "0.2.1"
elif env_name == 'uat':
BETA_GROUP_NAME = "UAT"
TARGET_VERSION = "0.3.1"
# 接收参数结束
# 配置参数
API_KEY = "ACRB9XFL9G"
API_ISSUER = "69a6de7f-3378-47e3-e053-5b8c7c11a4d1"
APP_NAME = "吉AI学"
API_ENDPOINT = "https://api.appstoreconnect.apple.com/v1"
if 'PYCHARM_HOSTED' in os.environ:
Auth_Key_Path = './'
else:
Auth_Key_Path = '/Users/jlbank/Desktop/lx_uniapp/auto_build'
private_key_path = Path(f'{Auth_Key_Path}/AuthKey_{API_KEY}.p8')
def generate_jwt_token(api_key, api_issuer, private_key_path):
"""生成 JWT 令牌"""
with open(private_key_path, "r") as f:
private_key = f.read()
current_time = int(time.time())
payload = {
"iss": api_issuer,
"exp": current_time + 20 * 60,
"iat": current_time,
"aud": "appstoreconnect-v1"
}
token = jwt.encode(
payload,
private_key,
algorithm="ES256",
headers={"kid": api_key}
)
return token
def find_specific_build(app_id, headers):
"""通过预发布版本ID查找指定构建版本,带重试机制"""
retries = 0
while retries < MAX_RETRIES:
try:
print(f"\n🔍 查找版本 {TARGET_VERSION} 构建号 {TARGET_BUILD_NUMBER} (尝试 {retries + 1}/{MAX_RETRIES})")
print(" 正在获取预发布版本ID...")
pre_release_url = f"{API_ENDPOINT}/preReleaseVersions"
pre_release_params = {
"filter[version]": TARGET_VERSION,
"filter[app]": app_id
}
pre_release_response = requests.get(pre_release_url, headers=headers, params=pre_release_params)
pre_release_response.raise_for_status()
pre_release_data = pre_release_response.json()
if not pre_release_data["data"]:
print(f"❓ 未找到版本为 {TARGET_VERSION} 的预发布版本")
retries += 1
if retries < MAX_RETRIES:
time.sleep(RETRY_DELAY)
continue
pre_release_id = pre_release_data["data"][0]["id"]
print(f" 找到预发布版本ID: {pre_release_id}")
builds_url = f"{API_ENDPOINT}/builds"
params = {
"filter[preReleaseVersion]": pre_release_id,
"sort": "-version",
"limit": "10",
"include": "buildBetaDetail,betaBuildLocalizations,preReleaseVersion,betaAppReviewSubmission,icons,buildBundles,betaGroups",
"limit[betaBuildLocalizations]": "40",
"limit[betaGroups]": "50"
}
response = requests.get(builds_url, headers=headers, params=params)
if response.status_code == 400:
print(f" 错误响应内容: {response.text}")
response.raise_for_status()
builds_data = response.json()
matching_builds = [
b for b in builds_data["data"]
if b["attributes"].get("version") == TARGET_BUILD_NUMBER
]
if not matching_builds:
print(f"❓ 未找到版本 {TARGET_VERSION} 构建号 {TARGET_BUILD_NUMBER} 的构建")
else:
valid_build = next((
b for b in matching_builds
if b["attributes"]["processingState"] == "VALID"
), None)
if valid_build:
return valid_build
else:
states = [b["attributes"]["processingState"] for b in matching_builds]
print(f"⏳ 找到匹配的构建但状态为: {', '.join(states)},等待处理完成...")
if retries < MAX_RETRIES - 1:
print(f"将在 {RETRY_DELAY} 秒后重试...")
time.sleep(RETRY_DELAY)
retries += 1
except Exception as e:
print(f"查询过程中出错: {str(e)}")
retries += 1
if retries < MAX_RETRIES:
time.sleep(RETRY_DELAY)
return None
# ==============================================
# 新增:带重试的提交审核函数
# ==============================================
def submit_for_review_with_retry(build_id, headers):
"""提交审核(带初始等待和动态递增间隔重试)"""
# 第一步:初始等待
print(f"\n⌛ 提交审核前,先等待 {SUBMIT_INIT_WAIT} 秒(确保构建完全处理完成)...")
time.sleep(SUBMIT_INIT_WAIT)
# 第二步:重试提交
for retry in range(SUBMIT_MAX_RETRIES):
current_delay = SUBMIT_BASE_DELAY + (retry * SUBMIT_DELAY_INCREMENT)
try:
print(f"\n📋 提交构建版本审核 (尝试 {retry + 1}/{SUBMIT_MAX_RETRIES})")
print(f" 当前重试间隔:{current_delay} 秒")
submit_url = f"{API_ENDPOINT}/betaAppReviewSubmissions"
submit_payload = {
"data": {
"type": "betaAppReviewSubmissions",
"relationships": {
"build": {
"data": {"id": build_id, "type": "builds"}
}
}
}
}
submit_response = requests.post(submit_url, headers=headers, json=submit_payload)
submit_response.raise_for_status()
print("✅ 构建版本已成功提交审核")
return True # 提交成功,退出函数
except requests.exceptions.HTTPError as e:
# 处理已提交的情况(409冲突)
if submit_response.status_code == 409:
print("⚠️ 构建已提交审核,无需重复操作")
return True
else:
print(f"❌ 第 {retry + 1} 次提交审核失败: {e}")
print("错误详情:", submit_response.json())
# 最后一次重试失败则退出
if retry == SUBMIT_MAX_RETRIES - 1:
print(f"\n❌ 已达到最大重试次数 ({SUBMIT_MAX_RETRIES}),提交审核失败")
return False
# 非最后一次重试,等待后继续
print(f" 将在 {current_delay} 秒后进行第 {retry + 2} 次重试...")
time.sleep(current_delay)
except Exception as e:
print(f"❌ 第 {retry + 1} 次提交审核异常: {str(e)}")
# 最后一次重试失败则退出
if retry == SUBMIT_MAX_RETRIES - 1:
print(f"\n❌ 已达到最大重试次数 ({SUBMIT_MAX_RETRIES}),提交审核失败")
return False
# 非最后一次重试,等待后继续
print(f" 将在 {current_delay} 秒后进行第 {retry + 2} 次重试...")
time.sleep(current_delay)
return False
def main():
try:
# 1. 生成 JWT 令牌
if not private_key_path.exists():
raise FileNotFoundError(f"私钥文件不存在: {private_key_path}")
token = generate_jwt_token(API_KEY, API_ISSUER, private_key_path)
print("✅ JWT 令牌生成成功")
# 2. 配置请求头
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# 3. 获取应用 ID
print(f"\n🔍 查找应用: {APP_NAME}")
apps_url = f"{API_ENDPOINT}/apps"
response = requests.get(apps_url, headers=headers)
response.raise_for_status()
apps_data = response.json()
app = next((a for a in apps_data["data"] if a["attributes"]["name"] == APP_NAME), None)
if not app:
raise ValueError(f"未找到应用: {APP_NAME}")
app_id = app["id"]
print(f"✅ 找到应用 ID: {app_id}")
# 4. 获取指定版本和构建号的构建版本(带重试)
valid_build = find_specific_build(app_id, headers)
if not valid_build:
raise ValueError(
f"经过 {MAX_RETRIES} 次尝试后,仍未找到可用的构建版本: {TARGET_VERSION} ({TARGET_BUILD_NUMBER})")
build_id = valid_build["id"]
build_version = TARGET_VERSION
build_number = valid_build["attributes"]["version"]
print(f"✅ 找到构建版本 ID: {build_id} (版本号: {build_version}, 构建号: {build_number})")
# 5. 获取外部测试群组 ID
print(f"\n🔍 查找测试群组: {BETA_GROUP_NAME}")
groups_url = f"{API_ENDPOINT}/betaGroups"
response = requests.get(groups_url, headers=headers)
response.raise_for_status()
groups_data = response.json()
beta_group = next((g for g in groups_data["data"] if
g["attributes"]["name"] == BETA_GROUP_NAME and
g["attributes"]["isInternalGroup"] is False), None)
if not beta_group:
raise ValueError(f"未找到外部测试群组: {BETA_GROUP_NAME}")
beta_group_id = beta_group["id"]
print(f"✅ 找到外部测试群组 ID: {beta_group_id}")
# 6. 添加构建到测试群组后,更新测试内容(betaBuildLocalizations
print("\n📝 更新测试版本的本地化信息(whatsNew...")
# 7. 将构建版本添加到测试群组
print(f"\n🔗 将构建 {build_id} 添加到测试群组 {beta_group_id}...")
add_build_url = f"{API_ENDPOINT}/betaGroups/{beta_group_id}/relationships/builds"
add_build_payload = {
"data": [{"id": build_id, "type": "builds"}]
}
add_response = requests.post(add_build_url, headers=headers, json=add_build_payload)
try:
add_response.raise_for_status()
except requests.exceptions.HTTPError as e:
if add_response.status_code == 409:
print("⚠️ 构建已在测试群组中,跳过添加步骤")
else:
print(f"❌ 添加构建到测试群组失败: {e}")
print("错误详情:", add_response.json())
sys.exit(1)
else:
print("✅ 构建已成功添加到测试群组")
# 8. 更新测试内容(whatsNew
get_localizations_url = f"{API_ENDPOINT}/builds/{build_id}/betaBuildLocalizations"
response = requests.get(get_localizations_url, headers=headers)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"❌ 获取本地化信息失败: {e}")
print("错误详情:", response.json())
sys.exit(1)
localizations = response.json().get("data", [])
print("\n📝 更新测试版本说明...")
if localizations:
localization_id = localizations[0]["id"]
update_url = f"{API_ENDPOINT}/betaBuildLocalizations/{localization_id}"
print(f"使用现有本地化信息 ID: {localization_id}")
else:
print("创建新的本地化信息...")
create_url = f"{API_ENDPOINT}/betaBuildLocalizations"
create_payload = {
"data": {
"type": "betaBuildLocalizations",
"attributes": {"locale": "zh-Hans", "whatsNew": ""},
"relationships": {
"build": {"data": {"id": build_id, "type": "builds"}}
}
}
}
create_response = requests.post(create_url, headers=headers, json=create_payload)
create_response.raise_for_status()
localization_id = create_response.json()["data"]["id"]
update_url = f"{API_ENDPOINT}/betaBuildLocalizations/{localization_id}"
now_time_text = get_chinese_date_time()
update_payload = {
"data": {
"id": localization_id,
"type": "betaBuildLocalizations",
"attributes": {
"whatsNew": f"当前版本: {TARGET_BUILD_NUMBER}\n提交时间:{now_time_text}\n自动化提交"
}
}
}
update_response = requests.patch(update_url, headers=headers, json=update_payload)
update_response.raise_for_status()
print("✅ 测试版本说明已更新")
# 9. 提交审核(调用新增的带重试函数)
submit_success = submit_for_review_with_retry(build_id, headers)
if not submit_success:
sys.exit(1)
except Exception as e:
print(e)
exit(1)
if __name__ == "__main__":
main()