開發者使用 Mac 與 iPhone 完成 App Store 上架與 TestFlight 發版

App Store 上架在不少團隊裡仍是「PM 催、iOS 負責人手動 Archive、半夜傳 TestFlight、隔天再填一遍中繼資料」——功能早 merge 了,真正卡住的是沒有工程化的 Release 鏈路。本文從 CI/CD 視角把從 Xcode 到審核通過拆成六個可觀測階段,給出簽章、建置、上傳、提審與發布的邊界,並附可直接改造的 Runbook。若你正糾結建置節點放哪,可先讀 iOS 流水線新玩法 裡關於 Mac 建置島的分層思路。

讀完你會得到:一張六階段上架鏈路圖手工 vs 自動化對照表、fastlane + GitHub Actions 最小 YAML、以及拒審後 30 分鐘內再出 TestFlight 的檢查清單。

一句話:上架不是 Xcode 裡點一次 Product → Archive,而是簽章可信 + 制品可追溯 + Connect 中繼資料一致 + 審核狀態可輪詢四條約束下的 Release 工程。

為什麼要把「上架」當成工程鏈路,而不是 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 & Exportxcodebuild archive、匯出 IPA10–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 就報 errSecInternalComponentNo 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"
注意:首次在遠端 Mac 上匯入憑證時,往往需要在 VNC 圖形工作階段裡完成一次鑰匙圈信任與 TCC 授權;之後 Job 才能純 SSH 無頭跑。SSH 與 VNC 的分工見 企業遠端 CI:SSH vs VNC

建置制品: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 處理狀態(processingvalid
  • 檢查「出口合規」「內容版權」「廣告識別碼」等已儲存答案是否仍適用
  • 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.xcprivacyInfo.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。

相關閱讀