archive が通っても Distribution 証明書は1枚も発行されていない — SSH 越しの Mac mini で iOS を Ad Hoc 署名する
GUI のない Mac mini に SSH で入り、Capacitor でラップした iOS アプリを Ad Hoc 署名して IPA にした。配る先は手元の iPhone だけなので App Store は経由しない。headless で iOS に署名する解説はほぼ必ず App Store Connect API キーの発行から始まるが、今回はそれなしで通った。代わりに詰まったのは、** ARCHIVE SUCCEEDED ** が出た時点では署名の話が何ひとつ終わっていない、という点だった。
archive は Development 証明書で通ってしまう
出発点の Mac mini には、keychain に古い Apple Development 証明書が1枚あるだけだった。Apple Distribution 証明書もプロビジョニングプロファイルもローカルには無い(~/Library/MobileDevice/Provisioning Profiles/ は空)。この状態から -allowProvisioningUpdates を付けて archive を回す。
$ xcodebuild -project ios/App/App.xcodeproj -scheme App -configuration Release \
-destination "generic/platform=iOS" \
-archivePath ~/build/artifacts/cat-room.xcarchive \
-allowProvisioningUpdates \
DEVELOPMENT_TEAM=$TEAM_ID CODE_SIGN_STYLE=Automatic \
archive
** ARCHIVE SUCCEEDED **通った。しかしこのとき使われた署名は、もともと手元にあった Apple Development 証明書(Signing Identity: "Apple Development: ...")だった。Ad Hoc 配布に必要な Apple Distribution 証明書はまだ1枚も発行されていない。
自動発行が走るのは次の export だった。-exportArchive を叩いた時点で初めて、Apple 側に Distribution 証明書と Ad Hoc プロファイルが作られる。
# exportOptions.plist:
# method = release-testing / teamID = $TEAM_ID / signingStyle = automatic
$ xcodebuild -exportArchive \
-archivePath ~/build/artifacts/cat-room.xcarchive \
-exportOptionsPlist exportOptions.plist \
-exportPath ~/build/artifacts/cat-room-ios \
-allowProvisioningUpdates
Exported App to: .../cat-room-ios
** EXPORT SUCCEEDED **出来上がった IPA を開いて、署名が本当に配布用になっているかを確認する。
$ unzip -oq App.ipa
$ security cms -D -i Payload/App.app/embedded.mobileprovision > prof.plist
$ plutil -p prof.plist | grep -E '"Name"|ExpirationDate|get-task-allow'
"get-task-allow" => false
"ExpirationDate" => 2027-08-13 05:36:10 +0000
"Name" => "iOS Team Ad Hoc Provisioning Profile: com.ccya.nekokintai"
$ codesign -dvvv Payload/App.app | grep -E "Authority|TeamIdentifier"
Authority=Apple Distribution: ...
Authority=Apple Worldwide Developer Relations Certification Authority
Authority=Apple Root CAget-task-allow が false になっていて(配布署名の証)、Authority が archive 時の Apple Development から Apple Distribution に差し替わっている。プロファイルの有効期限は発行からちょうど1年。ここまで見て初めて「Ad Hoc 署名が通った」と言える。
App Store Connect API キーを発行しかけて、やめた
当初の計画は、App Store Connect API キー(.p8 と Key ID と Issuer ID)を発行して -authenticationKeyPath で渡す構成だった。CI で iOS 署名を自動化する記事も公式ドキュメントも、ほぼ必ずこの形を要求する。
発行を依頼する前に Mac mini の状態を見たら、Xcode に Apple ID でログインしたセッションが残っていた(defaults read com.apple.dt.Xcode IDEProvisioningTeams に Individual の有料 Developer Program のチームが1件入っている)。この状態だと -allowProvisioningUpdates はそのログインセッションを使う。GUI を一度も開かない非対話の SSH からでも使えて、Distribution 証明書の新規発行と Ad Hoc プロファイルの生成がそのまま通った。だから API キーの発行自体を取りやめた。ダウンロードが1回きりの .p8 を1本増やせば、そのぶん保管の手間が増える。使わずに済むなら増やさない。
ただしログインセッションはいつか切れる(Apple ID のパスワード変更、長期の未使用)。無人の CI にするなら App Store Connect API キーが正解のままだ。この手が成立するのは「手元の Mac を SSH で叩く半自動ビルド」という運用に限る。
keychain のロック解除と実機の登録は、自動発行の外にある
-allowProvisioningUpdates が面倒を見る範囲は思ったより狭い。前提として自分で用意するものが2つあった。
1つめは login keychain のロック解除。非対話の SSH セッションは keychain がロックされたまま始まるので、署名に入る前に開けておく必要がある。パスワードは 0600 のファイルから読む運用にした。
security unlock-keychain -p "$KEYCHAIN_PASS" "$HOME/Library/Keychains/login.keychain-db" security set-key-partition-list -S "apple-tool:,apple:,codesign:" -k "$KEYCHAIN_PASS" "$KEYCHAIN"
2つめは、実機の UDID が Apple Developer アカウントに登録済みであること。-allowProvisioningUpdates はプロファイルの生成と更新はするが、デバイスの新規登録だけはしない。今回は過去に別のアプリで登録した実機2台がアカウントに残っていて追加登録が要らなかっただけで、新しい端末を足すならここで止まる。登録状況は Apple のポータルを開かなくても、Expo プロジェクトのディレクトリで eas device:list を叩けば確認できた。
Xcode 26 で ad-hoc は release-testing になっていた
exportOptions.plist の method は、検索して出てくる手順ではどれも ad-hoc になっている。Xcode 26.1.1 の xcodebuild -help を引くと、そうは書かれていない。
method : String
Describes how Xcode should export the archive. Available options: app-store-connect,
release-testing, enterprise, debugging, developer-id, mac-application, and validation.
... Additional options include app-store (deprecated: use app-store-connect),
ad-hoc (deprecated: use release-testing), and development (deprecated: use debugging).Ad Hoc 配布の正しい値は release-testing で、ad-hoc は deprecated 扱いになっている。app-store → app-store-connect、development → debugging も同じ列に並んでいる。この改称は手元のファイルにも過去の手順にも痕跡が残らないので、気づけるかどうかは -help を1回引いたかどうかの差でしかない。
まとめ
今回いちばん効いたのは、自動化の途中に出てくる成功メッセージが何を保証していて何を保証していないのかを見分けることだった。 archive は手元にある材料だけで完了できるので、成功しても Apple 側とのやり取りは1回も起きていない。外部に依存する処理が後ろの工程に寄っているとき、「途中まで通った」は進捗として数えられない。
もう1つ。認証情報を新しく1つ増やす前に、すでにあるログインセッションで足りないかを見る価値はある。専用のマシンが1台あるなら足りてしまうことがあり、増やさずに済んだ鍵は保管も失効も考えなくていい。ただしそれは、無人で回る CI とは別の運用に自分を縛る選択でもある。