在雲端 Mac mini 上打通 notarytool 公證與 fastlane TestFlight 自動發布
凌晨兩點,出海團隊的營運在群組裡問「新版本什麼時候能進 TestFlight」,而你正對著終端機裡第三次報錯的 codesign 發愣——公證鏈路只要有一個環節設定不對,整套流程就會在提交那一步靜靜卡死,不報錯也不通過,只留一個模糊的狀態碼給你猜。這篇文章記錄我們在雲端 Mac mini 節點上,把簽章、公證、上傳 TestFlight 這三步徹底打通、跑成無人值守流水線的完整過程。
為什麼公證是出海團隊的隱形卡點
從 Xcode 13 開始,altool 的公證介面已經廢棄,蘋果統一收攏到 notarytool。不少團隊的 CI 腳本還停留在舊教學的寫法,證書類型、時間戳參數、entitlements 設定只要有一處出錯,公證請求就會被靜默拒絕,錯誤訊息藏在一份要另外拉取的 JSON 日誌裡。更麻煩的是,公證要排隊等蘋果伺服器處理,本地端根本無法重現「這次為什麼慢了 40 分鐘」,只能靠標準化流程來降低變數。
把這套流程放在雲端 Mac mini 上跑,好處是證書、API Key、keychain 狀態都固定在一台長期在線的節點上,不會因為換了台筆電或團隊成員離職就得重新匯入證書——這也是我們堅持「獨享實體機、按週期租用」而不是每次現開臨時環境的原因。
事前準備:證書與 API Key
先確認三件事都齊了:
- Apple Distribution 證書(用於 App Store 分發)+ 對應私鑰,匯出為
.p12後匯入節點的 keychain。 - App Store Connect API Key:在 App Store Connect 的「使用者與存取權」頁面產生,下載
.p8私鑰檔,記下 Key ID 和 Issuer ID。這是全程不需要登入任何 Apple 帳號的關鍵——公證與上傳都走 API Key 驗證。 - provisioning profile:要與 Bundle ID、證書相符,建議用
fastlane match統一託管,避免每台機器各自產生一份。
把證書匯入節點的 keychain:
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import DistributionCert.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security list-keychains -d user -s build.keychain login.keychain
.p8 檔案放到節點固定路徑,例如 ~/.appstoreconnect/private_keys/AuthKey_ABCDE12345.p8,權限設為 600,千萬不要提交進任何程式碼倉庫。
用 notarytool 走完簽章到公證
簽章時務必帶上時間戳與硬化執行時期(hardened runtime),否則公證會因為缺少 secure timestamp 而直接失敗:
codesign --sign "Apple Distribution: Your Company" \
--timestamp --options runtime \
--entitlements App.entitlements \
build/App.app
打包成 .pkg 或直接用 xcodebuild -exportArchive 產出 .ipa 後,送出公證:
xcrun notarytool submit build/App.ipa \
--key ~/.appstoreconnect/private_keys/AuthKey_ABCDE12345.p8 \
--key-id ABCDE12345 \
--issuer 69a6de7x-xxxx-47e3-e053-5b8c7c11a4d1 \
--wait
--wait 會阻塞直到蘋果伺服器回傳結果,通常 3~8 分鐘,偶爾遇到排隊高峰會拖到 20 分鐘以上。拿到 submissionId 後,如果狀態是 Invalid,一定要把完整日誌拉下來查清楚,別憑猜測就重新簽章:
xcrun notarytool log <submissionId> \
--key ~/.appstoreconnect/private_keys/AuthKey_ABCDE12345.p8 \
--key-id ABCDE12345 --issuer 69a6de7x-xxxx-47e3-e053-5b8c7c11a4d1
我們踩過的一個坑:entitlements 裡開了 App Sandbox,但某個第三方動態庫沒有對應簽章,notarytool 的日誌只會提示「The binary is not signed with a valid Developer ID certificate」,看起來像是證書問題,實際上是子模組漏簽——用
codesign --verify --deep --verbose=4逐層檢查才能定位到具體檔案。
用 fastlane pilot 自動分發到 TestFlight
公證通過後,用 fastlane pilot 上傳到 TestFlight,同樣走 API Key 驗證,不需要互動式登入:
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_filepath: ENV["ASC_KEY_PATH"]
)
build_app(scheme: "App", export_method: "app-store")
pilot(
api_key: api_key,
skip_waiting_for_build_processing: true,
changelog: "自動化分發:修復啟動崩潰與網路重試邏輯"
)
end
skip_waiting_for_build_processing: true 讓腳本上傳完就結束,不會卡住節點上的其他任務;後續處理進度改用輪詢,或乾脆等蘋果的通知信即可,沒必要把 CI 任務耗在等待上。
常見錯誤與排查清單
| 錯誤訊息關鍵字 | 可能原因 | 處理方式 |
|---|---|---|
| signature does not include a secure timestamp | codesign 沒加 --timestamp |
重新簽章並加上 --timestamp --options runtime |
| The binary is not signed with a valid Developer ID | 某個內嵌 framework/動態庫未簽章 | 用 codesign --verify --deep 逐層排查後單獨簽章 |
| Missing Info.plist key: ITSAppUsesNonExemptEncryption | 使用了加密相關 API 但未宣告 | 在 Info.plist 補上對應鍵值,或依實際情況宣告豁免 |
| Invalid Provisioning Profile | profile 與證書/Bundle ID 不匹配 | 用 fastlane match 重新同步證書與 profile |
| 提交後長時間無回應 | 蘋果伺服器在排隊 | 不要重複提交,等待或查詢 submissionId 狀態 |
把這張表貼進團隊的排障文件,能省下大量「到底是誰的錯」的來回溝通。
把流程釘進雲端 Mac mini 的排程任務
最後一步,是讓整條流水線自動跑起來,而不是靠人手動觸發。在雲端 Mac mini 上用 launchd 註冊一個回應 Git webhook 或定時輪詢的任務,把簽章、公證、上傳三步串接執行,失敗時透過郵件或工單系統通知負責人,而不是讓腳本靜默退出。因為節點是獨享實體機、常駐在線,keychain 狀態與證書不會像臨時容器那樣每次重新初始化,公證請求的成功率與排隊時間也更穩定。
常見問題
notarytool 顯示 The signature does not include a secure timestamp 怎麼辦?
簽章時缺少時間戳記所致,重新以 codesign --timestamp --options runtime 簽章後再提交公證即可。
fastlane pilot 上傳後 TestFlight 一直顯示處理中正常嗎?
蘋果通常 15~60 分鐘處理完成,超過 2 小時建議查看 App Store Connect 活動紀錄,常見原因是 Info.plist 缺少 ITSAppUsesNonExemptEncryption 導致人工審核。
在雲端 Mac mini 跑這套流程需要登入 Apple 帳號嗎?
不需要,只要把憑證匯入節點鑰匙圈,並放上 App Store Connect API Key 的 .p8 檔與環境變數即可全程無人值守。