在 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
:
1 | /Applications/Transporter .app /Contents/itms/bin/iTMSTransporter |
然后配置环境变量以便可全局访问到该命令。编辑 .bash_profile
文件,添加以下命令内容:
1 2 | export TRANSPORTER_HOME= /Applications/Transporter .app /Contents/itms export PATH=${PATH}:${TRANSPORTER_HOME} /bin |
最后,可任意位置打开终端,执行如下命令测试是否安装成功:
1 | iTMSTransporter -version |
Transporter 支持多系统。关于 Windows 和 Linux 系统上的安装,可以参考该文档:安装 Transporter
也可以直接下载安装包安装:
2 准备工作
2.1 生成 App 专用密码
打开该地址并登录:https://appleid.apple.com,然后在 App 专用密码
处创建一个用于上传专用的密码。该密码只显示一次,注意复制并及时保存。若遗忘了可移除重新创建。
2.2 查询团队简称 shortName
执行如下命令以获取开发团队 Short Name
的值:
1 | iTMSTransporter -m provider -u <username> -p <App专用密码> |
3 使用 iTMSTransporter 命令行上传 IPA
3.1 命令行上传
上传的命令格式参考:
1 2 3 4 5 6 | # 基本格式 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 文件之后生成。生成命令参考:
1 | xcrun swinfo -o AppStoreInfo.plist -prettyprint true --plistFormat binary -f xxx.ipa |
3.2 一个基于 Node.js 的上传函数封装
用于 CLI 程序中的函数参考,基于 Node.js 和 TypeScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | /** * transporter 上传相关的参数 */ 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
日志报错如下:
1 2 3 4 5 6 7 8 9 | 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
参数设置后报错
主要日志报错如下:
1 | ERROR: The username xx@lzw.me is not a member of the provider xxxx. Contact you team admin for assistance.(1296) |
由于参考了其他文章,将其取值设置为了团队ID。当前此处应当设置为团队 Short Name
简称。获取方法为:
1 | iTMSTransporter -m provider -u <username> -p <App专用密码> |