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 / Linux 主力 + 遠端 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。