App Store 上架在不少团队里仍是「PM 催、iOS 负责人手动 Archive、半夜传 TestFlight、第二天再填一遍元数据」——功能早 merge 了,真正卡住的是没有工程化的 Release 链路。本文从 CI/CD 视角把从 Xcode 到审核通过拆成六个可观测阶段,给出签名、构建、上传、提审与发布的边界,并附可直接改造的 Runbook。若你正纠结构建节点放哪,可先读 iOS 流水线新玩法 里关于 Mac 构建岛的分层思路。
读完你会得到:一张六阶段上架链路图、手工 vs 自动化对照表、fastlane + GitHub Actions 最小 YAML、以及拒审后 30 分钟内再出 TestFlight 的检查清单。
为什么要把「上架」当成工程链路,而不是 checklist
新手常把上架理解成一张 checklist:截图、隐私政策、点提交。问题在于——checklist 不记录状态,也不保证第 N 次发版与第 1 次用同一套前提。工程化视角下,上架是一条有向流水线,每个阶段产出明确制品,下一阶段只消费上一阶段的输出:
- 可重复:同一 git tag 在构建岛上应打出与本地一致的 IPA(给定相同 Xcode 与依赖锁)
- 可观测:每个阶段有日志、耗时、失败码;Slack 里应看到「upload 成功 / 处理中 / 可提审」而非「好像传了」
- 可回滚:商店侧保留上一版 build;git 侧保留 tag 与 dSYM,崩溃符号化不断档
- 职责分离:研发管二进制与版本号;运营/产品管商店文案与截图——CI 用不同 Job 或审批闸门隔开
没有 Mac 的跨平台团队同样适用这条链路:编码在 Windows / Linux,Release 阶段集中到 macOS 构建节点即可,分工模式见 Windows 主力 + 远程 Mac 构建岛。
六阶段:从 Xcode Scheme 到 App Store 可售
下面这张表是全文骨架。后文 Runbook 按此顺序编排 Job,避免「先传了包才发现版本号没 bump」。
| 阶段 | 主要动作 | 典型耗时 | 自动化程度 |
|---|---|---|---|
| ① 预检 | 版本号、依赖锁、单元测试、隐私清单草案 | 5–15 min | 高(Linux/Mac 均可) |
| ② 签名准备 | 证书、描述文件、钥匙串解锁、ASC API Key | 一次性 + 轮换 | 中(Secret 注入) |
| ③ Archive & Export | xcodebuild archive、导出 IPA | 10–40 min | 高(独占 Mac) |
| ④ TestFlight | 上传、处理、内测分组 | 15–60 min | 高 |
| ⑤ 提审 | 元数据、截图、出口合规、submit for review | 人工 30 min + 等待 | 中(需审批闸门) |
| ⑥ 发布 | 审核通过、 phased release / 全量 | 24–48 h 审核 | 中(状态轮询) |
工程上的分水岭在阶段 ④ 与 ⑤ 之间:TestFlight 应尽可能自动化(每个 main 合并或 nightly 都可出包);提审则建议加人工或 PM 审批,避免文案未就绪就送进审核队列。阶段 ③ 必须在版本 pinned 的 Mac 上执行——这正是 Cloud Mac 作 iOS CI 节点 的主场景,而非为了日常写 Swift 代码。
签名与证书:CI 里最贵的非确定性
本地 Xcode 能 Archive,一到 CI 就报 errSecInternalComponent 或 No profiles for ...——几乎总是身份与环境未固化,不是项目本身坏了。
三类凭据,别混在一个 Secret 里
| 凭据 | 用途 | CI 存放建议 |
|---|---|---|
| Distribution 证书(p12) | codesign 可安装包 | 加密 Secret;构建岛钥匙串或临时 keychain |
| Provisioning Profile | 绑定 Bundle ID、能力、设备 | 按 UUID 下载到 Job 私有目录,禁止全局覆盖 |
| ASC API Key(.p8) | 上传、查 build 状态、deliver 提审 | GitHub Encrypted Secret;无需进钥匙串 |
多 Job 并发签名时,钥匙串争用与 Profile 目录 race 是长尾故障源。Runner 级独立用户、Job 级临时 keychain 是常见解法,细节见 codesign 并发与钥匙串隔离 FAQ。
CI 钥匙串最小脚本(Xcode 16 / macOS 14 假设)
# 在 self-hosted Mac runner 上,每个 release job 开头
KEYCHAIN=$RUNNER_TEMP/build.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN" login.keychain-db
security import dist.p12 -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
构建制品:Archive → Export → 上传 TestFlight
Release 构建与 Debug 构建的差异不在「多一个优化开关」,而在签名配置、Export 选项与版本元数据必须对齐 App Store Connect 里已登记的 App 记录。
xcodebuild Archive(命令行等价于 Xcode Product → Archive)
xcodebuild -scheme MyApp -configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
archive \
DEVELOPMENT_TEAM=XXXXXXXXXX \
-allowProvisioningUpdates
导出 IPA 并上传
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist ExportOptions.plist
# ExportOptions.plist 中 method 应为 app-store
# 上传可用 xcrun altool(旧)或 fastlane pilot / deliver
工程化要点:
- CFBundleVersion(build number)每次上传必须递增,与 git tag 或 CI run number 绑定,写进 Runbook
- 保留 .xcarchive 与 dSYM,上传 GitHub Artifacts 或 S3;拒审与线上崩溃都依赖符号表
- DerivedData 缓存键带 Xcode 主版本,避免升级 Xcode 后脏缓存编译通过、签名失败
构建岛磁盘与缓存策略直接影响阶段 ③ 的耗时稳定性,可参考 自托管 Runner 缓存与磁盘 FAQ。
提审与审核:人审之外还能自动化什么
苹果审核仍是人工为主,但审核前后的大量重复劳动可以流水线化:
适合自动化的部分
- 轮询 build 处理状态(
processing→valid) - 检查「出口合规」「内容版权」「广告标识符」等已保存答案是否仍适用
- fastlane
deliver或 App Store Connect API 提交元数据与截图(前提:Git 里存了截图源文件) - 审核通过后自动发 Slack / 触发 phased release 百分比
建议保留人工闸门的部分
- 首次上架某地区、涉及支付/健康/儿童等敏感类目
- 大版本营销文案、预览视频与商店截图最终稿
- 「提交审核」按钮本身——用 GitHub Environment
required_reviewers或手动workflow_dispatch
拒审(Rejected)或元数据被拒(Metadata Rejected)时,Runbook 应规定:先修 git,再 bump build,再走 ③→④,不要只改 Connect 网页却不发新二进制(除非纯元数据问题)。常见拒因——隐私清单(Privacy Manifest)缺失、登录需提供演示账号、第三方 SDK 声明不全——应在阶段 ① 预检 Job 里用脚本扫描 PrivacyInfo.xcprivacy 与 Info.plist 关键键。
CI/CD Runbook:GitHub Actions + fastlane 最小闭环
下面给出「合并 main → TestFlight 可见」的最小 Workflow。提审 Job 默认关闭,需打 release/* tag 或手动触发。
Fastfile 片段(上传 TestFlight)
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
lane :beta do
setup_ci if ENV['CI']
match(type: "appstore", readonly: true)
build_app(
scheme: "MyApp",
export_method: "app-store",
output_directory: "./build"
)
upload_to_testflight(
skip_waiting_for_build_processing: false,
api_key_path: "asc_api_key.json"
)
end
lane :release do
upload_to_app_store(
submit_for_review: true,
automatic_release: false,
precheck_include_in_app_purchases: false
)
end
end
GitHub Actions:预检 + Release(自托管 Mac)
name: App Store Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
submit_review:
description: 'Submit to App Review'
type: boolean
default: false
jobs:
preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: swiftlint --strict
- run: swift test --parallel
- name: Check Privacy Manifest exists
run: test -f MyApp/PrivacyInfo.xcprivacy
beta:
needs: preflight
runs-on: [self-hosted, macos, ios-release]
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/ci/DerivedData
key: dd-xcode16-${{ hashFiles('**/Package.resolved') }}
- name: TestFlight upload
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
run: bundle exec fastlane beta
submit-review:
if: github.event.inputs.submit_review == true
needs: beta
runs-on: ubuntu-latest
environment: app-store-review
steps:
- uses: actions/checkout@v4
- run: bundle exec fastlane release
编排层仍用 GitHub Actions,执行层固定在自托管 Mac 构建岛——与 混合流水线 同一套哲学:Linux 做快检,Mac 只做 Archive 与上传。发版周临时加 Runner 可参考该文「发版 burst」一节。
常见反例:什么时候别全自动提审
工程化不等于「一切按钮交给机器人」。以下情况强行全自动 submit_for_review 往往得不偿失:
- 商店文案仍在 Google Doc 里改——Connect 与文档不一致会直接 Metadata Rejected
- 后端 API 与 App 版本强耦合——应先确认服务端灰度就绪,再提交审核
- 首次接入 ATT、支付、HealthKit——审核问答与演示视频往往需要产品现场确认
- 共享托管 macOS runner 上做签名——环境漂移比排队更难排;发版链路应上独占 Mac
更稳妥的默认策略:全自动到 TestFlight,半自动到 App Review。内测群体验证通过 + PM 在 Environment 点批准,再跑 submit-review Job。
常见问题 FAQ
App Store 上架必须先有本地 Mac 吗?
不必。日常编码可在 Windows / Linux 完成,但 Archive、签名、上传 TestFlight 必须在 macOS。无本地 Mac 的团队可用 SSH / VNC 连接 Cloud Mac 或自托管构建岛完成整条上架链路。
TestFlight 和 App Store 提审可以放在同一条 CI 流水线吗?
可以共用构建与签名阶段,但应拆成两个 Job:先 upload_to_testflight 供内测;提审 Job 需人工或审批闸门触发,并单独校验元数据、截图与隐私清单是否就绪。
fastlane 和原生 xcodebuild 怎么选?
xcodebuild 负责编译与 Archive;fastlane 封装签名、上传、元数据与 deliver 提审。小团队可 fastlane 一把梭;大仓建议 CI 里显式调用 xcodebuild,fastlane 只管 release 层。
ASC API Key 和 p12 证书哪个更适合 CI?
上传二进制与查审核状态优先 ASC API Key(.p8),无需导入钥匙串;codesign 仍要 Distribution 证书与描述文件。常见做法是 API Key 管 Connect 侧,p12 管签名侧,分 Secret 存储。
审核被拒后如何快速再出一版?
保留被拒 build 的 git tag 与 dSYM;修复后 bump build number、走同一 release 流水线 upload_to_testflight,验证通过后重新 submit_for_review。关键是 build number 单调递增与元数据版本一致。
Export Compliance 和隐私清单必须手动填吗?
首次可在 App Store Connect 网页填写并固化模板;CI 可通过 fastlane deliver 或 App Store Connect API 写入已审核过的答案。加密出口合规若 App 仅用 HTTPS,多数团队选标准豁免并写入 Info.plist。
从 Archive 到审核通过一般要多久?
构建 10–40 分钟(视工程大小);上传与处理 15–60 分钟;审核 24–48 小时常见,节假日更长。工程化目标是把「等苹果」之外的时间压缩并可预测。
远程 Cloud Mac 适合做上架节点吗?
适合。独占物理 Mac、固定 Xcode 版本、持久钥匙串与 DerivedData,比共享托管 runner 更适合签名稳定与发版 deadline;首次 TCC 用 VNC,之后 SSH 无头跑流水线即可。
总结
App Store 上架在 CI/CD 视角下是六阶段 Release 链路,而不是 Xcode 里的单次 Archive。把签名、构建、TestFlight、提审与发布拆开治理,你才能把失败定位到具体阶段,而不是「又上架失败了」。
- 预检与 TestFlight 尽量全自动;提审加审批闸门
- 签名凭据分三类存放;并发构建注意钥匙串与 Profile 隔离
- build number 与 git tag 绑定;dSYM 与 archive 必须留存
- 独占 Mac 构建岛承载阶段 ③④,Linux 承载阶段 ①
- 拒审走同一流水线 bump build,不要只改网页不发包
下一步建议:在现有仓库加一条只上传到 TestFlight 的 Workflow,跑通后再加 environment: app-store-review 提审 Job。构建节点可先用按天 Cloud Mac 验证一周,再决定是否常驻 Mac mini。