382 lines
14 KiB
Python
382 lines
14 KiB
Python
import jwt
|
||
import time
|
||
import requests
|
||
import os
|
||
import sys
|
||
import datetime
|
||
from pathlib import Path
|
||
|
||
|
||
def get_chinese_date_time():
|
||
"""
|
||
获取当前时间,以中文格式返回(如:8月5日 上午8点24分)
|
||
包含月、日、上午/下午、时、分,不包含秒数
|
||
"""
|
||
# 获取当前时间
|
||
now = datetime.datetime.now()
|
||
|
||
# 获取月份和日期
|
||
month = now.month
|
||
day = now.day
|
||
|
||
# 获取24小时制小时数和分钟数
|
||
hour_24 = now.hour
|
||
minute = now.minute
|
||
|
||
# 确定上午/下午
|
||
if hour_24 < 12:
|
||
period = "上午"
|
||
# 转换为12小时制(0点特殊处理为12点)
|
||
hour_12 = hour_24 if hour_24 != 0 else 12
|
||
else:
|
||
period = "下午"
|
||
# 转换为12小时制(12点保持12点,13点及以后减12)
|
||
hour_12 = hour_24 if hour_24 == 12 else hour_24 - 12
|
||
|
||
# 格式化返回字符串
|
||
return f"{month}月{day}日 {period}{hour_12}点{minute}分"
|
||
|
||
|
||
# 获取参数
|
||
# 处理第一个参数(环境),默认值为'develop'
|
||
if len(sys.argv) >= 2 and sys.argv[1]:
|
||
env_name = sys.argv[1]
|
||
else:
|
||
env_name = 'develop'
|
||
|
||
# 处理第二个参数(构建号),默认值为'2'
|
||
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 ['develop', 'sit', 'uat']:
|
||
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"
|
||
|
||
|
||
|
||
MAX_RETRIES = 10 # 最大重试次数
|
||
RETRY_DELAY = 30 # 重试间隔(秒)
|
||
if 'PYCHARM_HOSTED' in os.environ: # 说明是pycharm
|
||
Auth_Key_Path = './'
|
||
else:
|
||
Auth_Key_Path = '/Users/jlbank/Desktop/lx_uniapp/auto_build' # p8私钥文件位置
|
||
|
||
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()
|
||
|
||
# 令牌有效期 20 分钟
|
||
current_time = int(time.time())
|
||
payload = {
|
||
"iss": api_issuer,
|
||
"exp": current_time + 20 * 60,
|
||
"iat": current_time,
|
||
"aud": "appstoreconnect-v1"
|
||
}
|
||
|
||
# 生成 JWT 令牌
|
||
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})")
|
||
|
||
# 1. 先获取目标版本对应的预发布版本ID
|
||
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}")
|
||
|
||
# 2. 使用预发布版本ID筛选构建版本(修正参数错误)
|
||
builds_url = f"{API_ENDPOINT}/builds"
|
||
params = {
|
||
"filter[preReleaseVersion]": pre_release_id,
|
||
"sort": "-version",
|
||
"limit": "10",
|
||
# 移除了无效的ciBuildGroup关联
|
||
"include": "buildBetaDetail,betaBuildLocalizations,preReleaseVersion,betaAppReviewSubmission,icons,buildBundles,betaGroups",
|
||
# 移除了无效的ciBuildGroups字段
|
||
"limit[betaBuildLocalizations]": "40",
|
||
# 将betaGroups的limit调整为最大值50
|
||
"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的构建
|
||
valid_build = next((
|
||
b for b in matching_builds
|
||
if b["attributes"]["processingState"] == "VALID"
|
||
), None)
|
||
|
||
if valid_build:
|
||
return valid_build
|
||
else:
|
||
# 显示找到的构建但状态不是VALID
|
||
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 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:
|
||
# 处理已存在的情况(409 Conflict 是正常现象,说明已添加)
|
||
if add_response.status_code == 409:
|
||
print("⚠️ 构建已在测试群组中,跳过添加步骤")
|
||
else:
|
||
print(f"❌ 添加构建到测试群组失败: {e}")
|
||
print("错误详情:", add_response.json())
|
||
sys.exit(1)
|
||
else:
|
||
print("✅ 构建已成功添加到测试群组")
|
||
# 8. 更新测试内容(whatsNew)
|
||
# 先获取该构建的本地化信息 ID(如果已有)
|
||
# 构建 ID(build_id 是你要操作的构建版本 ID)
|
||
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. 提交审核(触发测试状态)- 修复API端点
|
||
print("\n📋 提交构建版本审核...")
|
||
|
||
# 正确的提交审核API端点
|
||
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)
|
||
try:
|
||
submit_response.raise_for_status()
|
||
except requests.exceptions.HTTPError as e:
|
||
# 处理已提交的情况(409冲突)
|
||
if submit_response.status_code == 409:
|
||
print("⚠️ 构建已提交审核,无需重复操作")
|
||
else:
|
||
print(f"❌ 提交审核失败: {e}")
|
||
print("错误详情:", submit_response.json())
|
||
sys.exit(1)
|
||
else:
|
||
print("✅ 构建版本已成功提交审核")
|
||
|
||
|
||
except Exception as e:
|
||
print(e)
|
||
exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
|
||
main() |