资讯动态

iOS上架工具链全解析:从签名打包到上传审核排错

发布时间:2026/10/27 22:07:12 来源:尧图企业网站定制
做 iOS 上架这件事第一次点下“提交审核”之前多数人是懵的证书在哪里生成描述文件为什么会突然“Invalid”Archive 打包出来的 ipa 到底走哪条路上传审核被拒后错误码指向的又是哪一环。真正把 App Store 上架工具摸一遍之后你会发现这件事的本质不是“会用 Xcode 点几下”而是一条由签名、构建、元数据、传输、审核反馈五段串起来的工具链每一段都有官方工具和第三方工具两条路可走选错了就会出现“本地能跑、打包失败”“上传成功、审核卡住”这种典型撕裂。这篇内容面向所有正在或即将把 iOS 应用送上 App Store 的开发者不管你是第一次独立上架还是团队里负责维护发布流水线的那位我都会把每一段的工具选择逻辑、参数含义、踩坑经验摊开讲清楚让你少走几轮来回。1. 一条上架链路上的六个工具位先搞清楚谁管什么1.1 把“上架”拆成五段工具才不会被乱用很多人把上架理解成一个动作其实它是五个动作的组合每一段失败的表现形式完全不同。第一段是签名决定你的包能不能被系统信任第二段是构建决定产物是 ipa 还是 xcarchive第三段是元数据包括截图、描述、关键词、隐私声明第四段是传输把包和元数据送到服务端第五段是审核反馈包括构建处理状态和审核结果。这五段之间不是严格的线性关系构建可能做了三次才成功元数据可能在审核阶段被打回重改所以工具链的设计原则是每一段都能独立重跑且重跑不污染前一段的产物。我见过不少团队的流程是这样的在 Xcode 里点 Archive然后 Organizer 里点 Distribute弹出窗口里选证书导出一个 ipa再用某个图形化工具拖进去上传。这条链路能跑通但它有三个问题一是 Archive 产物没有归档命名规则出问题时不知道要复现哪一个二是证书选择依赖本机钥匙串状态换一台机器就崩三是元数据全靠网页后台手填版本一多就开始记不清哪张截图对应哪个语言。这三个问题的根因都不是“工具不好用”而是没有把每一段的输入输出显式化。所以我建议的做法是先用一张表把五段对应的工具位定下来再决定哪一段手工、哪一段自动化。下面这张表是我自己在项目里常用的分工方式你可以直接照抄再微调阶段官方工具第三方/自动化工具是否建议自动化签名与证书Xcode 自动签名、钥匙串fastlane match、sigh强烈建议构建打包xcodebuild、Archivefastlane gym建议元数据与截图App Store Connect 后台fastlane deliver、snapshot、frameit视版本节奏传输上传Transporter、iTMSTransporterfastlane deliver/pilot、App Store Connect API强烈建议审核反馈App Store Connect、邮件无成熟替代手工为主这张表最关键的一列不是“工具名”而是最后一列。签名和传输是最应该自动化的两段因为它们的失败率最高、手工成本最大、且结果完全确定元数据自动化要看你的发版频率一个季度发一次的应用手填几十分钟完全可以接受一周发三次的就必须上脚本审核反馈这一段目前没有靠谱的自动化替代方案因为审核结论里包含大量需要人判断的语义信息。1.2 官方工具和第三方工具的边界在哪里一个很实际的问题既然 fastlane 能一把梭为什么还要懂 xcodebuild答案是故障定位。fastlane 本质上是官方命令行的封装它把 xcodebuild 的调用参数、输出日志、退出码都包了一层。当 gym 报一个Error DomainIDEProvisioningErrorDomain的时候如果你只会看 fastlane 的日志你就只能去搜关键词但如果你知道它在底下调的是哪条 xcodebuild 命令你可以把那条命令单独拎出来跑一遍加上-verbose错误信息会清晰十倍。同理上传环节的iTMSTransporter也是一个容易被忽略的官方工具它是 Transporter 图形界面背后的实际执行者。很多上传卡在“正在验证包”这一步半小时不动的情况直接用 iTMSTransporter 的命令行模式跑日志会一行一行打出来能立刻看出是网络问题还是包本身的问题。所以我的原则是自动化用封装工具排错回原始命令行。这两套东西必须都会缺一套就会在关键时刻卡死。还有一个边界问题是签名。Xcode 的自动签名在个人开发者账号下非常好用它会自动帮你创建证书、App ID、描述文件甚至自动处理推送和关联域这类 capability。但它在两种情况下会失效一是团队多人协作证书被覆盖二是账号权限受限比如你只有 App Manager 角色但没有 Admin 角色自动签名会卡在创建描述文件这一步。这两种情况都必须切到手动或 match后面第 2 章会细讲。2. 签名与描述文件自动签名、手动配置和 match 各自适合谁2.1 自动签名到底“自动”了什么打开 Xcode 的 Signing Capabilities 面板勾上 Automatically manage signing选一个 Team剩下的它全包了。这个过程里它实际做了四件事在你的开发者账号下查找或创建一张分发证书创建或更新一个 App ID并把你在 Capabilities 里勾选的推送、关联域、应用内购买等能力写进去生成一份 App Store 分发类型的描述文件把证书和 App ID 绑在一起把描述文件下载到本机的~/Library/MobileDevice/Provisioning Profiles/目录。理解了这四件事很多“莫名其妙”的问题就有解释了。比如你换了台机器自动签名重新生成了一张证书开发者账号下的分发证书数量是有上限的达到上限后它会把最旧的那张吊销于是原来那台机器上的包就再也无法用旧证书重签。再比如你加了一个新的 capability自动签名会去更新描述文件但如果这个 capability 需要账号管理员在后台额外开通比如某些需要申请的能力自动签名就会报错。所以自动签名适合的场景非常明确单人开发、账号权限完整、不需要在多个环境重复构建同一个版本。2.2 手动配置的三件套证书、App ID、描述文件手动配置的核心是把三个文件的关系理清楚这三者的关系是描述文件 一张分发证书 一个 App ID 一组设备分发类型下设备列表为空。任何一处对不上签名就会失败。这三件套对应的常见报错可以对照下面这张表报错关键词实际含义处理方向No signing certificate found本机钥匙串里没有与描述文件匹配的私钥导入含私钥的 p12而不是只有 cerProvisioning profile doesnt include signing certificate描述文件绑定的证书与你当前选的证书不是同一张重新生成描述文件或换证书doesnt match any valid, non-expired certificate/private key pair证书过期或私钥缺失检查证书有效期与钥匙串配对状态Bundle identifier is not availableApp ID 已被占用或拼写不符核对 Bundle ID 大小写与连字符这里有一个特别容易被忽略的细节导出的 .cer 文件只有公钥没有私钥。证书本身不包含私钥私钥在你生成证书请求的那台机器的钥匙串里。所以团队协作时导出给别人的必须是 .p12含私钥并设密码而不是 .cer。我见过不止一次有人把 .cer 发给同事同事导入后 Xcode 显示证书存在但一签名就报“no private key”来回折腾半天。2.3 match 把证书变成可以版本管理的资产手动配置的下一个问题是三件套怎么在团队和 CI 之间同步最原始的办法是压缩包传来传去密码写在文档里一旦有人离职或者密码改了全线崩。fastlane 的 match 解决的正是这个问题它的思路很直接把证书和描述文件加密后存到一个私有的 Git 仓库里所有人包括 CI 都从这个仓库拉取。初始化只需要一条命令fastlane match init它会问你仓库地址然后生成一个Matchfile。之后执行# 首次生成或更新全部证书与描述文件 fastlane match appstore # 只读模式CI 上必须用这个防止 CI 误创建新证书 fastlane match appstore --readonly # 指定某个 bundle id fastlane match appstore -a com.example.app--readonly这个参数是重中之重。CI 环境的账号权限通常是受限的如果不加只读模式某次构建发现本地没有证书match 就会尝试去创建一个新的结果就是又吊销了一张旧证书其他环境全线挂掉。我在一个项目里就因为这个参数漏加导致测试同事的包全部失效重新配置花了整整一个下午。match 还有一个隐含价值它会把你的签名配置变成文本可审计的。仓库里的文件是加密的但Matchfile和调用脚本是可以走代码评审的谁改了签名方式、加了哪个 capability在合并请求里一目了然。这一点比“某个人在本机 Xcode 里点了几下”要可靠得多。注意match 仓库的加密密码不要和仓库地址放在同一个地方。密码进 CI 的密钥管理仓库地址可以写在公开配置里。3. 打包环节xcodebuild 与 gym 的参数账3.1 Archive 和 Export 是两个动作不是一个Xcode 图形界面里 Archive 之后点 Distribute看起来是一个连续动作实际背后是两条命令xcodebuild archive生成.xcarchivexcodebuild -exportArchive从 xcarchive 里导出 ipa。拆开这两步的价值在于xcarchive 是可以复用的。你打一次包可以导出一份 App Store 的 ipa同时导出一份 Ad Hoc 的 ipa 给测试不用重新编译。对于编译要十几分钟的中大型项目这个差别非常明显。标准的 Archive 命令大致是这样xcodebuild archive \ -workspace MyApp.xcworkspace \ -scheme MyApp \ -configuration Release \ -archivePath ./build/MyApp.xcarchive \ -destination generic/platformiOS \ | xcpretty这里有几个参数值得单独说。-workspace用于有 CocoaPods 或需要多 target 联编的项目纯 Swift Package 项目可以用-project。-destination generic/platformiOS表示不针对模拟器也不针对某台真机只针对 iOS 通用设备这是打包必须的如果误写成模拟器 destinationArchive 会直接失败。-configuration Release不用多说但要注意如果你的工程里 Release 配置的优化等级或编译条件有特殊设置打包前一定要确认因为 Debug 和 Release 在真机上表现不一致的 bug 太常见了。导出的命令是这样xcodebuild -exportArchive \ -archivePath ./build/MyApp.xcarchive \ -exportOptionsPlist ./ExportOptions.plist \ -exportPath ./build/output \ -allowProvisioningUpdates-allowProvisioningUpdates这个参数在新版 Xcode 里几乎是必需的否则在 CI 上导出时会因为无法访问开发者账号而失败。它的作用是允许命令行工具去更新描述文件配合 match 的只读模式使用时要小心两者结合的效果是“能拉取但不新建”这个组合是我目前认为最稳的。3.2 ExportOptions.plist 里真正影响结果的字段ExportOptions.plist是导出环节的控制中心很多人是随手复制一份别人的文件然后用但里面几个字段如果写错会直接导致上传阶段被拒或者证书不匹配。下面这几个字段必须自己核对keymethod/key stringapp-store-connect/string keyteamID/key stringABCDE12345/string keysigningStyle/key stringmanual/string keyprovisioningProfiles/key dict keycom.example.app/key stringMyApp AppStore Profile/string /dict keyuploadSymbols/key true/ keystripSwiftSymbols/key true/method字段在不同版本的工具链里取值命名有过调整早期是app-store后来区分出更细的上传用途具体取值建议以你当前 Xcode 版本导出的模板为准最保险的方式是先在图形界面里人为导出一次把生成的 plist 留下来当模板再改成脚本用。teamID必须和证书所属团队一致团队账号和公司账号混用的情况特别容易在这里出错。signingStyle设为manual时provisioningProfiles就必须逐个 bundle id 写清楚多 target 项目比如主 App 加几个 Extension要全部列出来少一个就会导出失败。uploadSymbols打开是为了把 dSYM 一起传上去后面崩溃日志符号化要靠它如果关掉线上崩溃堆栈就全是十六进制地址。3.3 gym 的封装与反封装什么时候该放弃 gymgym 是 xcodebuild 的封装它的好处是把 Archive 和 Export 合并成一个 action参数用 Ruby 的键值对写可读性好很多gym( workspace: MyApp.xcworkspace, scheme: MyApp, configuration: Release, export_method: app-store-connect, output_directory: ./build, output_name: MyApp.ipa, include_symbols: true, clean: true )这段配置和前面两条 xcodebuild 命令做的事情完全一样。gym 的优点是它能和 match、deliver 共享同一份Appfilebundle id 和 team id 不用重复写缺点是它把中间产物藏起来了.xcarchive在哪里、导出日志在哪、失败时是哪一条命令报的错都需要翻 fastlane 的 log 目录。所以我的实际用法是日常发版用 gym一旦出问题就关掉 gym用 xcodebuild 原生命令重跑一遍。这里有一条经验值得记下来gym 报错时log 里会有一行Exit status of command xcodebuild ... was 65把这行后面的完整命令复制出来去掉输出重定向直接在你本机终端跑。这一步能把“封装层丢失的信息”全部还原绝大多数签名类、Provisioning 类的错误用原生命令跑一次就能看到 Xcode 自己给出的建议修复项。这个习惯帮我节省了大量在论坛里翻帖子的时间。4. 截图与元数据deliver、snapshot 能省下的重复劳动4.1 元数据不是一张表是一棵目录树App Store Connect 后台的元数据看起来是一堆输入框但 fastlane 把它们映射成了一个固定结构的目录树fastlane deliver init会在项目里生成fastlane/metadata/结构大致是这样metadata/ en-US/ description.txt keywords.txt promotional_text.txt release_notes.txt support_url.txt marketing_url.txt privacy_url.txt name.txt subtitle.txt zh-Hans/ ... review_information/ first_name.txt email_address.txt notes.txt这个结构的价值不在于“少点几次网页”而在于它进了版本库。每一次文案改动都有 diff谁改了关键词、哪个版本的更新说明写了什么全都可以追溯。审核被拒时最常见的场景是“审核员认为描述与实际功能不符”这时候翻出上一个版本的description.txt对比一下就知道是这次改文案引入的问题还是原来就有的表述。review_information这个目录特别值得说它是给审核团队看的备注。很多人不填觉得浪费时间但实操中这一块填得好能省掉一次完整的往返。演示账号、是否需要登录、哪些功能依赖特定网络环境、哪些页面的入口在哪这些写清楚审核员就不用发消息问你。我在一个需要登录才能看到核心功能的应用上因为备注里写清了测试账号和推荐的操作路径审核一次通过同一团队的另一个应用没写被问了一轮“如何访问完整功能”来回三天。4.2 截图自动化的两个瓶颈设备矩阵与本地化snapshot 的作用是自动在多个模拟器上跑 UI 测试、在每个关键页面截图、自动加上状态栏的美化。它的原理其实很朴素启动模拟器、跑一个 UI Test target、在指定位置调用snapshot()截图然后把图按语言和设备尺寸归档。snapshot( devices: [iPhone 16 Pro Max, iPhone 16, iPad Pro 13-inch (M4)], languages: [en-US, zh-Hans], output_directory: ./screenshots, clear_previous_screenshots: true, reinstall_app: true, erase_simulator: true )这里第一个瓶颈是设备矩阵。App Store 对截图尺寸有明确要求而你不需要为每一种尺寸都真跑一遍通常选三到四档覆盖主流尺寸即可剩下的交给系统缩放。跑太多设备只会让流水线时间翻倍收益很低。第二个瓶颈是本地化。语言数量乘以设备数量乘以页面数量是截图任务的实际数量。四个语言、三台设备、五个页面就是六十张图跑一次可能二十分钟。我的做法是把截图任务从每次提交里拆出来只在 UI 有明显变化或新增语言时跑其他版本复用上一次的截图。还有一个隐藏坑点截图里的内容必须是真实可展示的。如果 UI 测试用的是假数据填充截出来的图里出现了“[Mock]”“test123”这类字样审核是会被拒的。所以 UI 测试的数据源要么走一个专门的演示环境要么在截图前用固定的种子数据。我见过因为截图里露出内部测试水印被打回的案例非常不值。4.3 隐私声明与审核备注的版本化管理近两年审核对隐私的要求越来越细PrivacyInfo.xcprivacy这类隐私清单文件已经成了必填项第三方 SDK 也需要各自带上自己的清单并完成签名。这块内容放在元数据章节讲是因为它实际上有“代码侧”和“申报侧”两份代码侧是打包进 ipa 的清单文件申报侧是 App Store Connect 里填写的隐私问卷。两份必须一致不一致是常见的拒审原因之一。我的做法是在项目里维护一份privacy-checklist.md列出每个第三方 SDK 的名称、版本、收集的数据类型、是否用于追踪、对应的清单文件路径。每次升级 SDK 都要过一遍这张表同时更新后台问卷。这份表看起来是额外工作但当审核问起“你们是否使用了某类数据用于广告归因”时能在五分钟内给出准确答案比临时翻代码强太多。5. 传输与上传Transporter、iTMSTransporter 与 API 通道5.1 三条上传通道的差异和使用场景把 ipa 送到服务端这件事目前有三条路图形化的 Transporter 应用、命令行工具 iTMSTransporter、以及基于 App Store Connect API 的上传方式fastlane 的 deliver/pilot 底层走的就是 API。三者的差异不在功能而在可观测性和可自动化程度。Transporter 是图形界面拖进去点“交付”进度条走完就结束。它最大的优点是日志相对友好失败时会给出明确的错误描述最大的缺点是完全无法自动化而且它只能上传已经导出的 ipa不能上传元数据。iTMSTransporter 是它的命令行版本用xcrun iTMSTransporter调用可以写进脚本日志输出比图形界面更细。App Store Connect API 这条路是最现代的用密钥对认证不依赖账号密码和双重验证非常适合 CI。通道认证方式可自动化适合场景Transporter 图形界面账号登录否临时手动补传、排查问题iTMSTransporter账号或 API Key是脚本化上传日志详细App Store Connect APIAPI Keyp8是CI 流水线首选这里要提醒一句早期那套基于账号密码的上传命令已经逐步退出历史舞台如果还在脚本里用老式写法很可能某天突然失效。迁移到 API Key 是迟早的事早迁早省心。5.2 App Store Connect API Key 的权限设计API Key 在后台生成会给你三样东西Key ID、Issuer ID、以及一个只能下载一次的.p8私钥文件。这三样东西的组合就是认证凭据。fastlane 里配置大致是这样app_store_connect_api_key( key_id: ENV[ASC_KEY_ID], issuer_id: ENV[ASC_ISSUER_ID], key_filepath: ENV[ASC_KEY_PATH], duration: 1200, in_house: false )权限设计上有一个原则按用途分开建 Key。上传构建、管理 TestFlight 测试员、读取销售报表这三类操作的权限范围不一样如果共用一个 Key一旦泄露影响面就大。后台可以给每个 Key 指定角色把上传用的 Key 限制在最小的必要权限上。另外.p8文件下载后无法再次获取一定要放进密钥管理系统不要提交到代码仓库也不要以明文形式出现在任何日志里。duration参数是令牌有效期单位是秒默认值通常够用。如果你的流水线里上传大包耗时较长可以适当调大但不要设得过长令牌失效时的重试逻辑也要有否则一个长任务跑到一半令牌过期会得到一个很隐晦的认证错误。5.3 上传失败的常见报错读法上传阶段的报错往往以ITMS-开头配一段英文描述。这些错误的排查有一个通用套路先判断是包本身的问题还是服务端状态的问题。判断方法很简单同一个包隔几小时重传一次如果还是同样报错就是包的问题如果变成成功就是服务端处理队列的问题。包本身的问题里最常见的是版本号冲突和签名不匹配。版本号分两个字段CFBundleShortVersionString是对外的版本号比如 2.3.0CFBundleVersion是构建号同一版本下每次上传必须递增。很多人只改前者不改后者就会收到“已存在相同版本”的报错。构建号的递增规则建议直接用流水线序号或者时间戳不要手工维护手工维护必出错。签名不匹配的表现是上传成功但处理阶段失败提示找不到匹配的签名证书或者描述文件类型不对。这种情况要回头检查导出的 ipa 到底用的是哪种签名方式有时候本地 Xcode 缓存的描述文件和实际使用的不一致最保险的做法是用codesign -dv --verbose4 MyApp.app直接看包里的签名信息比猜要快。6. 拒审与构建失败一套可复现的排查链路6.1 先分清“构建处理失败”和“审核被拒”这两个是完全不同的东西但经常被混为一谈。构建处理失败发生在包上传之后、进入审核队列之前App Store Connect 的构建列表里会显示一个感叹号点进去有具体的 ITMS 错误码。审核被拒发生在人工审核之后会给你一份 Resolution Center 的说明里面引用具体的审核条款编号。前者是技术问题后者可能是技术问题也可能是内容、元数据、隐私申报的问题。我把排查顺序固定成四步每次都按这个走看构建状态。构建列表里如果这个版本还没变成“可供提交”说明卡在处理阶段先解决技术问题别急着提交。读错误码原文。ITMS 错误码的描述往往是精确的比如“缺少某个用途描述字符串”说明你的 Info.plist 里少了一个 key。本地复现。用xcodebuild原生命令把整个打包流程跑一次很多时候本地就能看到警告。对照条款。如果是审核拒绝先找到条款编号再看 Resolution Center 里审核员写的具体句子别凭感觉改。这套顺序的价值在于避免“改一堆东西然后重新提交”的盲目操作。盲目改的后果是即使通过了你也不知道是哪一处改动起了作用下次遇到同类问题还是要重来一遍。6.2 一张按现象索引的排查表下面这张表是我这几年积累下来的按“你看到的现象”索引而不是按错误码索引因为很多人在拿到错误码之前只知道现象现象最可能的原因首选排查动作上传后一直“正在处理”服务端队列积压或包内含超大资源等两小时再传一次检查资源体积构建显示“无效二进制”签名类型不匹配、缺少必需架构codesign -dv看签名lipo -info看架构审核拒绝引用隐私条款隐私问卷与实际采集行为不符对照三方 SDK 清单逐项核对审核拒绝称功能不完整演示账号失效或关键入口隐藏重置演示账号补全审核备注审核拒绝称内容重复应用与已有应用同质化突出差异化功能调整描述与截图TestFlight 安装失败设备未加入测试组或构建未通过处理检查测试组分配与构建状态表格里“功能不完整”这一条特别值得展开。很多应用的核心功能需要登录才能看到而审核员拿到的演示账号如果过期、或者权限不足看到的就是一个空壳。审核员不会替你去猜直接按“功能不完整”拒。解决方式是在审核备注里写清楚完整路径并保证演示账号在整个审核期间有效。我现在的做法是给审核专门准备一个长期有效的演示账号密码写在备注里且这个账号的数据是固定种子不随真实用户数据变化。6.3 隐私清单和第三方 SDK 签名这两年的新门槛这两项是近两年新增的硬性要求也是“以前能过、现在过不了”的主要来源。隐私清单要求应用和第三方 SDK 各自声明数据收集类型、是否用于追踪、以及使用某些系统 API 的理由。第三方 SDK 还需要提供带签名的二进制未签名的 SDK 会导致整个包在处理阶段失败。排查这类问题的方法是从包的结构入手把 ipa 解开看Frameworks目录里有哪些 framework逐个确认是否带签名、是否带隐私清单# 查看已签名信息 codesign -dvvv MyApp.app/Frameworks/SomeSDK.framework # 列出包内框架 ls MyApp.app/Frameworks/如果某个 SDK 既没有签名也没有清单唯一的办法是升级到该 SDK 提供了合规版本的新版本号或者换掉它。这一步没有捷径因为它是二进制层面的要求改配置绕不过去。我的建议是把“三方 SDK 合规检查”加进发版前的检查清单每次升级依赖都过一遍别等到上传之后才发现。7. 把上架流程放进流水线从手动点到一键出包7.1 流水线拆解与密钥管理把上架自动化本质上就是把前面五段串成一条命令能跑完的链路。一个典型的分段是拉代码、装依赖、跑测试、match 拉取签名、gym 打包、deliver 上传、上传 dSYM。每一段都应该能单独触发用于定位问题。steps: - run: bundle install - run: bundle exec fastlane match appstore --readonly - run: bundle exec fastlane build - run: bundle exec fastlane upload密钥管理是这条链路里最容易被轻视的环节。需要放进密钥系统的东西至少包括match 仓库地址与解密密码、App Store Connect API Key 的三件套、以及可能的通知 webhook 凭据。原则是流水线里不出现任何明文密钥全部通过环境变量注入且这些变量的读取权限限制在需要的步骤上。另外构建日志要过滤敏感输出fastlane 的某些步骤在调试模式下会把环境变量打出来这一点要提前关掉。7.2 构建号的唯一性与自增策略构建号冲突是自动化流水线最常见的失败原因。手工维护的构建号在多分支并行时必然撞车。我的建议是用一个单调递增的来源比如流水线的全局计数器或者基于时间生成。fastlane 提供了现成的 actionincrement_build_number( build_number: latest_testflight_build_number 1 )这个写法会去查询 TestFlight 上最新的构建号再加一好处是绝对不会撞车坏处是它依赖网络请求如果查询失败整个流程就断。所以更稳的做法是本地维护一个计数文件或者直接用 CI 的系统变量。我在项目里用的是“主版本号来自代码构建号来自 CI 序列号”的混合方案代码里的CFBundleShortVersionString由人在合并请求里改CFBundleVersion完全由流水线写入两边职责清晰不会互相干扰。7.3 TestFlight 的分发策略与灰度节奏上传成功不等于可以让用户用了中间还隔着 TestFlight 这一层。TestFlight 的价值在于它是审核之前的一次真实环境验证。内部测试组不需要审核就能装外部测试组需要一次简化的审核但比正式上架快很多。我的常规节奏是构建上传后先进内部组自己装一遍走核心路径确认没有问题再提交外部测试最后才是提交正式审核。这个节奏的意义在于把问题挡在最便宜的地方。内部测试发现崩溃你只需要重新传一个包外部测试发现崩溃你需要处理测试员的反馈正式审核被拒你要重新排队。三者的成本差得很远。另外 TestFlight 上的构建是有有效期的测试组的成员也要维护如果长期不用记得清理避免堆积一堆无效测试员影响后续分发。7.4 我在这条链路上踩过的几个坑第一个坑是描述文件的缓存。本机 Xcode 缓存了旧的描述文件脚本跑的时候用的却是新的两边不一致导致导出的包签名异常。解决办法是在打包前主动清理~/Library/MobileDevice/Provisioning Profiles/下的旧文件让 match 重新拉一遍。这个动作在 CI 上是必须的因为 CI 的缓存有时候比你想的更顽固。第二个坑是多 target 的签名配置。主 App 加几个 Extension 的项目ExportOptions.plist里少写一个 bundle id导出就会失败而且错误提示不会明确告诉你是哪一个缺失。我的做法是把所有 bundle id 写进一个配置文件脚本里遍历生成 plist从源头上避免漏写。第三个坑是元数据上传覆盖了人工修改。deliver 默认会覆盖后台的所有字段如果某次有人在网页上紧急改了一句描述而流水线又跑了一次这句改动就没了。解决办法是明确规则所有元数据改动必须走代码仓库网页上的修改只作为临时应急且要立刻回写到仓库。这条规则听起来是流程问题但它是自动化能否长期维持的关键。第四个坑是截图的语言目录缺失。deliver 上传时如果某个语言目录下没有截图它不会报错而是保留后台原有截图于是出现了“描述是新的、截图是旧的”这种奇怪状态。解决办法是在上传前做一次目录校验缺哪个语言的截图直接让流水线失败逼着补齐。最后分享一个小习惯每次发版之后把当次的构建号、提交哈希、上传时间、审核结果记进一个简单的表格里。看起来是多余的动作但当你在三个月后需要回溯“哪个版本引入了某个线上问题”时这张表能让你在几分钟内定位到具体的包和对应的代码状态比在聊天记录里翻要快得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑