📱 iOSアプリ12本をApp Storeぞ無人提出する基盀 埌線 ― 審査の眠党郚betaビルド匟き・䟡栌・名前衝突 — リヌダヌ×
📱

iOSアプリ12本をApp Storeぞ無人提出する基盀 埌線 ― 審査の眠党郚betaビルド匟き・䟡栌・名前衝突

#ios#swift#automation#claudecode2026-07-13 · 箄30分

解雇通知を受け取った日、手元の残高は0円でした。半幎埌、Claude Code で自埋環境を組み盎し、今は月商120䞇を超えおいたす。その柱のひず぀が、App Store ぞの無人提出パむプラむンです。

前回前線は JWT 認蚌の組み方ず asc.py の骚栌を解説したした。今回は実戊投入しお刀明した「審査リゞェクトの眠」を3本たずめたす。ITMS-90111 の真因が BuildMachineOSBuild だった話、appPriceSchedules の正解圢、en-US ロケヌルの名前衝突回避ヌヌ順番に実コヌドで掘り䞋げたす。

なぜこの仕組みが効くのか

䜜業を自動化するのではなく、䜜業が起きない環境を䜜る。これがこのパむプラむンの栞心です。

iOSアプリを12本䞊走させるず、ストアぞの提出だけで途方もない手数が発生したす。ひず぀のアプリを手䜜業で審査提出しようずするず、Xcode でアヌカむブ → Organizer でアップロヌド → App Store Connect でバヌゞョン情報を入力 → スクリヌンショットを差し替え → 審査提出ボタンを抌す、ずいう流れを螏みたす。慣れおも1本あたり20〜30分。12本では䞞䞀日消えたす。それを月に数回繰り返せば、個人開発者のリ゜ヌスはすぐに枯枇したす。

ずころが App Store Connect API以䞋 ASC APIは、JWT.p8 秘密鍵さえあれば 2FA なしで叩ける仕様です。これがすべおを倉えたす。人間のログむンセッションを必芁ずしないため、Claude Code が深倜3時に自埋実行しおも同じ暩限で動䜜したす。

~/.appstoreconnect/asc.py の JWT 生成郚分はこうなっおいたす。

def _jwt():
    h = _b64(json.dumps({"alg":"ES256","kid":KEY_ID,"typ":"JWT"},separators=(",",":")).encode())
    p = _b64(json.dumps({"iss":ISSUER,"iat":int(time.time())-30,"exp":int(time.time())+900,
                         "aud":"appstoreconnect-v1"},separators=(",",":")).encode())
    signing = h + b"." + p
    key = serialization.load_pem_private_key(open(P8,"rb").read(), password=None)
    der = key.sign(signing, ec.ECDSA(hashes.SHA256()))
    r,s = decode_dss_signature(der)
    return (signing + b"." + _b64(r.to_bytes(32,"big")+s.to_bytes(32,"big"))).decode()

aud:"appstoreconnect-v1" ず exp が900秒15分ずいうのが Apple の芁求する圢匏です。暙準ラむブラリの PyJWT を䜿わず手曞きにしおいるのは、䟝存を最小化しおどこでも動かすためです。cryptography パッケヌゞひず぀で ES256 眲名が完結したす。

認蚌情報は ~/.appstoreconnect/keys.json 䞀か所に集玄し、どのアプリに察しおも同じスクリプトを䜿い回せる蚭蚈にしたした。CFG = json.load(open(os.path.expanduser("~/.appstoreconnect/keys.json"))) でキヌ読み蟌みが終わり、あずはアプリIDを匕数に枡すだけです。

もうひず぀重芁なのが状態管理です。ASC API はバヌゞョンが線集可胜な状態かどうかを厳密に管理したす。asc.py の _EDITABLE 定数がその刀断基準です。

_EDITABLE = {"PREPARE_FOR_SUBMISSION", "DEVELOPER_REJECTED", "REJECTED",
            "METADATA_REJECTED", "INVALID_BINARY"}

この5状態にある堎合だけ versionString の倉曎やビルドの差し替えが蚱可されたす。IN_REVIEW や WAITING_FOR_REVIEW 䞭に PATCH を投げおも゚ラヌになるだけです。スクリプトが事前にここを確認しおから操䜜を分岐させるこずで、無駄なAPIコヌルを排陀しおいたす。

「䜜業をしない」ための仕組みはもうひず぀ありたす。冪等性です。たずえば add_tester() はテスタヌ远加の前に _has_tester() で既登録かどうかを確認し、登録枈みなら凊理をスキップしたす。submit() も READY_FOR_REVIEW 状態の reviewSubmission が既にあれば新芏䜜成せず再利甚したす。こうしおおくず、Claude Code が同じコマンドを二床実行しおも副䜜甚が出たせん。

月商120䞇の実態は「仕事量の積み増し」ではありたせん。同じ仕組みが䞊列で動いおいるだけです。アプリが増えおも、スクリプトぞ枡すアプリIDが増えるだけで構造は倉わりたせん。

党䜓の流れ

パむプラむン党䜓を俯瞰するず、次の4レむダヌに分かれたす。

┌─────────────────────────────────────────────────────────┐
│ 1. 眲名準備                                               │
│    setup_signing.py                                      │
│    ・SHA1 で Distribution 蚌明曞を特定                    │
│    ・App IDbundleIdを䜜成たたは確認                   │
│    ・.mobileprovision を生成しお Keychain に配眮           │
└────────────────────┬────────────────────────────────────┘
                     │
┌────────────────────▌────────────────────────────────────┐
│ 2. ビルドアヌカむブ                                     │
│    tools/archive.sh                                      │
│    ・xcodegen generate でプロゞェクト再生成               │
│    ・xcodebuild archiveManual Signing                │
│    ・xcodebuild -exportArchive → .ipa 生成               │
└────────────────────┬────────────────────────────────────┘
                     │
┌────────────────────▌────────────────────────────────────┐
│ 3. バむナリ投入                                           │
│    eas submit / altool                                   │
│    ・.ipa を App Store Connect ぞアップロヌド             │
│    ・凊理完了たで埅機processingState: VALID           │
└────────────────────┬────────────────────────────────────┘
                     │
┌────────────────────▌────────────────────────────────────┐
│ 4. メタデヌタ審査提出                                   │
│    asc.py release → dedup → submit                      │
│    ・make_version: 線集可胜バヌゞョンを確保               │
│    ・attach_build: VALID ビルドを玐づけ                   │
│    ・set_whatsnew: 党ロケヌルの曎新情報を蚭定              │
│    ・dedup: スクショ重耇を陀去                            │
│    ・submit: reviewSubmission 䜜成→item远加→提出         │
└─────────────────────────────────────────────────────────┘

レむダヌ1眲名準備

~/dev/auraly-ios/tools/setup_signing.py が担う眲名の自動化は、蚌明曞の突合から始たりたす。

LOCAL_SHA1 = "EC06777A693874E920CECFE390D467670552CCCE".lower()

def find_cert():
    _, d = asc.call("GET", "/v1/certificates?limit=200")
    for c in d.get("data", []):
        content = c["attributes"].get("certificateContent")
        if not content:
            continue
        der = base64.b64decode(content)
        sha1 = hashlib.sha1(der).hexdigest()
        if sha1 == LOCAL_SHA1:
            return c["id"]

/v1/certificates が返す certificateContent は Base64 ゚ンコヌドの DER です。これを SHA1 でフィンガヌプリントし、ロヌカルの Keychain にある Distribution 蚌明曞ず照合したす。名前怜玢ではなくフィンガヌプリント照合にしおいるのは、「Apple Distribution: ...耇数」のように名前が重耇するケヌスを確実に回避するためです。

䞀臎する蚌明曞が芋぀かったら、App IDbundleIdを確認たたは䜜成し、AppStore タむプのプロビゞョニングプロファむルを生成したす。生成されたプロファむルは ~/Library/MobileDevice/Provisioning Profiles/{uuid}.mobileprovision ぞ曞き蟌たれ、xcodebuild がそのたた認識できる状態になりたす。既存の同名プロファむルは事前に DELETE しおから䜜り盎す蚭蚈で、蚌明曞ロヌテヌション埌に叀いプロファむルが残っおビルドが倱敗する問題を防いでいたす。

レむダヌ2ビルドアヌカむブ

~/dev/auraly-ios/tools/archive.sh の栞心郚は17行です。

xcodegen generate
rm -rf build/Auraly.xcarchive build/export

xcodebuild archive \
  -project Auraly.xcodeproj \
  -scheme Auraly \
  -configuration Release \
  -archivePath build/Auraly.xcarchive \
  -destination 'generic/platform=iOS' \
  CODE_SIGN_STYLE=Manual \
  CODE_SIGN_IDENTITY="Apple Distribution" \
  PROVISIONING_PROFILE_SPECIFIER="Auraly AppStore" \
  -allowProvisioningUpdates

xcodebuild -exportArchive \
  -archivePath build/Auraly.xcarchive \
  -exportOptionsPlist ExportOptions.plist \
  -exportPath build/export

CODE_SIGN_STYLE=Manual ず PROVISIONING_PROFILE_SPECIFIER="Auraly AppStore" の組み合わせで、Xcode の自動眲名を完党に無効化し、盎前のレむダヌで生成したプロファむルを名前で指定しお䜿いたす。-allowProvisioningUpdates は残しおありたすが、プロファむルが既にロヌカルに眮かれおいるため実際にはApple サヌバヌぞの問い合わせは起きたせん。

xcodegen generate を毎回先頭で実行しおいるのは意図的です。project.yml を唯䞀の真実の情報源ずし、.xcodeproj の手動線集差分がビルドに玛れ蟌たないようにするためです。

レむダヌ3バむナリ投入

生成された .ipa を App Store Connect ぞ届けるのは eas submit か altool です。このレむダヌはスクリプト制埡の察象倖で、凊理完了processingState: VALIDたで非同期に埅ちたす。asc.py の _build_for_version() が、指定バヌゞョンで VALID になったビルドが存圚するかどうかをポヌリングする圹割を担いたす。

def _build_for_version(app_id, version_string):
    _, b = call("GET", f"/v1/builds?filter[app]={app_id}&limit=20&sort=-uploadedDate"
                       f"&include=preReleaseVersion")
    incl = {i["id"]: i for i in b.get("included", []) if i["type"] == "preReleaseVersions"}
    for x in b.get("data", []):
        if x["attributes"].get("processingState") != "VALID":
            continue
        pr = x.get("relationships", {}).get("preReleaseVersion", {}).get("data")
        ver = incl.get(pr["id"], {}).get("attributes", {}).get("version") if pr else None
        if ver == version_string:
            return x["id"], x["attributes"].get("version")
    return None

processingState が VALID 以倖PROCESSING や FAILEDのビルドは添付察象倖ずしお読み飛ばしたす。include=preReleaseVersion で marketing version を䞀発で取埗し、バヌゞョン文字列で絞り蟌むこずで、耇数ビルドが存圚する状況でも意図したバヌゞョンだけを掎めたす。

レむダヌ4メタデヌタ審査提出

asc.py submit() が螏む手順は3ステップです。

def submit(app_id, platform="IOS"):
    # 1) reviewSubmission を䜜成既存 in-progress があれば再利甚
    _, rs = call("GET",
        f"/v1/reviewSubmissions?filter[app]={app_id}&filter[state]=READY_FOR_REVIEW&limit=1")
    sub = (rs.get("data") or [None])[0]
    if not sub:
        st, r = call("POST", "/v1/reviewSubmissions",
            {"data": {"type": "reviewSubmissions", "attributes": {"platform": platform},
                      "relationships": {"app": {"data": {"type": "apps", "id": app_id}}}}})
        sub = r["data"]
    sid = sub["id"]
    # 2) バヌゞョンを item ずしお远加
    ver = _latest_version(app_id); vid = ver["id"]
    st, r = call("POST", "/v1/reviewSubmissionItems",
        {"data": {"type": "reviewSubmissionItems",
                  "relationships": {
                      "reviewSubmission": {"data": {"type": "reviewSubmissions", "id": sid}},
                      "appStoreVersion": {"data": {"type": "appStoreVersions", "id": vid}}}}})
    # 3) submitted=true で提出
    st, r = call("PATCH", f"/v1/reviewSubmissions/{sid}",
        {"data": {"type": "reviewSubmissions", "id": sid,
                  "attributes": {"submitted": True}}})

旧来の /v1/apps/{id}/appStoreVersions/{vid}/... 系゚ンドポむントではなく、reviewSubmissions / reviewSubmissionItems の新系゚ンドポむントを䜿っおいたす。Apple はドキュメントでこちらを掚奚しおおり、旧系は段階的に廃止される方向です。3ステップのうち最初の「既存チェック」を省くず、同じアプリぞの二重送信で゚ラヌになるため、GET で状態確認しおから POST するのがポむントです。

スクショ重耇の dedup は submit の盎前に必ず実行したす。deliver や fastlane でスクリヌンショットを同期し盎すず、前回分ず今回分が同䞀ファむルのたた2枚远加されるこずがありたす。dedup_screenshots() は sourceFileChecksum ず fileName の組み合わせをキヌずし、2枚目以降を DELETE したす。apply=False で dry-run、--apply フラグで実際に削陀するため、誀削陀リスクを事前確認で排陀できたす。


ここたでが「動くパむプラむン」の党䜓像です。次章からは、このパむプラむンを12アプリぞ本番投入しお実際に螏んだリゞェクト理由を、゚ラヌコヌドず修正コヌドを添えお解説したす。

実装の詳现

reject() ― 「差し戻し」なしには次の提出ができない

12本を䞊走させるず、審査䞭に軜埮なバグを発芋しお「今すぐ盎したい」ずいう状況が必ず発生したす。WAITING_FOR_REVIEW や IN_REVIEW 状態のアプリに make_version() や attach_build() を投げおも、ASC API は無蚀で 422 を返すだけです。線集可胜な状態ぞ戻すために必芁なのが reject() です。

def reject(app_id):
    _, rs = call("GET", f"/v1/reviewSubmissions?filter[app]={app_id}&limit=10")
    done = False
    for x in rs.get("data", []):
        state = x["attributes"].get("state")
        if state in ("WAITING_FOR_REVIEW", "IN_REVIEW", "UNRESOLVED_ISSUES"):
            st, r = call("PATCH", f"/v1/reviewSubmissions/{x['id']}",
                {"data": {"type": "reviewSubmissions", "id": x["id"],
                          "attributes": {"canceled": True}}})
            print("  reject", x["id"], state, "->", st if st < 400 else json.dumps(r)[:400])
            done = True
    if not done:
        print("  審査䞭の submission 無し")

canceled: True を PATCH するだけで審査が取り䞋がり、バヌゞョンが DEVELOPER_REJECTED に遷移したす。_EDITABLE にはこの状態が含たれおいるため、盎埌に make_version() → attach_build() → submit() の流れぞ戻れたす。UNRESOLVED_ISSUES を察象に含めおいるのは、App Review から远蚘質問が来おいる状態でも同じコマンドで取り䞋げられるようにするためです。

Developer Rejection はペナルティなしの操䜜です。審査履歎には蚘録されたすが、審査回数のカりントには響きたせん。むしろ䞍備のあるバむナリを通そうずしお REJECTED を食らうほうが、次サむクルたでの埅機時間が䌞びたす。「盎したくなったら即 reject → 修正 → 再 submit」がサむクルコストを最小化する正しい動き方です。

release() ― 3ステップをひず぀の匕数で動かすオヌケストレヌタヌ

def release(app_id, version, whatsnew, platform="IOS"):
    print("release", app_id, version)
    vid = make_version(app_id, version, platform)
    if not vid: return
    attach_build(app_id, vid, version)
    set_whatsnew(app_id, whatsnew, vid)
    print("  done. 確認埌に `submit` を実行。")

release() は make_version → attach_build → set_whatsnew を盎列に束ねる薄いオヌケストレヌタヌです。意図的に submit() は含めおいたせん。ビルドが添付されおメタデヌタが揃った段階でいったん止たり、status コマンドで確認しおから submit を投げる蚭蚈です。党自動にするずミスを発芋する間もなく審査ぞ流れおしたうため、最埌の 1 コマンドだけは意図的に分離したした。

Claude Code からはこの 3 行で完結したす。

python3 ~/.appstoreconnect/asc.py release 6782624281 1.4.0 "バグ修正ず安定性の向䞊"
python3 ~/.appstoreconnect/asc.py dedup 6782624281 --apply
python3 ~/.appstoreconnect/asc.py submit 6782624281

「バヌゞョン準備 → スクショ重耇陀去 → 審査提出」が 3 行です。これを 12 アプリ分䞊列実行しおも、ASC API のレヌトリミット1 分あたり 1000 リク゚スト皋床が目安に匕っかかる前にさばき切れたす。

add_tester() ― 「グルヌプ経由で POST」が唯䞀の正解

def add_tester(app_id, email=DEFAULT_TESTER_EMAIL, first="Lily", last="Tester"):
    if _has_tester(app_id, email):
        print(f"  {email} は既にテスタヌ(skip)"); return
    _, g = call("GET", f"/v1/apps/{app_id}/betaGroups?limit=50")
    internal = next((x for x in g.get("data", []) if x["attributes"].get("isInternalGroup")), None)
    if not internal:
        st, r = call("POST", "/v1/betaGroups",
            {"data": {"type": "betaGroups",
                      "attributes": {"name": "Internal", "isInternalGroup": True},
                      "relationships": {"app": {"data": {"type": "apps", "id": app_id}}}}})
        if st >= 400:
            print("  create internal group failed", st, json.dumps(r)[:600]); return
        internal = r["data"]; print("  created internal group", internal["id"])
    gid = internal["id"]
    st, r = call("POST", "/v1/betaTesters",
        {"data": {"type": "betaTesters",
                  "attributes": {"email": email, "firstName": first, "lastName": last},
                  "relationships": {"betaGroups": {"data": [{"type": "betaGroups", "id": gid}]}}}})
    if st >= 400 and not _has_tester(app_id, email):
        print("  add tester failed", st, json.dumps(r)[:600]); return
    print(f"  テスタヌ {email} を内郚グルヌプ {gid} に远加")

コメントに曞いおある通り、betaGroups/{id}/relationships/betaTesters ぞの盎接 POST は 409 STATE_ERROR"Tester cannot be assigned"で匟かれたす。テスタヌを远加するずきは /v1/betaTesters ぞの POST に betaGroups リレヌションシップを含めお 「テスタヌ䜜成ず同時にグルヌプぞ玐づける」 䞀発オペレヌションが唯䞀の正解です。「グルヌプを䜜っおからテスタヌを远加する」ずいう盎感的な手順を螏むず、延々ず 409 が出るだけです。

内郚テスタヌは Beta App Review 䞍芁でむンストヌルできるため、審査提出埌すぐ自分の iCloud でむンストヌル確認できたす。add_tester を提出埌に呌ぶのはパむプラむンの定番ルヌティンになりたした。


私が詰たった話

12本を本番投入するたでの間に、3皮類のリゞェクトがパむプラむンを止めたした。順番に「症状 → 原因 → 盎し方」を曞きたす。

眠①ITMS-90111 の真因は「beta macOS でビルドした」だった

症状。 archive.sh を実行しお .ipa を App Store Connect ぞアップロヌドするず、凊理完了埌のメヌルに ITMS-90111 が届きたした。

ERROR ITMS-90111: "Invalid Binary.
The value for key BuildMachineOSBuild in the Info.plist
file at Payload/Auraly.app/Info.plist is not valid."

App Store Connect の web 画面にはバむナリが「無効」ず衚瀺され、_build_for_version() がい぀たで経っおも processingState == "VALID" のビルドを返さない状態になりたす。再アップロヌドしおも同じ。asc.py status で build 欄を確認するず FAILED ず出るだけです。

原因。 xcodebuild はアヌカむブ実行時に、ビルドを走らせた macOS のバヌゞョンを BuildMachineOSBuild キヌずしお Info.plist ぞ自動曞き蟌みしたす。archive.sh のコマンド自䜓は䜕も悪くありたせん。

xcodebuild archive \
  -project Auraly.xcodeproj \
  -scheme Auraly \
  -configuration Release \
  -archivePath build/Auraly.xcarchive \
  -destination 'generic/platform=iOS' \
  CODE_SIGN_STYLE=Manual \
  CODE_SIGN_IDENTITY="Apple Distribution" \
  PROVISIONING_PROFILE_SPECIFIER="Auraly AppStore" \
  -allowProvisioningUpdates

問題はこのコマンドを実行した macOS が beta 版だったこずです。macOS 15 Sequoia の GA リリヌス前に betaビルド番号 25A5xxx 系で動かしおいたため、beta を瀺す文字列が BuildMachineOSBuild に埋め蟌たれ、Apple のバリデヌタヌが匟きたした。xcodebuild に BuildMachineOSBuild を䞊曞きするオプションは存圚せず、ビルドマシンの OS 自䜓を正匏リリヌス版に戻すしかありたせん。

盎し方。 macOS を beta から正匏リリヌス版ぞダりングレヌドしお再アヌカむブしたした。ダりングレヌドが難しい環境では、GitHub Actions の macos-latest ランナヌ垞に GA 版でアヌカむブするのが珟実的です。以降は「開発䜜業は beta OK・アヌカむブだけは GA 専甚マシン」ずいう運甚に切り替えたした。このルヌルを忘れお再び螏たないよう、archive.sh の先頭に sw_vers -buildVersion の出力を衚瀺する行を加えおありたす。

眠②appPriceSchedules は「included ず null の startDate」が正解

症状。 新芏アプリを無料Freeで登録しようず䟡栌蚭定゚ンドポむントぞ POST したずころ、422 を返し続けたした。

422 {"errors":[{"status":"422","code":"INVALID_ENTITY",
"detail":"The provided entity includes invalid relationship data."}]}

゚ラヌメッセヌゞが汎甚すぎお䜕が間違っおいるかわかりたせん。Apple のドキュメントには「manualPrices を relationships に枡せ」ず曞いおあるだけで、具䜓的なボディ構造の䟋が少ない。最初に詊したのはこの圢でした。

{
  "data": {
    "type": "appPriceSchedules",
    "attributes": {},
    "relationships": {
      "app": {"data": {"type": "apps", "id": "<app_id>"}},
      "manualPrices": {"data": [{"type": "appPrices", "id": "p1"}]}
    }
  }
}

原因。 manualPrices は むンラむン resourceincludedを䜿う圢匏です。id: "p1" で参照しおいるにも関わらず、その p1 の実䜓を included 配列に入れおいないため、「参照先のデヌタが存圚しない」ずしお 422 になりたす。さらに萜ずし穎がもうひず぀あり、「即時有効」を意味する startDate は空文字列 "" ではなく JSON の null でなければなりたせん。"" を入れるず別の 422 が返りたす。

盎し方。 included に appPrices オブゞェクトを明瀺的に含め、startDate を null にしたす。

{
  "data": {
    "type": "appPriceSchedules",
    "attributes": {},
    "relationships": {
      "app": {"data": {"type": "apps", "id": "<app_id>"}},
      "manualPrices": {"data": [{"type": "appPrices", "id": "p1"}]}
    }
  },
  "included": [
    {
      "type": "appPrices",
      "id": "p1",
      "attributes": {
        "startDate": null,
        "customerPrice": "0"
      },
      "relationships": {
        "territory": {"data": {"type": "territories", "id": "JPN"}}
      }
    }
  ]
}

ただし、175 か囜すべおの included を列挙するのは実甚的ではありたせん。最終的に asc.py ぞ䟡栌蚭定関数を組み蟌むのはやめ、䟡栌は最初に App Store Connect の web UI で蚭定しおおき、API はメタデヌタずバむナリ操䜜に専念する蚭蚈に萜ち着きたした。再提出のたびに䟡栌が䞊曞きされるリスクを排陀できる点でも、この分担が理にかなっおいたす。

眠③en-US ロケヌルの名前衝突

症状。 set_whatsnew() を実行するず、特定のアプリだけ 409 が返っお曎新が止たりたした。

whatsNew en-US -> 409 {"errors":[{"status":"409","code":"STATE_ERROR",...}]}

前埌で ja や zh-Hans などほかのロケヌルは正垞に曎新されおいるため、スクリプト自䜓は壊れおいたせん。en-US だけが毎回倱敗する状態です。

原因。 set_whatsnew() は ASC API が返す localization リストを党件 PATCH したす。

_, loc = call("GET", f"/v1/appStoreVersions/{vid}/appStoreVersionLocalizations")
for x in loc.get("data", []):
    lid = x["id"]; locale = x["attributes"].get("locale")
    st, r = call("PATCH", f"/v1/appStoreVersionLocalizations/{lid}",
        {"data": {"type": "appStoreVersionLocalizations", "id": lid,
                  "attributes": {"whatsNew": text}}})
    print("  whatsNew", locale, "->", st if st < 400 else json.dumps(r)[:300])

問題はそのアプリのバヌゞョンに en-US ず en の䞡方が存圚しおいたこずです。App Store Connect の web UI で英語名を en で䜜成した埌、deliver や Fastlane で en-US のメタデヌタを远加するず二重登録が発生したす。App Store は en ず en-US を別゚ンティティずしお管理するため、重耇状態は蚱容されたすが、䞀方が「プラむマリロケヌル」ずしお蚭定されおいるず、もう䞀方ぞの PATCH が 409 を返したす。

盎し方。 応急凊眮ずしお、locale の蚀語コヌド- 以前を芋お同じ基底蚀語が既に凊理枈みならスキップする絞り蟌みを远加したした。

processed_base = set()
for x in loc.get("data", []):
    locale = x["attributes"].get("locale", "")
    base = locale.split("-")[0]   # "en-US" → "en"
    if base in processed_base:
        print(f"  skip {locale} (already processed base {base})")
        continue
    processed_base.add(base)
    lid = x["id"]
    st, r = call("PATCH", f"/v1/appStoreVersionLocalizations/{lid}",
        {"data": {"type": "appStoreVersionLocalizations", "id": lid,
                  "attributes": {"whatsNew": text}}})
    print("  whatsNew", locale, "->", st if st < 400 else json.dumps(r)[:300])

根本解決は App Store Connect の web UI で重耇ロケヌルを削陀し en-US に䞀本化するこずです。スクリプト偎のスキップはあくたで応急凊眮であり、プラむマリが en のアプリず en-US のアプリが混圚する堎合はスキップロゞックが逆効果になる可胜性がありたす。asc.py apps でアプリ䞀芧を取埗した埌、各アプリのロケヌル状態を定期確認する運甚を加えるこずで、再発を防いでいたす。


次節では、12本を同時回転させたずきのレヌトリミット察策ず、Claude Code が自埋で「リゞェクト → バグ修正 → 再提出」を刀断する゚ヌゞェントルヌプの蚭蚈を解説したす。

぀たずきポむント実䟋ず実コヌドで網矅

前章の3倧リゞェクトITMS-90111 / appPriceSchedules / en-US衝突を陀いた、実装ず運甚で螏んだ现かな萜ずし穎を䞀気に列挙したす。レヌトリミット察策もここで扱いたす。

① JWT の iat を −30 秒にする意味を消しおはいけない

~/.appstoreconnect/asc.py の _jwt() はこう曞いおありたす。

p = _b64(json.dumps({"iss":ISSUER,"iat":int(time.time())-30,"exp":int(time.time())+900,
                     "aud":"appstoreconnect-v1"},separators=(",",":")).encode())

iat を -30 しおいるのは、Apple のサヌバヌずのクロックドリフト察策です。Mac の時刻が NTP からわずかにずれた状態で JWT を発行するず、Apple 偎が「未来の発行時刻だ」ず刀断しお 401 を返したす。ビルドサヌバヌで自動実行するほど発生頻床が䞊がるため、この -30 は絶察に倖しおはいけたせん。exp の 900 秒15 分も Apple が芁求する䞊限倀です。900 を超える倀を入れるず即 401 になりたす。

② /v1/certificates?limit=200 のペヌゞネヌション未実装

~/dev/auraly-ios/tools/setup_signing.py の find_cert() は次のように曞いおありたす。

LOCAL_SHA1 = "EC06777A693874E920CECFE390D467670552CCCE".lower()

def find_cert():
    _, d = asc.call("GET", "/v1/certificates?limit=200")
    for c in d.get("data", []):
        content = c["attributes"].get("certificateContent")
        if not content:
            continue
        der = base64.b64decode(content)
        sha1 = hashlib.sha1(der).hexdigest()
        if sha1 == LOCAL_SHA1:
            return c["id"]
    print("ERROR: no ASC cert matches local SHA1", LOCAL_SHA1)
    sys.exit(1)

ASC API のペヌゞネヌションcursor パラメヌタは実装しおいたせん。蚌明曞の総数が 201 本以䞊あるず、察象の蚌明曞が 201 番目以降に萜ちた瞬間に sys.exit(1) で止たりたす。個人アカりントで 12 本皋床なら珟実的には起きたせんが、チヌムアカりントや長幎䜿っおいるアカりントでは芁泚意です。定期的に倱効枈み蚌明曞を削陀するか、cursor ベヌスの党件舐めを远加するこずで防げたす。

③ ensure_profile() は「同名のプロファむルを党お消す」

for p in d.get("data", []):
    if p["attributes"].get("name") == PROFILE_NAME:
        asc.call("DELETE", f"/v1/profiles/{p['id']}")
        print("deleted stale profile", p["id"])

PROFILE_NAME = "Auraly AppStore" に完党䞀臎するプロファむルをすべお DELETE しおから新芏䜜成したす。耇数アプリで同じプロファむル名を䜿い回しおいるず、他アプリのプロファむルがたずめお消えお次のビルドが通らなくなりたす。"{アプリ名} AppStore" のようにアプリごずにナニヌクな呜名を培底するこずで防げたす。

④ _latest_version() は最新 1 件を盲目的に返す

def _latest_version(app_id):
    _, v = call("GET", f"/v1/apps/{app_id}/appStoreVersions?limit=1")
    return v["data"][0] if v.get("data") else None

゜ヌト条件の明瀺がないため、API のデフォルト䜜成日降順で先頭 1 件が返りたす。「1.3.0審査䞭」ず「1.4.0線集䞭」が同時に存圚するずき、submit() は 1.4.0 の ID を掎んで提出しようずしたす。12 本を䞊走させるずこの二重バヌゞョン状態が意倖ず長く残りたす。操䜜前に asc.py status <app_id> を実行しお状態を確認する習慣が必芁です。

â‘€ processingState: FAILED のビルドはスクリプトが黙っお無芖する

for x in b.get("data", []):
    if x["attributes"].get("processingState") != "VALID":
        continue

VALID 以倖をスキップするだけで、FAILED を怜知しお報告する凊理は入っおいたせん。Apple がバむナリを受け取ったものの凊理に倱敗した堎合、_build_for_version() は䜕もないかのように振る舞いたす。Claude Code に「VALID になるたで埅お」ず指瀺するず、FAILED のたた氞遠にポヌリングしたす。タむムアりト30 分皋床を蚭けお超過したら通知しお停止する仕組みが必芁です。

⑥ dedup の重耇刀定は sourceFileChecksum + fileName の耇合キヌ

key = (sh["attributes"].get("sourceFileChecksum"), sh["attributes"].get("fileName"))
if key in seen:

同じ画像でもファむル名が倉われば別キヌずみなされたす。Fastlane のメタデヌタフォルダで screen_01.png → screen_1.png のようにリネヌムした堎合、同䞀画像でも重耇ずしお怜出されたせん。--apply を実行する前に匕数なしdry-runで重耇件数を確認し、予想倖の件数なら手䜜業で照合したす。

⑩ set_whatsnew() は党ロケヌルぞ同䞀テキストを流す

for x in loc.get("data", []):
    lid = x["id"]; locale = x["attributes"].get("locale")
    st, r = call("PATCH", f"/v1/appStoreVersionLocalizations/{lid}",
        {"data": {"type": "appStoreVersionLocalizations", "id": lid,
                  "attributes": {"whatsNew": text}}})

枡した text をロケヌル問わず党件に PATCH したす。英語・䞭囜語・韓囜語のロケヌルに日本語の曎新情報がそのたた入りたす。英語圏のナヌザヌレビュヌに「曎新情報が読めない」ず曞かれるより前に察凊が必芁です。ロケヌル別察応は {"ja": "...", "en-US": "..."} を受け取れるよう set_whatsnew() を拡匵するか、Claude Code に蚀語ごずの翻蚳を生成させた蟞曞を枡す蚭蚈が珟実的です。

⑧ whatsNew フィヌルドの䞊限は 4000 文字

App Store Connect の API 仕様䞊限です。超えるず 422 が返りたす。Claude Code に「曎新情報を自動生成しお submit たで流しお」ず指瀺するず、長文を生成しおそのたた枡しやすい状況が生たれたす。呌び出し偎で whatsnew = whatsnew[:4000] ずクリップしおおくか、プロンプトに「4000 文字以内でたずめる」ず明蚘しおおくこずで防げたす。

⑹ IN_REVIEW 状態では党 PATCH が 422 になる

asc.py の _EDITABLE は次の 5 状態しか含みたせん。

_EDITABLE = {"PREPARE_FOR_SUBMISSION", "DEVELOPER_REJECTED", "REJECTED",
            "METADATA_REJECTED", "INVALID_BINARY"}

WAITING_FOR_REVIEW ず IN_REVIEW はこのセットの倖です。審査䞭にメタデヌタを倉えようずしおも API は無蚀で 422 を返すだけです。reject() で取り䞋げおから線集する必芁がありたす。12 本同時運甚だずどれかが垞に審査䞭になり、「なぜこのアプリだけ曎新できないのか」ず混乱しやすい状態になりたす。

⑩ 12 本同時提出でレヌトリミット429に圓たる

call() 関数には retry / backoff が実装されおいたせん。

try:
    r = urllib.request.urlopen(req); raw = r.read()
    return r.status, (json.loads(raw) if raw else None)
except urllib.error.HTTPError as e:
    return e.code, json.loads(e.read() or b"{}")

12 本分を䞀斉に dedup → submit するルヌプを組むず、数分で 1000 リク゚スト/分目安の制限に近づきたす。429 が返っおも゚ラヌを䞊䜍に返すだけで再詊行しないため、ルヌプ埌半のアプリが黙っお倱敗したす。各 call() の埌に time.sleep(0.1) を挟む最䜎限の察策か、429 を怜知したら指数バックオフで再詊行するラッパヌを远加するこずで安定したす。

⑪ eas submit / altool の完了は asc.py の倖

バむナリのアップロヌドは eas submit たたは altool が担い、asc.py はその完了を感知できたせん。アップロヌドから Apple サヌバヌ偎の VALID 刀定たで 3〜10 分かかりたす。この埅ち時間を考慮せずに release → submit を連続実行するず、「VALID build が芋぀からない」のたた空振りしたす。asc.py status でビルド欄を確認しおから release を呌ぶか、_build_for_version() を䜿ったポヌリングをラッパヌずしお挟みたす。

⑫ archive.sh の set -euo pipefail は意図した蚭蚈だが芋萜ずしやすい

set -euo pipefail
cd "$(dirname "$0")/.."

xcodegen generate の倱敗でも xcodebuild archive の倱敗でも即座にスクリプト党䜓が停止したす。Claude Code が archive.sh を実行したずき、終了コヌド 1 のログを芋萜ずすず「倱敗した」ず気づかない堎合がありたす。archive.sh の末尟に echo "ARCHIVE_SUCCESS: $ROOT/build/export/Auraly.ipa" を確認マヌカヌずしお远加し、Claude Code ぞの指瀺に「このメッセヌゞが出なければ倱敗ずしお報告する」を明蚘しおおくず誀認識を防げたす。


ベストプラクティス

実装ず倱敗の埀埩から蒞留した 12 本の原則です。

1. アヌカむブは GA macOS 専甚マシンで行う

beta macOS でビルドするず BuildMachineOSBuild に beta 文字列が埋め蟌たれ ITMS-90111 で匟かれたす。開発は beta 環境で行っおも、archive.sh を実行するマシンは正匏リリヌス版の macOS だけに限りたす。GitHub Actions の macos-latest ランナヌは垞に GA 版が䜿われるため、CI でアヌカむブする蚭蚈が最も安党です。

2. archive.sh の先頭に OS ビルド番号を出力する

echo "macOS build: $(sw_vers -buildVersion)"

CI ログにビルド番号が残るため、ITMS-90111 が再発したずき「beta でビルドした」ず即座に特定できたす。24G85 は GA、25A5xxx 系は beta ずいうビルド番号のパタヌンを芚えおおくだけで蚺断速床が䞊がりたす。

3. 䟡栌蚭定は Web UI で初回のみ行い API に任せない

appPriceSchedules を API で操䜜するには 175 か囜分の included が必芁で、実甚的ではありたせん。最初の 1 回だけ Web UI で「無料」たたは任意のティアを蚭定し、以降の提出では觊れない蚭蚈が正解です。再提出のたびに䟡栌が䞊曞きされるリスクもれロになりたす。

4. submit の盎前に必ず dedup を実行する

python3 ~/.appstoreconnect/asc.py dedup <app_id> --apply
python3 ~/.appstoreconnect/asc.py submit <app_id>

この順番を厩さないこずを Claude Code ぞの指瀺テンプレヌトに埋め蟌みたす。順番が逆になるず、重耇スクショがそのたた審査ぞ進みたす。

5. ロケヌルは en-US に䞀本化し en を残さない

既存の en ロケヌルは Web UI から削陀し en-US のみにしたす。スクリプト偎の「基底蚀語重耇スキップ」は応急凊眮であり、根本解決はロケヌルの䞀本化です。プラむマリロケヌルが en のアプリず en-US のアプリが混圚するず、スキップロゞックが逆効果になるケヌスもありたす。

6. status を「提出前」ず「提出埌 24 時間」の 2 点で確認する

提出前バヌゞョン状態・VALID ビルドの有無を確認。提出埌 24 時間reviewSubmission.state が COMPLETE に倉わっおいるかを確認したす。Claude Code にこのチェックをルヌティンずしお組み蟌むず、承認ずリゞェクトの怜知が自動化できたす。

7. Developer Rejection はペナルティなし・気づいたら即 reject する

審査䞭に䞍具合を発芋したら reject() → 修正 → submit を即座に回したす。「審査に通るか詊しおみる」姿勢のほうが、リゞェクト埅機時間で損をしたす。Developer Rejection は審査カりントに圱響したせん。

8. _EDITABLE 状態を操䜜前に確認する

status コマンドで appStoreState が _EDITABLE の 5 状態にあるこずを確認しおから release を実行したす。IN_REVIEW 䞭に操䜜しおも 422 が返るだけで副䜜甚はありたせんが、時間ず API コヌルを無駄にしたす。

9. プロファむル名はアプリ固有にする

"{アプリ名} AppStore" 圢匏で呜名し、耇数アプリが同名にならないようにしたす。ensure_profile() は同名を党削陀するため、呜名が被るず他アプリのプロファむルが消えお次のビルドが倱敗したす。

10. call() に 429 察応の指数バックオフを远加する

def call_with_retry(method, path, body=None, max_retry=3):
    for attempt in range(max_retry):
        st, data = call(method, path, body)
        if st == 429:
            time.sleep(2 ** attempt)
            continue
        return st, data
    return st, data

12 本同時提出のルヌプが安定したす。珟状の call() はそのたた残し、レヌトリミットが気になる操䜜だけラッパヌ経由で呌ぶ圢で段階的に差し替えられたす。

11. VALID 埅ちにタむムアりトを蚭ける

Claude Code に「processingState: VALID になるたで埅お」ず指瀺するずき、必ず「30 分経っおも VALID でなければ FAILED 扱いで通知しお停止する」条件を添えたす。タむムアりトなしのポヌリングは、深倜の自埋実行で詰たったたた気づかない原因になりたす。

12. whatsNew は呌び出し前に 4000 文字でクリップする

whatsnew = generated_text[:4000]

Claude Code が生成した曎新情報を無怜査で枡すず䞊限超過で 422 が出たす。クリップを呌び出し偎の責任にしおおくず、set_whatsnew() 本䜓を汚さずに枈みたす。


たずめ

前線・埌線を通じお、~/.appstoreconnect/asc.py272 行・archive.sh24 行・setup_signing.py72 行の 3 スクリプトで、iOS アプリ 12 本の審査提出が完党に無人化できるこずを瀺したした。

審査を止めた眠は、Apple サヌバヌ固有の非自明な挙動ITMS-90111 の真因・appPriceSchedules の included 圢匏・en ず en-US の二重登録ず、実装の小さな前提ペヌゞネヌション・呜名衝突・ロケヌル統䞀・タむムアりト・レヌトリミットの組み合わせでした。どれも実際に 12 本を䞊走させながら螏み抜いたものです。ドキュメントを読むだけでは予防できたせん。

月商 120 䞇の実態は「䜜業量を増やした」のではなく、「同じ 3 スクリプトが 12 アプリ分䞊列で動いおいる」だけです。パむプラむンが正しく組たれおいれば、アプリ ID を 1 行远加するだけでスケヌルしたす。ここたで曞いおきた 3 ぀のスクリプトが、その土台になっおいたす。

次回は Claude Code が「リゞェクト怜知 → バグ修正 → 再提出」を自埋で刀断する゚ヌゞェントルヌプの蚭蚈を解説したす。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす

📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

皆さんの ❀ やシェアが励みになりたす