ログオフ
開発2026-08-15

Xcode を開かずに新規iOSアプリを TestFlight へ上げる:archive は通るのに export だけ落ちるとき

新しい bundle id で作ったアプリを CLI だけで配信したい。archive は成功するのに exportArchive だけが Cloud signing permission error / No profiles were found で落ちる。export を手動署名に倒すと越えられる。

新しい bundle id(アプリを識別する文字列。com.example.app のような形)で作ったアプリを、Xcode の画面を一度も開かずに TestFlight まで上げたい。xcodebuild archive は通るのに、xcodebuild -exportArchive だけが落ちる。2回同じところで止まって、原因が署名の権限だと分かった。

事象

  • 新規の bundle id で、配布用のプロビジョニングプロファイル(このアプリを配布用に署名してよい、という許可証)がまだ1枚も無い
  • Xcode にアカウントをログインさせていない(CI や、GUI を開きたくない環境)
  • xcodebuild archive は成功し、.xcarchive もできる
  • xcodebuild -exportArchive だけがこれで落ちる
error: exportArchive: Cloud signing permission error
error: exportArchive: No profiles for 'com.example.app' were found

コードもビルド設定も触っていないので、原因がプロジェクト側にあるようには見えない。

原因

2つが重なっている。

ひとつ。クラウド署名(Xcode が Apple 側に配布証明書とプロファイルを自動生成させる仕組み)を使うには、App Store Connect API キーに Admin 権限が要る。Developer や App Manager の権限しか持たないキーだと、「クラウド管理の配布証明書へのアクセス権が無い」という趣旨のメッセージが返る。これが Cloud signing permission error の中身だ。

もうひとつ。新規 bundle id には配布プロファイルがまだ存在しない。クラウド署名で作ってもらえないなら、手動署名に切り替えるしかないが、切り替えたところで指すプロファイルがローカルに無い。だから No profiles ... were found が続く。

切り分けの手がかりは、archive と export の非対称だ。落ちているのは export の段=配布用の署名一式が要求される場所で、archive の段ではまだそこまで要らない。ビルドの中身ではなく、署名の種類が切り替わる境目で止まっている。

解決策

Admin 権限のキーを用意できるなら、それが一番早い。権限を広げたくない場合や、そもそも自分が Account Holder ではない場合は、export だけを手動署名に倒すと越えられる。archive は自動署名のままでいい。

手順は4つ。

1. API キーで xcodebuild を認証させる

.p8(Apple が発行する秘密鍵ファイル)のパス・キーID・Issuer ID の3点を渡す。

xcodebuild archive \
  -workspace App.xcworkspace -scheme App \
  -configuration Release -destination 'generic/platform=iOS' \
  -archivePath build/App.xcarchive \
  -allowProvisioningUpdates \
  -authenticationKeyPath "$HOME/private_keys/AuthKey_XXXXXXXXXX.p8" \
  -authenticationKeyID XXXXXXXXXX \
  -authenticationKeyIssuerID 00000000-0000-0000-0000-000000000000

2. bundle id を API で登録する

まず API 用のトークンを作る。App Store Connect API は ES256 署名の JWT を要求する。ここに罠がひとつあって、Node 標準の crypto は既定で DER 形式の署名を出すが、Apple が受け付けるのは生の64バイト形式(P1363)だ。dsaEncoding の指定を忘れると、正しい鍵でも 401 が返る。

import { createSign, createPrivateKey } from 'node:crypto'
import { readFileSync } from 'node:fs'

const b64url = (o) => Buffer.from(JSON.stringify(o)).toString('base64url')
const now = Math.floor(Date.now() / 1000)

const header = b64url({ alg: 'ES256', kid: KEY_ID, typ: 'JWT' })
const payload = b64url({
  iss: ISSUER_ID,
  iat: now,
  exp: now + 600,          // 20分以内。長すぎると弾かれる
  aud: 'appstoreconnect-v1',
})

const signer = createSign('SHA256')
signer.update(`${header}.${payload}`)
const sig = signer
  .sign({ key: createPrivateKey(readFileSync(P8_PATH)), dsaEncoding: 'ieee-p1363' })
  .toString('base64url')

const jwt = `${header}.${payload}.${sig}`

dsaEncoding: 'ieee-p1363' を付けると署名部が64バイト固定になる。これが正しい形。付け忘れると70バイト前後のDERになり、見た目は同じ JWT なのに認証だけ通らない。

トークンができたら登録する。

curl -X POST https://api.appstoreconnect.apple.com/v1/bundleIds \
  -H "Authorization: Bearer $JWT" \
  -H 'Content-Type: application/json' \
  -d '{"data":{"type":"bundleIds","attributes":{
        "identifier":"com.example.app","name":"Example App","platform":"IOS"}}}'

3. 配布プロファイルを自作して、ローカルに置く

POST /v1/profiles で作る。profileType は App Store 配布なら IOS_APP_STORE。関連付けとして、さきほどの bundle id と、手元にある配布証明書(GET /v1/certificatescertificateTypeIOS_DISTRIBUTION のもの)のIDを渡す。

curl -X POST https://api.appstoreconnect.apple.com/v1/profiles \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"data":{"type":"profiles",
        "attributes":{"name":"Example App Store Profile","profileType":"IOS_APP_STORE"},
        "relationships":{
          "bundleId":{"data":{"type":"bundleIds","id":"'"$BUNDLE_ID_RESOURCE_ID"'"}},
          "certificates":{"data":[{"type":"certificates","id":"'"$CERT_ID"'"}]}}}}'

ここからが飛ばしやすい。レスポンスの profileContent はプロファイル本体を base64 にしたもので、これをデコードして所定の場所に置かないと export は見つけられない。ファイル名はレスポンスの uuid を使う。

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
UUID=$(jq -r '.data.attributes.uuid' profile.json)
jq -r '.data.attributes.profileContent' profile.json | base64 --decode \
  > ~/Library/MobileDevice/Provisioning\ Profiles/"$UUID".mobileprovision

作っただけで満足して次に進むと、また No profiles ... were found が出る。API 上に存在することと、手元から見えることは別だ。

4. 手動署名で export する

ExportOptions.plist で署名方式を明示する。provisioningProfiles に書くのはプロファイルの名前で、UUIDではない。

<key>method</key>            <string>app-store-connect</string>
<key>signingStyle</key>      <string>manual</string>
<key>signingCertificate</key><string>iPhone Distribution</string>
<key>provisioningProfiles</key>
<dict>
  <key>com.example.app</key><string>Example App Store Profile</string>
</dict>

method は Xcode 15 で app-store から app-store-connect に変わった。古い手順書をコピーすると値だけ古いままになる。

xcodebuild -exportArchive \
  -archivePath build/App.xcarchive \
  -exportPath build/ipa \
  -exportOptionsPlist ExportOptions.plist

xcrun altool --upload-app -f build/ipa/App.ipa -t ios \
  --apiKey XXXXXXXXXX --apiIssuer 00000000-0000-0000-0000-000000000000

altool--apiKey にIDしか受け取らず、.p8 本体は決まった場所から読む。~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8 に置いておく。鍵をどこか整理しやすい場所へ移すと、ここが静かに壊れる。

補足:API でやれないことがひとつだけ残る

App Store Connect 上のアプリの枠そのものは、API では作れない。Apple のリファレンスにも、このAPIで新規アプリを作らず App Store Connect のサイトで作るように、と書かれている。ブラウザで1回だけアプリを登録する必要がある。裏を返すと、そこから先の bundle id 登録・プロファイル作成・ビルド・アップロードは全部 CLI に寄せられる。

なお、公開済みアプリの更新でどこまで自動化できるかは別記事「App Store Connect API で iOS リリースを自動化する」で扱った。この記事は、その手前にある「初回ビルドをどう通すか」の話になる。

次に同じ症状を見たときの1手

archive が通って -exportArchive だけが落ちたら、権限を取りに行く前に ExportOptions.plistmanual に倒す。プロファイルを1枚自作して所定の場所に置けば済む話で、Admin 権限の申請より速い。エラー文言が Cloud signing と言っているせいでクラウド側を直しに行きたくなるが、直すべきなのは自分の export の設定のほうだった。

関連記事

開発 一覧へ