TL;DR

iOSアプリのアップデート版を ビルド → アップロード → ビルド紐付け → whatsNew設定 → 審査提出 まで、すべてコマンドラインとApp Store Connect REST APIで完結させた。初回リリース時と異なり、メタデータやスクリーンショットは既存のものが引き継がれるため、更新時に必要な操作は少ない。

前提: App Store Connect APIだけでiOSアプリを審査提出する完全ガイドのセットアップ(APIキー取得・JWT生成・ヘルパー関数)が完了しているものとする。

全体の流れ

  1. ビルド番号のインクリメント
  2. アーカイブ・IPA書き出し・アップロードxcodebuild + xcrun altool
  3. ビルドの処理完了を確認(API)
  4. ビルドをバージョンに紐付け(API)
  5. 暗号化コンプライアンスの設定(API)
  6. whatsNew(新機能)の設定(API)
  7. 審査提出(API)

1. ビルド番号のインクリメント

App Store Connectは同じビルド番号のアップロードを拒否する。CURRENT_PROJECT_VERSION を上げる必要がある。

XcodeGenを使っている場合は project.yml を編集する:

# project.yml
settings:
  base:
    MARKETING_VERSION: "1.1.0"
    CURRENT_PROJECT_VERSION: "4"  # 3 → 4 に変更

ポイント: マーケティングバージョン(1.1.0)はユーザーに見えるバージョン番号、ビルド番号(4)は同一バージョン内の連番。不具合修正の再提出など、ユーザーから見た変更がない場合はビルド番号だけ上げればよい。

2. アーカイブ・アップロード

# 環境変数を設定
export APP_STORE_API_KEY="YOUR_KEY_ID"
export APP_STORE_API_ISSUER="YOUR_ISSUER_ID"

# XcodeGenでプロジェクト再生成
xcodegen generate

# アーカイブ
xcodebuild archive \
    -project KotenOCR.xcodeproj \
    -scheme KotenOCR \
    -archivePath build/KotenOCR.xcarchive \
    -destination "generic/platform=iOS" \
    -quiet

# IPA書き出し
xcodebuild -exportArchive \
    -archivePath build/KotenOCR.xcarchive \
    -exportPath build/export \
    -exportOptionsPlist scripts/ExportOptions.plist \
    -quiet

# App Store Connectへアップロード
xcrun altool --upload-app \
    --type ios \
    --file build/export/KotenOCR.ipa \
    --apiKey "$APP_STORE_API_KEY" \
    --apiIssuer "$APP_STORE_API_ISSUER"

注意: xcrun altool に渡す環境変数は export しておく必要がある。source .env だけではサブプロセスに渡らない。

アップロード成功時の出力例:

UPLOAD SUCCEEDED with no errors
Delivery UUID: cdf01bf8-...
Transferred 80033738 bytes in 7.487 seconds (10.7MB/s)

3. ビルドの処理完了を確認

アップロード後、App Store Connectがビルドを処理するまで数分かかる。processingStateVALID になるまで待つ。

result = api_request("GET",
    f"builds?filter[app]={APP_ID}&filter[version]=4&sort=-uploadedDate&limit=1")

build = result["data"][0]
state = build["attributes"]["processingState"]
print(f"Build state: {state}")  # VALID になれば完了
BUILD_ID = build["id"]

4. ビルドをバージョンに紐付け

既存のバージョン(PREPARE_FOR_SUBMISSION 状態)に新しいビルドを紐付ける。

# バージョンIDを取得
result = api_request("GET",
    f"apps/{APP_ID}/appStoreVersions"
    "?filter[versionString]=1.1.0&filter[platform]=IOS")
VERSION_ID = result["data"][0]["id"]
state = result["data"][0]["attributes"]["appStoreState"]
print(f"Version: {VERSION_ID}, state: {state}")

# ビルドを紐付け
api_request("PATCH",
    f"appStoreVersions/{VERSION_ID}/relationships/build", {
    "data": {
        "type": "builds",
        "id": BUILD_ID
    }
})
print("Build assigned to version")

5. 暗号化コンプライアンスの設定

標準的なHTTPS通信のみで独自の暗号化を使用していない場合:

api_request("PATCH", f"builds/{BUILD_ID}", {
    "data": {
        "type": "builds",
        "id": BUILD_ID,
        "attributes": {
            "usesNonExemptEncryption": False
        }
    }
})

注意: 既に設定済みの場合は 409 エラーが返る。その場合はスキップしてよい。

6. whatsNew(新機能)の設定

アップデート版の審査提出で最も重要なステップ。各ローカライズに対して whatsNew を設定しないと審査提出がブロックされる。

# ローカライズ一覧を取得
result = api_request("GET",
    f"appStoreVersions/{VERSION_ID}/appStoreVersionLocalizations")

whats_new = {
    "ja": "- チップ(Tip Jar)機能を追加しました\n- 不具合を修正しました",
    "en-US": "- Added Tip Jar feature\n- Bug fixes"
}

for loc in result["data"]:
    locale = loc["attributes"]["locale"]
    loc_id = loc["id"]

    if locale in whats_new:
        api_request("PATCH",
            f"appStoreVersionLocalizations/{loc_id}", {
            "data": {
                "type": "appStoreVersionLocalizations",
                "id": loc_id,
                "attributes": {
                    "whatsNew": whats_new[locale]
                }
            }
        })
        print(f"whatsNew set for {locale}")

初回リリースとの違い: 初回は description(説明文)、keywords(キーワード)、スクリーンショットなども設定が必要だが、アップデートではこれらは前のバージョンから引き継がれる。whatsNew だけが新たに必要になる。

7. 審査提出

reviewSubmissions API を使って審査に提出する(旧 appStoreVersionSubmissions は非推奨)。

# Step 1: レビュー提出を作成
result = api_request("POST", "reviewSubmissions", {
    "data": {
        "type": "reviewSubmissions",
        "attributes": {
            "platform": "IOS"
        },
        "relationships": {
            "app": {
                "data": {
                    "type": "apps",
                    "id": APP_ID
                }
            }
        }
    }
})
SUBMISSION_ID = result["data"]["id"]

# Step 2: バージョンを提出に追加
api_request("POST", "reviewSubmissionItems", {
    "data": {
        "type": "reviewSubmissionItems",
        "relationships": {
            "reviewSubmission": {
                "data": {
                    "type": "reviewSubmissions",
                    "id": SUBMISSION_ID
                }
            },
            "appStoreVersion": {
                "data": {
                    "type": "appStoreVersions",
                    "id": VERSION_ID
                }
            }
        }
    }
})

# Step 3: 審査に提出
result = api_request("PATCH", f"reviewSubmissions/{SUBMISSION_ID}", {
    "data": {
        "type": "reviewSubmissions",
        "id": SUBMISSION_ID,
        "attributes": {
            "submitted": True
        }
    }
})
print(f"State: {result['data']['attributes']['state']}")
# => WAITING_FOR_REVIEW

8. 審査承認後のリリース

App Store Connectでリリース方法を「手動リリース」に設定している場合、審査承認後のステータスは PENDING_DEVELOPER_RELEASE になる。APIでリリースするには appStoreVersionReleaseRequests を使う。

# バージョンの状態を確認
result = api_request("GET",
    f"apps/{APP_ID}/appStoreVersions"
    "?filter[versionString]=1.1.0&filter[platform]=IOS")
version = result["data"][0]
VERSION_ID = version["id"]
state = version["attributes"]["appStoreState"]
print(f"State: {state}")  # PENDING_DEVELOPER_RELEASE

# リリース
api_request("POST", "appStoreVersionReleaseRequests", {
    "data": {
        "type": "appStoreVersionReleaseRequests",
        "relationships": {
            "appStoreVersion": {
                "data": {
                    "type": "appStoreVersions",
                    "id": VERSION_ID
                }
            }
        }
    }
})
print("Released!")

注意: リリース後、App Storeへの反映には数時間かかる場合がある。

9. 配信地域の設定

デフォルトでは配信地域が限定されている場合がある。全世界で利用可能にするには appAvailabilities API(v2)を使う。

# 全テリトリーを取得
result = api_request("GET", "territories?limit=200")
all_territories = [t["id"] for t in result["data"]]
print(f"Total territories: {len(all_territories)}")

# インライン作成用のペイロードを構築
included = []
territory_refs = []
for tid in all_territories:
    local_id = f"${{territory-{tid}}}"
    territory_refs.append({"type": "territoryAvailabilities", "id": local_id})
    included.append({
        "type": "territoryAvailabilities",
        "id": local_id,
        "attributes": {"available": True},
        "relationships": {
            "territory": {
                "data": {"type": "territories", "id": tid}
            }
        }
    })

# v2 APIで配信地域を設定
token = generate_token()
url = "https://api.appstoreconnect.apple.com/v2/appAvailabilities"
data = {
    "data": {
        "type": "appAvailabilities",
        "attributes": {"availableInNewTerritories": True},
        "relationships": {
            "app": {
                "data": {"type": "apps", "id": APP_ID}
            },
            "territoryAvailabilities": {
                "data": territory_refs
            }
        }
    },
    "included": included
}

# v2エンドポイントなので直接リクエスト
import urllib.request
req = urllib.request.Request(
    url, data=json.dumps(data).encode(), method="POST",
    headers={
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }
)
resp = urllib.request.urlopen(req)
print(f"Status: {resp.status}")  # 201 で成功

注意: このAPIは v2 エンドポイント(/v2/appAvailabilities)を使用する。v1の api_request ヘルパーはベースURLが /v1/ のため、直接リクエストを送る必要がある。また、インライン作成では id${local-id} 形式を使う。

IAP(アプリ内課金)の審査提出に関する注意

初回のIAPはブラウザでの操作が必須

アプリで初めてIAPを提出する場合、App Store ConnectのブラウザでバージョンにIAPを紐づけて提出する必要がある。 APIだけでは完結できない。

これは Apple の仕様で、inAppPurchaseSubmissions API を呼び出すと以下のエラーが返る:

STATE_ERROR.FIRST_IAP_MUST_BE_SUBMITTED_ON_VERSION
"The first In-App Purchase for an app must be submitted for review
at the same time that you submit an app version."

また、reviewSubmissionItems でIAPを指定するリレーションシップは存在しない:

# reviewSubmissionItems で指定できるリレーションシップ(Apple公式ドキュメントより)
- reviewSubmission(必須)
- appStoreVersion
- appEvent
- appCustomProductPageVersion
- appStoreVersionExperiment / V2
- gameCenterLeaderboardVersion 等
# IAP関連のリレーションシップは存在しない

初回IAP提出の手順

  1. APIでバージョンの作成・ビルド紐付け・whatsNew設定を行う
  2. App Store Connectのブラウザで該当バージョンのページを開く
  3. アプリ内課金とサブスクリプション」セクションで「アプリ内課金またはサブスクリプションを選択」をクリック
  4. IAP商品にチェックを入れて「完了」
  5. APIで reviewSubmissions を作成し、審査提出する
# 手順5: ブラウザでIAPを紐づけた後、APIで審査提出
result = api_request("POST", "reviewSubmissions", {
    "data": {
        "type": "reviewSubmissions",
        "attributes": {"platform": "IOS"},
        "relationships": {
            "app": {"data": {"type": "apps", "id": APP_ID}}
        }
    }
})
SUBMISSION_ID = result["data"]["id"]

api_request("POST", "reviewSubmissionItems", {
    "data": {
        "type": "reviewSubmissionItems",
        "relationships": {
            "reviewSubmission": {"data": {"type": "reviewSubmissions", "id": SUBMISSION_ID}},
            "appStoreVersion": {"data": {"type": "appStoreVersions", "id": VERSION_ID}}
        }
    }
})

api_request("PATCH", f"reviewSubmissions/{SUBMISSION_ID}", {
    "data": {
        "type": "reviewSubmissions",
        "id": SUBMISSION_ID,
        "attributes": {"submitted": True}
    }
})

2回目以降のIAP追加

初回のIAP審査が承認されれば、以降のIAP追加は inAppPurchaseSubmissions APIで個別に提出できる(バージョン提出不要)。

api_request("POST", "inAppPurchaseSubmissions", {
    "data": {
        "type": "inAppPurchaseSubmissions",
        "relationships": {
            "inAppPurchaseV2": {
                "data": {"type": "inAppPurchases", "id": IAP_ID}
            }
        }
    }
})

確認方法

審査承認後、IAP商品の状態をAPIで確認する:

result = api_request("GET", f"apps/{APP_ID}/inAppPurchasesV2")
for iap in result["data"]:
    state = iap["attributes"]["state"]
    product_id = iap["attributes"]["productId"]
    print(f"{product_id}: {state}")
    # APPROVED なら正常、READY_TO_SUBMIT なら審査に含まれていない

教訓: IAP商品がアプリ審査に含まれたかは、審査承認後に stateAPPROVED に変わったかどうかで判断する。READY_TO_SUBMIT のままであれば、ブラウザでIAPを紐づけ直して再提出する。

参考: IAP商品の登録方法は App Store Connect APIでチップ(Tip Jar)機能を追加する完全ガイド を参照。

まとめ

アップデート版の審査提出で必要なAPI操作は以下の通り:

操作エンドポイントメソッド
ビルド状態確認builds?filter[app]={id}GET
ビルド紐付けappStoreVersions/{id}/relationships/buildPATCH
暗号化コンプライアンスbuilds/{id}PATCH
whatsNew設定appStoreVersionLocalizations/{id}PATCH
審査提出(作成)reviewSubmissionsPOST
審査提出(アイテム追加)reviewSubmissionItemsPOST
審査提出(確定)reviewSubmissions/{id}PATCH
リリースappStoreVersionReleaseRequestsPOST
配信地域設定v2/appAvailabilitiesPOST

初回リリースと比べて操作が少なく、ビルドのアップロードからリリースまでほぼコマンドラインで完結できる。ただし、初回のIAP提出時のみApp Store Connectブラウザでの紐づけ操作が必要(2回目以降はAPIで完結可能)。CI/CDパイプラインへの組み込みも容易である。

関連記事