開発者が Mac と iPhone で App Store リリースと TestFlight 配布を完了するイメージ

App Store リリースは、多くのチームでまだ「PM が催促、iOS 担当が手動 Archive、深夜に TestFlight へアップロード、翌日またメタデータを入力」という運用のままです。機能はとっくに merge 済みなのに、本当のボトルネックは工程化されていない Release チェーンにあります。本記事は CI/CD 視点で Xcode から審査通過までを 6 つの観測可能な段階に分解し、署名・ビルド・アップロード・審査提出・公開の境界を示します。ビルドノードの置き場所で悩んでいるなら、先に iOS パイプライン新戦略 の Mac ビルド島のレイヤー分けを読むと整理しやすいです。

読み終えると手に入るもの:6 段階リリースチェーン図手作業 vs 自動化対照表、fastlane + GitHub Actions 最小 YAML、拒否後 30 分で TestFlight を再配布するチェックリスト。

ひとことで:リリースは Xcode で Product → Archive を一度押す作業ではなく、署名の信頼性 + 成果物のトレーサビリティ + Connect メタデータの整合 + 審査ステータスのポーリングという 4 制約下の Release エンジニアリングです。

なぜ「リリース」をチェックリストではなく工程チェーンとして扱うか

初心者はリリースをチェックリストと捉えがちです:スクリーンショット、プライバシーポリシー、提出ボタン。問題は——チェックリストは状態を記録しないし、N 回目のリリースが 1 回目と同じ前提で走る保証もありません。工程化の視点では、リリースは有向パイプラインであり、各段階が明確な成果物を出力し、次段階は前段の出力だけを消費します:

  • 再現可能:同一 git tag がビルド島で、同一 Xcode と依存ロックがあればローカルと同じ IPA を生成できる
  • 観測可能:各段階にログ・所要時間・失敗コード。Slack には「アップロード成功 / 処理中 / 審査提出可」が見えるべきで、「たぶん上がった」ではない
  • ロールバック可能:ストア側に前バージョン build を残す。git 側に tag と dSYM を残し、クラッシュのシンボル化を途切れさせない
  • 責務分離:開発がバイナリとバージョン番号、プロダクト / マーケがストア文案とスクリーンショット——CI では別 Job か承認ゲートで分離

Mac を持たないクロスプラットフォームチームにも同じチェーンが当てはまります。コーディングは Windows / Linux、Release 段階だけ macOS ビルドノードに集約すればよい。分担パターンは Windows 主力 + リモート Mac ビルド島 を参照。

6 段階:Xcode Scheme から App Store 販売まで

以下の表が本文の骨格です。後半の Runbook はこの順で Job を並べ、「先にアップロードしてからバージョン bump を忘れていた」という事故を防ぎます。

段階主な作業典型所要時間自動化度
① 事前検証バージョン番号、依存ロック、単体テスト、プライバシー manifest 草案5–15 min高(Linux / Mac 可)
② 署名準備証明書、プロファイル、キーチェーン解除、ASC API Key初回 + ローテーション中(Secret 注入)
③ Archive & Exportxcodebuild archive、IPA エクスポート10–40 min高(専有 Mac)
④ TestFlightアップロード、処理、社内テストグループ15–60 min
⑤ 審査提出メタデータ、スクリーンショット、輸出コンプライアンス、submit for review人手 30 min + 待ち中(承認ゲート要)
⑥ 公開審査通過、phased release / 全量24–48 h 審査中(ステータスポーリング)

工程上の分水嶺は段階 ④ と ⑤ の間です。TestFlight は可能な限り自動化(main マージや nightly ごとにビルド配布)。審査提出には人手または PM 承認を挟み、文案未確定のまま審査キューに入れない。段階 ③ はバージョン固定の Macで実行する必要があり——これが Cloud Mac を iOS CI ノードにする主なユースケースで、日常の Swift コーディングのためではありません。

署名と証明書:CI で最も高コストな非決定性

ローカル Xcode では Archive できるのに CI で errSecInternalComponentNo profiles for ... が出る——ほぼいつもアイデンティティと環境が固定されていないことが原因で、プロジェクト自体の不具合ではありません。

3 種類の認証情報を 1 つの Secret に混ぜない

認証情報用途CI 保管の推奨
Distribution 証明書(p12)codesign でインストール可能パッケージを生成暗号化 Secret。ビルド島キーチェーンまたは一時 keychain
Provisioning ProfileBundle 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 ビルドの差は「最適化フラグが 1 つ多い」だけではありません。署名設定、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 アップグレード後に汚キャッシュでコンパイルは通るが署名が落ちる事故を防ぐ

ビルド島のディスクとキャッシュ戦略は段階 ③ の所要時間安定性に直結します。self-hosted Runner キャッシュとディスク FAQ を参照。

審査提出と審査:人手審査の外側で自動化できること

Apple の審査は依然として人手中心ですが、審査前後の反復作業の多くはパイプライン化できます:

自動化に向く部分

  • 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 の Web だけ直して新バイナリを出さない(純メタデータ問題を除く)。よくある拒否理由——プライバシー manifest(Privacy Manifest)欠落、ログイン用デモアカウント未提供、サードパーティ SDK 宣言不足——は段階 ① の事前検証 Job で PrivacyInfo.xcprivacyInfo.plist のキーをスクリプトスキャンすべきです。

CI/CD Runbook:GitHub Actions + fastlane 最小ループ

以下は「main マージ → TestFlight で確認可能」の最小 Workflow。審査提出 Job はデフォルト off。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(self-hosted 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、実行層はself-hosted Mac ビルド島に固定——混合パイプラインと同じ思想:Linux で高速検証、Mac は Archive とアップロードだけ。リリース週に Runner を一時増設する場合は同記事の「burst」節を参照。

よくあるアンチパターン:全自動審査提出を避ける場面

工程化は「すべてのボタンをロボットに任せる」ことではありません。以下では無理に全自動 submit_for_review すると損になりがちです:

  • ストア文案がまだ Google Doc で推敲中——Connect と文書の不一致は Metadata Rejected に直結
  • バックエンド API と App バージョンが強結合——サーバー側の段階公開が整ってから審査提出すべき
  • 初めて ATT・決済・HealthKit を導入——審査 Q&A とデモ動画はプロダクト現場の確認が要る
  • 共有ホスト 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 や self-hosted ビルド島に接続し、リリース全体を完遂できます。

TestFlight と App Store 審査提出を同じ CI パイプラインに載せられますか?

ビルドと署名フェーズは共有可能ですが、Job は 2 つに分けましょう。先に upload_to_testflight で社内テスト用ビルドを配布し、審査提出 Job は人手または承認ゲートで起動し、メタデータ・スクリーンショット・プライバシー manifest の準備を個別に検証します。

fastlane とネイティブ xcodebuild はどう選べばよいですか?

xcodebuild がコンパイルと Archive を担当し、fastlane が署名・アップロード・メタデータ・deliver 審査提出をラップします。小規模チームは fastlane 一本化でも可。大規模リポジトリは CI で xcodebuild を明示呼び出しし、fastlane は release 層だけに限定するのが定石です。

CI には ASC API Key と p12 証明書のどちらが向いていますか?

バイナリアップロードと審査ステータス確認は ASC API Key(.p8)を優先。キーチェーンへのインポートは不要です。codesign には引き続き Distribution 証明書とプロビジョニングプロファイルが必要です。一般的には API Key を Connect 側、p12 を署名側に分けて Secret 保管します。

審査却下後、どう素早く再ビルドしますか?

却下された build の git tag と dSYM を保持。修正後に build number を bump し、同じ release パイプラインで upload_to_testflight を実行。検証後に submit_for_review を再実行します。build number の単調増加とメタデータのバージョン一致が鍵です。

Export Compliance とプライバシー manifest は手入力必須ですか?

初回は App Store Connect の Web で入力しテンプレート化できます。CI では fastlane deliver または App Store Connect API で承認済みの回答を書き込めます。暗号化輸出コンプライアンスで App が HTTPS のみの場合、多くのチームは標準免除を選び Info.plist に記載します。

Archive から審査通過までどれくらいかかりますか?

ビルド 10–40 分(プロジェクト規模による)。アップロードと処理 15–60 分。審査は 24–48 時間が一般的で、連休はさらに長くなります。工程化の目標は「Apple を待つ」以外の時間を圧縮し予測可能にすることです。

リモート Cloud Mac はリリースノードに向いていますか?

向いています。専有物理 Mac、固定 Xcode バージョン、永続キーチェーンと DerivedData は、共有ホスト runner より署名の安定性とリリース deadline に適しています。初回 TCC は VNC、以降は SSH でヘッドレス実行が可能です。

まとめ

App Store リリースを CI/CD 視点で見ると、Xcode 内の一度きりの Archive ではなく 6 段階の Release チェーンです。署名・ビルド・TestFlight・審査提出・公開を分けて統治すれば、失敗を「またリリースに失敗した」ではなく具体段階に特定できます。

  • 事前検証と TestFlight は可能な限り全自動。審査提出には承認ゲート
  • 署名認証情報は 3 種類に分離保管。並行ビルドではキーチェーンと Profile を分離
  • build number を git tag と紐付け。dSYM と archive は必ず保管
  • 専有 Mac ビルド島が段階 ③④、Linux が段階 ① を担う
  • 拒否時は同一パイプラインで bump build。Web だけ直してパッケージを出さない

次の一手:既存リポジトリに TestFlight のみアップロードする Workflow を 1 本追加し、通ったら environment: app-store-review の審査提出 Job を足す。ビルドノードは日単位の Cloud Mac で 1 週間検証し、常駐 Mac mini が要るか判断するのが現実的です。

関連記事