見出し画像

CodexとClaude Codeで作ったiOS開発ハーネスの設計書——3+N構成からApp Store公開まで

iOSアプリのコードだけなら、一つのリポジトリでも開発できます。

ところがApp Storeで公開しようとすると、Xcodeでのbuildと署名だけでは終わりません。TestFlight、App Store Connect、Privacy Policy、問い合わせ窓口までつながってきます。

AIへ実装を任せる範囲が広がるほど、「どこまで進めてよいか」「何を人が確認したか」も曖昧にできません。

最初の記事では、CodexとClaude Codeを使って「星環庭園」を開発し、この環境が広がっていった経緯を書きました。

CodexとClaude Codeで初のiOSアプリを公開——コードの外側まで作り込んだら、iOS開発ハーネスになった

このページでは経緯を繰り返さず、現在動いている仕組みを設計としてまとめます。

対象は、一人で複数のiOSアプリを継続して作る環境です。変更管理の中心はGitHub。物理Macは一台で、Xcodeが必要な処理だけをMacへ寄せています。大人数の組織や24時間稼働を想定したものではありません。

設計は六つの問いから組み立てた

機能を足す前に、次の六つへ答えられることを基準にしました。

  1. どのリポジトリが、どの情報の正本を持つのか

  2. Codex、Claude Code、GitHub Actions、Mac、人は何を担当するのか

  3. どのcommit、build、実機QA、審査情報が同じreleaseに属するのか

  4. 何を並列に進め、何を一つずつ実行するのか

  5. 外部サービスへの書き込み結果が分からないとき、どこから再開するのか

  6. 会話の履歴が短くなっても、現在地へ戻れるか

全体像は次のようになります。

```mermaid
graph TD;
    H["開発者"] --> G["Governance Hub"];
    G --> A["iOS App"];
    G --> W["公開Web"];
    G --> S["問い合わせ基盤"];
    A --> M["Macでbuild・test・署名"];
    M --> TF["TestFlight"];
    TF --> ASC["App Store Connect"];
    W --> WEB["Privacy・利用規約・Support"];
    S --> FORM["Forms・Sheets・Apps Script"];
```

中央のGovernance Hubが、ほかのリポジトリを勝手に書き換えるわけではありません。各リポジトリの所在と責任を台帳で結び、作業に必要なルールと検証を渡します。変更そのものは、それぞれのリポジトリでPull Requestとして管理します。

3+Nで、共有部分とアプリを分ける

全アプリで共有する責任を三つのリポジトリへ置き、製品コードだけをアプリごとに分けました。アプリがN個なら、全体は3+Nリポジトリです。

四つのリポジトリ責任

Governance Hubが持つのは、共通ルール、Schema、アプリ台帳、検証、進行状態、操作手順です。製品コードやAppleの秘密情報は持ちません。

iOS Appリポジトリには、Swiftコード、テスト、製品仕様、データ利用の事実、App Store情報、release設定を置きます。別のアプリのrelease履歴は入りません。

Shared Public Webでは、Privacy Policy、利用規約、Supportページをアプリ別に公開します。Shared Support Automationでは、Google Forms、Sheets、Apps Scriptの定義を管理します。利用者がフォームへ入力した回答本文は、GitにもAIにも取り込みません。

この四つは、同じアプリに関係していても変更理由が異なります。

  • アプリの挙動を変えるなら、iOS Appリポジトリを変更する

  • 共通ルールや検査を変えるなら、Governance Hubを変更する

  • 公開文書やSupportページを変えるなら、Shared Public Webを変更する

  • 問い合わせフォームの項目や処理を変えるなら、Shared Support Automationを変更する

複数の責任へ影響する変更でも、一つのリポジトリが別のリポジトリを直接書き換えません。
変更先ごとにfeature branchとPull Requestを作り、前提となるPull Requestのmergeを確認してから後続を進めます。

アプリ追加時に増えるもの

新しいアプリを増やすとき、物理的に追加するのは原則としてiOS Appリポジトリ一つです。三つの共有側には、そのアプリの論理登録だけを足します。

具体的には、Governance Hubの台帳へアプリを登録し、公開Webと問い合わせ基盤へ同じproject IDを持つ論理projectを追加します。
TestFlightへ進むアプリだけ、GitHubの非秘密設定とMac上のReleaseRunner identityをアプリ単位で用意します。
まだ試作段階なら、Appleへ接続せず、buildと検証だけを先に整えられます。

この分け方にしたのは、フォルダをきれいにするためではありません。

アプリへSDKを追加した場合、データ利用の事実はiOS App側で変わります。その差分を起点に公開文書への影響を調べ、Shared Public Web側へ更新案を出す。問い合わせ項目を変えるなら、Shared Support Automation側の変更として扱う。複数リポジトリにまたがっても、どこを直すかが分かれます。

正本、途中状態、秘密情報を混ぜない

同じフォルダへ何でも保存すると、残すべき履歴と、残してはいけない情報の境界が崩れます。そこで情報を四種類に分けました。

機械が判定する正本はYAMLやJSONへ置きます。共通ルール、Schema、アプリ台帳などです。

人が読む理由、判断基準、操作手順はMarkdownへ置きます。Codex用とClaude Code用の指示、検証用manifest、templateは、この正本から生成します。生成後のファイルだけを書き換えても、CIが正本との差を検知します。

作業途中の計画、外部サービスから読み戻した値、App Review準備の監査結果は、実行時データ(runtime)へ置きます。再開には使いますが、共通ルールを更新する入口にはしません。今回の失敗を次回のルールへ反映するときも、通常のfeature branchとPull Requestを通します。

Appleの鍵、証明書、token、審査連絡先、問い合わせ回答は、このどこにも入りません。AIへ渡さず、Gitにも保存しません。

情報がどこからどこへ流れるかを図にすると、次のようになります。

```mermaid
graph TD;
    RULE["Rules・Schema・台帳"] --> GEN["決定的に生成"];
    GEN --> DIST["Skills・Workflow・template"];
    DIST --> CI["CIがparityとdriftを検査"];
    FACT["アプリの実装事実"] --> AUDIT["Store・Privacyへの影響を監査"];
    AUDIT --> PR["文書・metadataの変更PR"];
    RUN["runtime・readback・途中証跡"] --> RESUME["現在地を復元"];
    SECRET["Secret・個人情報"] --> OUT["GitとAIの外で保管"];
```

生成物は、AIがその場で考えて作るのではなく、正本から同じ入力で再生成できるものに限定します。
runtimeは再開に使いますが、そこへ書かれた一時的な観測結果が共通ルールを自動変更することはありません。
運用上の気づきをルールへ反映するときは、通常の変更と同じくPull Requestで内容を確認します。

一つのreleaseも、versionやbuild番号だけでは識別しません。

```mermaid
graph TD;
    SRC["repository・exact commit"] --> REL["version・build番号"];
    REL --> BIN["Archive・IPA digest"];
    BIN --> TF["TestFlight上のbuild"];
    TF --> QA["実機・OS・QA結果"];
    QA --> INFO["説明文・画像・申告のdigest"];
    INFO --> APPROVE["人の承認"];
    APPROVE --> SUBMIT["提出・審査結果"];
```

「build 80を確認した」だけでなく、どのcommitから作られ、どの端末とOSで確認し、どの説明文とスクリーンショットで提出するのかまで結びます。人の承認も同じ組へ固定し、後から作った別buildへ流用しません。

各段階は前の段階の識別情報を参照します。
TestFlightで処理が完了したこと、App Store versionでbuildを選んだこと、審査へ提出したこと、公開されたことは、それぞれ別の状態です。
後の状態だけを見て前の工程まで完了したと推測しません。

AIは次の承認地点まで進み、人は後戻りしにくい判断をする

CodexとClaude Codeは、調査からPull Requestの準備まで担当します。実装、テスト、差分確認、commit、pushもこの範囲です。GitHub Actionsは、検証対象を固定した同じcommitを、Secretなしで再検証します。

人が担当するのは、次の判断です。

  • 製品方針と実機QA

  • Pull Requestのmerge

  • Privacy Policyや利用規約の最終確認

  • 審査するbuildの選択

  • App Reviewへの提出とApp Storeでの公開

```mermaid
graph TD;
    AI["AIが実装・検証・PR準備"] --> CI["CIがexact commitを再検証"];
    CI --> MERGE["人がmerge"];
    MERGE --> MAC["Macでbuild・署名・許可済みupload"];
    MAC --> QA["人が実機QA"];
    QA --> PREP["提出情報と差分を準備"];
    PREP --> SUBMIT["人が審査へ提出"];
    SUBMIT --> RELEASE["人が公開"];
```

確認を細かな処理ごとに挟むと、本当に確認したい操作まで流れ作業になります。機械だけで確定できる区間は、次の承認地点までまとめて進める。後戻りしにくい操作の前で、対象、差分、検証結果をそろえて止める。この境界を先に決めました。

AIへ読ませるルールも、全量を常時読み込ませません。常時読むのは短い共通ルールと入口だけです。iOS実装やApp Reviewなどの作業へ入った時点で、作業別の手順(Skill)と正本ルールを追加で読みます。

同じ会話の前提に引っ張られて原因が見えないときは、別のAIへレビューを依頼できる経路も分けています。送るのは問題を説明できる最小限の抜粋だけ。返答はそのまま採用せず、実際のコード、公式資料、テスト、readbackで確かめます。

worktreeでコードを分け、外部書き込みは一つずつ進める

独立した変更は、feature branchごとの`git worktree`へ分けます。primary checkoutをcleanなmainとして残し、アプリの機能開発、共通基盤の修正、別アプリの作業が同じindexへ混ざらないようにしました。

実装後はローカル検証を行い、Pull Requestを作ります。CIが同じhead commitを検証し、成功したら人がmergeします。merge後はPull Request、merge commit、remote main、Actionsを照合し、安全に削除できるbranchとworktreeだけを整理します。

```mermaid
graph TD;
    MAIN["cleanなmain checkout"] --> WT1["worktree A: アプリ実装"];
    MAIN --> WT2["worktree B: Governance改善"];
    MAIN --> WT3["worktree C: 別アプリ"];
    WT1 --> PR1["個別のPR"];
    WT2 --> PR2["個別のPR"];
    WT3 --> PR3["個別のPR"];
    PR1 --> MERGE["人が順序を確認・merge"];
    PR2 --> MERGE;
    PR3 --> MERGE;
    MERGE --> WRITE["外部書き込みは対象単位で直列化"];
```

worktreeが分けるのはworking treeとindexです。
同じファイルを別worktreeで変更すればmerge conflictは起こり、同じruntimeやApp Store Connectの対象を同時に更新してよい根拠にもなりません。
sourceの分離、途中状態の排他、外部書き込みの直列化を別々の仕組みで扱います。

iOSの検証は、一つの「テスト成功」へまとめません。

  1. Windowsまたは共通runtimeで、Schema、生成物、ルール、静的検査を確認する

  2. Macで、Xcode build、unit test、必要なSimulator検証を行う

  3. TestFlightで、Appleが処理したbuildを実機へ配る

  4. 人が実機で、主要操作、表示、端末固有の挙動を確認する

一方、worktreeで分けられるのはGit上の作業です。同じApp Store version、同じスクリーンショット集合、同じrelease状態を複数処理が同時に変えれば、外部側で競合します。

独立したソース変更は並列化する。TestFlight upload、App Store metadata、スクリーンショット更新は、アプリ、version、locale、表示種別などの単位で直列化する。速く進める仕組みと、二重実行を防ぐ仕組みを分けています。

一台のMacでも、三つの実行境界へ分ける

物理Macは一台ですが、Runnerの役割は混ぜません。

開発用executorは、build、test、Simulator、画面確認を担当し、Appleへ書き込みません。

Governance検証用の境界は、固定runtimeとmacOS固有の検査を担当します。Appleの資格情報は使いません。

リリース専用Runner(ReleaseRunner)は、署名と、許可されたTestFlight uploadを担当します。アプリごとにRunnerのlabelと作業rootを分け、別のアプリへ承認を広げません。

```mermaid
graph TD;
    HOST["一台の物理Mac"] --> DEV["開発用: build・test・Simulator"];
    HOST --> GOV["Governance検証: 固定runtime・Mac固有検査"];
    HOST --> REL["ReleaseRunner: 署名・許可済みupload"];
    DEV --> EVIDENCE["Secretなしの検証証跡"];
    GOV --> EVIDENCE;
    VAULT["専用userのVault・Keychain"] --> REL;
    REL --> APPLE["Apple"];
```

三つは別の物理Macではなく、同じMac上の論理的な信頼境界です。
開発用処理やPull Request由来の未確認コードが、Apple資格情報へ到達しないように分けています。
ReleaseRunnerは、対象アプリ、exact main、許可された操作を確認してから資格情報を使います。

App Store Connectへ接続する情報はMac上の共有保管場所へ集約し、Apple Distribution identityは専用ユーザーのKeychainへ置きます。同じ秘密情報をアプリごとのGitHub Secretsへ複製せず、AI、チャット、コマンド引数、ログへ出さないことを優先しました。

一台なので、Macが止まればXcode、署名、TestFlight uploadも止まります。この構成は高可用性を目指していません。別の役割へ勝手に処理を迂回せず、GitHubと台帳から同じ役割を再構築できる手順を残します。

TestFlightから公開までを、状態として追う

配信工程は、一つのreleaseボタンではありません。

```mermaid
graph TD;
    A["mainへmerge"] --> B["buildを検証"];
    B --> C["TestFlightへupload"];
    C --> D["Appleの処理完了"];
    D --> E["人が実機QA"];
    E --> F["提出情報を準備"];
    F --> G["最終提出チェック"];
    G --> H["人がbuildを選択"];
    H --> I["人が審査へ提出"];
    I --> J["審査中"];
    J --> K["修正が必要"];
    K --> E;
    J --> L["承認"];
    L --> M["人が公開"];
```

内部TestFlightを有効にしたアプリでは、人がmergeしたexact mainから一度だけuploadを開始できます。同じ承認で別commitや別buildを送りません。

upload後はAppleのprocessing完了を確認します。uploadは成功し、その確認中に処理が切れた場合、同じbuildを再uploadする前にApp Store Connectの現在値を読み直します。

実機QAが終わったら、説明文、キーワード、公開URL、Review Notes、スクリーンショットの候補をまとめます。APIで取得できる値は希望値との差分を出し、取得できない項目は画面へたどる手順を示します。

初回のアプリ登録は、Apple公式資料を参照し、Apple公式Webを入口にします。Codexが画面入力を補助しても、登録内容を確定する前に人が確認します。登録後の日常的な情報取得とmetadata準備には、公開APIを使います。

build選択、審査への追加、提出、公開は人が行います。審査連絡先、ログイン情報、税務、銀行情報は自動監査の対象にしません。

App Store Connectの確認場所を、一枚の提出資料へまとめる

App Store Connectには、APIで取得・設定できる項目と、画面上で人が確認する項目があります。

説明文や公開URLなど、許可したメタデータは、固定した実行計画と承認の範囲で更新できます。一方、App PrivacyやDSAなど、Appleへの申告と配信条件には人の判断が残ります。価格と地域、年齢区分、アクセシビリティ表示、MacやVisionでの配信可否も同様です。

「画面で確認してください」だけでは、設定場所を探す作業が毎回発生します。そこで最終提出チェックシートを作り、App Store Connectの大まかなページ順に次の情報を縦に並べます。

```mermaid
graph TD;
    APP["アプリ情報: 名前・カテゴリ・権利"] --> DIST["価格と配信: 地域・Mac・Vision"];
    DIST --> VER["App Store version: 説明・URL・画像・build"];
    VER --> REVIEW["App Review情報: Notes・連絡先・添付"];
    REVIEW --> PRIVACY["Privacy・年齢区分・アクセシビリティ"];
    PRIVACY --> COMPLY["暗号化・DSA・コンテンツ権利"];
    COMPLY --> FINAL["最終提出チェック"];
```

実際の画面では設定が複数ページへ分かれています。
そのためチェックシートも、API項目だけを一覧にせず、App Store Connectで人がたどる順序へ並べ替えます。
同じページ内では、先に希望値と現在値を比較し、その後にAPIで確認できなかった項目を画面で確認します。

  • 項目の名前と意味

  • App Store Connect上のページ名

  • 画面へたどる順序

  • 希望値

  • APIから取得できた現在値

  • APIで確認できない理由

  • 画面で設定または確認する手順

  • 人が確認した証跡

空欄や`null`も、機械だけで合否を決めません。「申告書類が不要なので登録がない」のか、「まだ回答していない」のかは意味が異なるためです。

初回App Reviewで追加情報を求められた経験から、Review Notesには七つの観点を最初から用意します。実機での操作動画、確認した端末とOS、機能と対象者、主要機能への到達手順、外部サービス、地域差、規制産業や第三者素材の該当有無です。

該当しない項目も省略せず、「該当なし」と書きます。アカウント、課金、ユーザー投稿、機密性の高い権限がある場合は、審査担当者が必要な状態へ到達できる情報を追加します。

外部サービスへの書き込みは、現在値の読み戻しまでを一組にする

App Store Connect、Cloudflare、Googleへの複数操作は、データベースのような一括処理になりません。途中まで成功し、こちらが応答を受け取る前に通信が切れることがあります。

書き込みを次の三段階へ分けました。

```mermaid
graph TD;
    P["準備:値・digest・承認を固定"] --> A["実行:許可した差分だけ更新"];
    A --> R["readback:現在値を再取得"];
    R --> DONE["一致:完了証跡を残す"];
    R --> STOP["不一致・不明:再送せず停止"];
```

書き込み前に、対象、現在値、希望値、順序を固定します。画像などの成果物は、内容を識別するチェックサムも記録。実行後は外部サービスの現在値を読み直します。この読み戻しをreadbackと呼んでいます。

結果が分からないときは、失敗と決めつけて同じ操作を送りません。外部側では成功し、応答だけを受け取れなかった可能性があるからです。相手が明確に拒否し、変更前の状態も確認できた場合に限り、決めた範囲で再試行します。

スクリーンショット更新では、この仕組みが実際に必要になりました。五枚のうち二枚だけ処理されたところで通信が切れたため、更新前の画像を退避し、新しい画像と順序をチェックサムで固定。Apple側の現在値を読み直し、処理済みの二枚を残して不足分だけを送りました。

Apple側の処理を、本当の一括処理へ変えたわけではありません。準備、実行、読み戻しを一組にし、部分成功を捨てずに希望状態へ収束させる擬似トランザクションです。

「続きをお願い」は、会話ではなく保存した状態から再開する

複数のリポジトリを長く扱うと、「前回どこまで終わったか」を探す時間が増えます。AIの会話履歴だけへ現在地を置くと、コンテキストが整理された後に同じ操作を繰り返す危険もあります。

再開に必要な状態は、少なくとも次の単位で保存します。

  • 現在の工程と対象リポジトリ

  • branch、exact commit、検証済みartifactのdigest

  • 完了した人間確認と、次に待っている承認

  • 外部書き込みのoperation identityとreadback結果

  • 未確定の前提、その確認担当、停止理由

  • 証拠から導ける次の安全な操作

そこで、確認する目的ごとに表示を分けました。全体の段階、対象アプリの現在値と停止理由、次の安全な一手、不整合の有無、人の判断が必要になる直前までの進行を、それぞれ別に確認できます。

機械が読む証跡はYAML、人が読む結論はMarkdownへ残します。Git、Pull Request、CI、remote main、外部サービスのreadbackを照合すれば、会話の記憶がなくても現在地を復元できます。

外部状態が一部進んだ場合、いつも変更前へ戻せるとは限りません。すでに採番されたbuildや公開済みURLは消せないからです。

何も変わっていないのか、一部だけ成功したのか、同じ対象として続けられるのかを確認します。元へ戻すより希望状態へ進める方が安全なら、重複を避けながら先へ進めます。どちらも安全に判断できなければ、人へ渡して止まります。

この設計で増やしたかったもの

この環境で増やしたかったのは、自動化の量ではありません。

AIが迷わず進める範囲と、止まった後に同じ指示を繰り返さず再開できる範囲です。そのために、正本、exact commit、人の承認、外部サービスのreadbackをつなぎました。

現在の実績は、「星環庭園」をTestFlight、App Review、App Store公開まで進めたところまでです。物理Macは一台で、Apple、GitHub、Cloudflare、Googleの画面やAPIが変われば手順も見直します。

星環庭園には、アカウント、アプリ内課金、ユーザー投稿がありません。これらを含むアプリでは、認証、購入復元、通報とブロック、データ削除などの設計が別に必要です。この構成だけで審査通過や無停止運用を保証するものでもありません。

今回の構成のより詳細な記事は、有償での公開を予定しています。この設計を自分の環境へ組み直すための設定値と手順を示します。設定値の置き換え、Rules・Skills・Schema・Workflowの対応、初期セットアップを扱う予定です。Macの構築と復旧、公開前チェックも含めます。

構成を手元で確認するための参考実装も付属します。ただし、そのまま導入できる完成品ではありません。自分のGitHub、Mac、Apple Developer、App Store Connectへ合わせて変更するための構成例です。

参考資料

※ Apple、GitHub、各サービスの仕様は変わることがあります。
この記事は2026年8月時点の環境と、星環庭園の開発から公開までに確認した内容をもとにしています。

いいなと思ったら応援しよう!