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()