在 iOS 开发过程中,经常需要将 ipa 文件上传至 App Store Connect 以供提交 testflight 交付测试。在 CI 持续集成过程中,我们可以使用 Transporter 工具来自动化上传 ipa 文件的过程。
Transporter 是 Apple 推出的基于 Java 的命令行工具,用于进行大批量交付。你可以使用 Transporter 将内容的 Store 数据包交付至 Apple TV、iTunes Store、Apple Books 和 App Store。
1 安装和配置 Transporter
Mac OS 电脑可以从 App Store 中搜索 Transporter
并安装。通过该方式安装后可从如下位置访问命令行工具 iTMSTransporter
:
/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter
然后配置环境变量以便可全局访问到该命令。编辑 .bash_profile
文件,添加以下命令内容:
export TRANSPORTER_HOME=/Applications/Transporter.app/Contents/itms export PATH=${PATH}:${TRANSPORTER_HOME}/bin
最后,可任意位置打开终端,执行如下命令测试是否安装成功:
iTMSTransporter -version
Transporter 支持多系统。关于 Windows 和 Linux 系统上的安装,可以参考该文档:安装 Transporter
也可以直接下载安装包安装:
2 准备工作
2.1 生成 App 专用密码
打开该地址并登录:https://appleid.apple.com,然后在 App 专用密码
处创建一个用于上传专用的密码。该密码只显示一次,注意复制并及时保存。若遗忘了可移除重新创建。
2.2 查询团队简称 shortName
执行如下命令以获取开发团队 Short Name
的值:
iTMSTransporter -m provider -u <username> -p <App专用密码>
3 使用 iTMSTransporter 命令行上传 IPA
3.1 命令行上传
上传的命令格式参考:
# 基本格式 iTMSTransporter -m upload -u <username> -p <App专用密码> -assetFile <ipa路径> -asc_provider <团队ShortName> -v informational # 推荐方式示例: export APPLEID_PASSWORD=xxx-xxx-xxx-xxx iTMSTransporter -m upload -u xx@lzw.me -p @env:APPLEID_PASSWORD -assetFile lzw.me.ipa -asc_provider LZWME -v informational
各参数说明参考:
-m
指定执行模式,此处固定为upload
-u
指定 Apple ID 用户名,应是邮箱格式-p
指定 APP 专用密码。为避免 CI 流程中打印到日志而泄露密码,可以设置到环境变量$APPLEID_PASSWORD
进行赋值,然后此处固定为@env:APPLEID_PASSWORD
-assetFile
指定要上传的 IPA 文件路径-asc_provider
指定所属团队的Short Name
-v
指定日志级别。可选值为:off | informational | critical | detailed | eXtreme
如果你因为公司网络安全管理的限制,CI 流程中禁止访问外部网络而不能直接上传,则需要人工介入,下载产物后手工上传。此时可能会使用 Windows 或 Linux 系统。
需要注意,使用非 Mac OS
系统上传,还需要指定 -assetDescription <AppStoreInfo.plist>
参数。AppStoreInfo.plist
文件可以在产出 ipa 文件之后生成。生成命令参考:
xcrun swinfo -o AppStoreInfo.plist -prettyprint true --plistFormat binary -f xxx.ipa
3.2 一个基于 Node.js 的上传函数封装
用于 CLI 程序中的函数参考,基于 Node.js 和 TypeScript:
/** * transporter 上传相关的参数 * @see https://help.apple.com/itc/transporteruserguide/zh_CN.lproj/static.html */ export type ITransporter = Record<string, string> & { /** 指定 ipa 文件 */ assetFile?: string; /** appleid 账号 */ u?: string; /** App 专用密码。设置到环境变量 APPLE_APP_PWD */ p?: string; /** 日志级别 */ v?: 'off' | 'critical' | 'informational' | 'detailed' | 'eXtreme'; /** 用于记录输出信息的目录和文件名,包括时间戳 */ o?: string; /** 团队简称 */ asc_provider?: string; }; export function uploadTransporter(args: ITransporter = {}) { if (!args.assetFile || !args.assetFile.endsWith('.ipa') || !existsSync(args.assetFile)) { logger.error(`[uploadTransporter] 请传入正确的 ipa 文件`, args.assetFile); return false; } const dirs = [ '/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter', '/Applications/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter', '/usr/local/itms/bin', ]; const transporterPath = dirs.find(dir => existsSync(dir)); if (!transporterPath) { logger.error('[uploadTransporter] 未找到上传工具 iTMSTransporter,请通过 App Store 安装 Transporter 应用!'); return false; } const mbdpConfig = getMbdpConfig(); args = { m: 'upload', assetFile: args.assetFile, ...mbdpConfig.ios.transporter!, ...args, }; if (!args.p || !args.u) { logger.warn(`[uploadTransporter] 请指定 u 和 p 参数`); return false; } const params = Object.entries(args).map(([k, v]) => { if (k === 'p') { process.env.APPLE_APP_PWD = v; return `-p @env:APPLE_APP_PWD`; } else return `-${k} "${v}"`; }); const cmd = [transporterPath, ...params].join(' '); logger.info('开始上传 ipa 至 App Store Connect'); execSync(cmd); return true; }
4 遇到的问题及解决参考
4.1 问题:认证失败返回 401
日志报错如下:
Error Messages: could not find a provider public id: actorsGetCollection call failed with: 401 - { "errors": [{ "status": "401", "code": "NOT_AUTHORIZED", "title": "Authentication credentials are missing or invalid.", "detail": "Provide a properly configured and signed bearer token, and make sure that it has not expired. Learn more about Generating Tokens for API Requests https://developer.apple.com/go/?id=api-generating-tokens" }] }
设置 -asc_provider
参数即可。该参数默认是可选的,但当自己的账号加入了多个团队时,必须指定该参数。
4.2 问题:-asc_provider
参数设置后报错
主要日志报错如下:
ERROR: The username xx@lzw.me is not a member of the provider xxxx. Contact you team admin for assistance.(1296)
由于参考了其他文章,将其取值设置为了团队ID。当前此处应当设置为团队 Short Name
简称。获取方法为:
iTMSTransporter -m provider -u <username> -p <App专用密码>