提出ボタンを押した数時間後に、App Store Connect から「ITMS-91053: Missing API declarations」とだけ書かれたメールが届いた朝がありました。壁紙アプリの小さな更新で、前の版はそのまま通っていたものです。私は個人で壁紙アプリと癒し系の音源アプリを長く運用しておりますが、2024 年に Privacy Manifest が必須化されてから、この種の差し戻しに当たる回数が一気に増えました。
ここで一番こたえたのは「自分が書いたコードのせいではない」ことでした。Rork や Expo が読み込むネイティブライブラリの奥深く、自分が触ったことのない SDK の中で UserDefaults が呼ばれていて、その宣言が一枚抜けているだけで提出全体が止まってしまいます。Apple の自動チェックは機械的で、しかも理由が表に出てくるまでに数時間かかります。事後対応ばかりしていると、月のリリース計画がじわじわ崩れていくのです。
この記事は、その「事後対応モード」から「事前監査モード」へ移行するための実用手順をまとめたものです。Rork で生成した Expo / React Native アプリを前提に、提出 1 週間前の半日〜1 日で SDK 連鎖を全数洗うフローを示します。読み終わったあとに、npm ls と find だけで実用的な監査スクリプトを 1 本作れるところまで持っていきます。
なぜ「リジェクト後対応」では遅いか
Required Reason API の事後対応は、見かけより重い作業です。1 回目のリジェクトを受けてから対応すると、最低でも次のサイクルが回ります。
まずリジェクトメールが届くまでに自動審査で 2〜6 時間。原因 SDK を突き止めるのに 1〜3 時間。PrivacyInfo.xcprivacy の編集と EAS Build のやり直しに 30 分〜2 時間。再アップロードとプロセスが終わるまでにさらに 1 時間。順調にいって半日、悪くて 1.5 日が消えます。
私の場合、2024 年の春にこれを 3 回続けてやりました。AdMob を入れる前は通っていたビルドが、AdMob を足したとたんに UserDefaults 系の Required Reason 違反で返ってきます。RevenueCat を追加すると、今度は NSPrivacyAccessedAPICategoryDiskSpace の宣言が要ります。SDK を 1 個入れるたびに 1 回差し戻され、リリース予定がまるごと 1 週間後ろへずれました。
事前監査に切り替えてから、私の個人開発のリリースサイクルでは ITMS-91053 のリジェクトはほぼゼロになりました。事前監査が「審査運に賭ける作業」を「再現性のある工程」に変えてくれたわけです。
SDK 連鎖の正体を最初に把握する
Privacy Manifest は階層構造を持ちます。アプリ本体の PrivacyInfo.xcprivacy 1 枚で済むケースはほぼなく、ネイティブモジュールごとに同じファイルが必要になります。Apple の判定はビルドされた .ipa の中身全体に対して行われるため、依存 SDK のどこか 1 枚でも未宣言だと全部落ちます。
Rork が生成する典型的な Expo アプリの構造は、おおよそ次の 3 階層になっています。
アプリ本体(app/ 配下と app.json から生成される iOS プロジェクト)
Expo SDK(expo-* 系モジュール、約 20〜40 個)
サードパーティ React Native ライブラリ(広告・課金・分析・ストレージ・通知系、5〜15 個)
このうちアプリ本体は自分で PrivacyInfo.xcprivacy を置けば終わりですが、残りの 2 階層は SDK 提供元が同梱しているかどうかに依存します。2026 年 5 月時点で、Expo SDK 53 系・54 系はほぼ全モジュールが対応済みですが、サードパーティはまだムラがあります。
特に注意すべきは、Expo の Config Plugin で動的にネイティブコードを差し込むタイプのライブラリです。Plugin 経由で追加されるネイティブ参照は EAS Build のたびに生成されるため、ローカルの ios/ フォルダを grep しても見えないことがあります。私自身、expo-notifications の Plugin が出力するファイルに PrivacyInfo.xcprivacy が含まれていないことに気づくのに 1 日かかった経験があります。
1 日で監査するスケジュール(半日コースと終日コース)
「半日コース」は新規追加 SDK が 1〜2 個のとき向けです。「終日コース」はメジャー Expo SDK アップグレード後・Rork の AI 提案で大幅に依存ツリーが変わったとき向けです。私は壁紙アプリ系を更新するときは半日コース、癒し系アプリで OS の年次アップデートに追従するときは終日コースを使っています。
半日コース(3 時間想定)の構成はこうです。
0:00〜0:30 — npm ls --production --depth=5 で依存ツリーを取得し、PrivacyInfo.xcprivacy の検索スクリプトを流します
0:30〜1:30 — 未同梱 SDK をリスト化し、最新版へのアップデートで解決するかを確かめます
1:30〜2:30 — PrivacyInfo.xcprivacy を自前でマージ・追記し、EAS Build をローカルで検証します
2:30〜3:00 — xcrun altool --validate-app 相当の自動チェックで .ipa 全体を検証します
終日コース(7〜8 時間想定)は、上記に加えて以下を入れます。
Expo Config Plugin が出力するファイルの追跡(EAS Build のログを読みます)
Apple の Required Reason API ドキュメントとの差分チェック
Privacy Nutrition Label(App Privacy)と Manifest の整合性チェック
ここからは半日コースの中身を、Rork で生成した実プロジェクトに当てはめながら順に展開していきます。
ステップ 1:依存ツリーを正確に取り出す
最初にやるのは「アプリのビルドに含まれる SDK を全部リストにする」ことです。Rork のチャットに依頼してもいいのですが、ここはローカルで完結する方が再現性があります。
# Rork が生成したプロジェクトのルートで実行
npm ls --production --depth=5 --json > /tmp/deps-tree.json
# ネイティブ実装を含む可能性が高いパッケージだけ抽出
jq -r '
[.. | objects | select(.dependencies != null)
| .dependencies | keys[]] | unique[]
' /tmp/deps-tree.json \
| grep -E "^(react-native-|expo-|@react-native-|@expo/|@stripe/|@sentry/|firebase|@react-native-firebase|react-native-google-mobile-ads|react-native-mmkv|react-native-purchases)" \
> /tmp/native-deps.txt
wc -l /tmp/native-deps.txt
--depth=5 にしているのは、Expo SDK の中の依存(expo-modules-core など)が 2〜3 階層深いところに居ることが多いからです。--depth=Infinity だとファイルが大きすぎて後段の処理が遅くなります。
私の壁紙アプリ(依存約 22 個)の場合、上記スクリプトで 40〜55 行ほどが返ってきます。Rork が AI でコードを書き換えるたびにこの数字が変動するので、リリース直前にもう一度走らせると差分が見えます。これが「監査の出発点」になります。
ステップ 2:PrivacyInfo.xcprivacy の同梱状況を一括チェック
依存パッケージごとに PrivacyInfo.xcprivacy が同梱されているかを確認します。素直にやると 1 個ずつ find する地獄になるので、私は以下のシェルスクリプトを _tools/audit-privacy-manifest.sh として置いています。
#!/usr/bin/env bash
# audit-privacy-manifest.sh — 依存 SDK の PrivacyInfo.xcprivacy 同梱状況を一括チェック
set -euo pipefail
NATIVE_DEPS = " ${1 :-/ tmp / native-deps . txt } "
REPORT = "/tmp/privacy-audit-$( date +%Y%m%d-%H%M).tsv"
printf "package\tstatus\tpath\n" > " $REPORT "
while IFS = read -r pkg ; do
base = "node_modules/ $pkg "
if [ ! -d " $base " ]; then
printf "%s\tmissing-dir\t-\n" " $pkg " >> " $REPORT "
continue
fi
found = $( find " $base " -name "PrivacyInfo.xcprivacy" 2> /dev/null | head -1 || true )
if [ -n " $found " ]; then
printf "%s\thas-manifest\t%s\n" " $pkg " " $found " >> " $REPORT "
else
printf "%s\tNO-MANIFEST\t-\n" " $pkg " >> " $REPORT "
fi
done < " $NATIVE_DEPS "
echo "report: $REPORT "
column -t -s $' \t ' " $REPORT " | head -30
実行すると、NO-MANIFEST の行が「今すぐ対応が必要な SDK」、has-manifest の行が「同梱済み・追加対応不要」になります。私の最新の壁紙アプリでは、依存 48 個のうち 6 個が NO-MANIFEST で、そのうち 3 個は最新版で対応済みだったので npm update で解消、残り 3 個を手動対応する流れでした。
ステップ 3:未同梱 SDK の判定フロー
NO-MANIFEST の行が出てきても、慌てる必要はありません。次の順番でチェックすれば、ほとんどは数十分で片づきます。
第一段階は「最新版で同梱されているか」です。npm view <package> versions で過去バージョンを並べて、npm view <package@latest> files で同梱ファイルを確認します。同梱されていれば npm install <package>@latest で解決です。私の経験では、2024 年中盤までにメジャーリリースした SDK の 80% は、2025 年中に Privacy Manifest 同梱版を出していました。
第二段階は「GitHub の Issues で対応状況を見る」です。https://github.com/<org>/<repo>/issues?q=PrivacyInfo でほぼ確実にスレッドが見つかります。マージ済みなら次のリリースを待つか、patch-package で当てる選択になります。
第三段階は「自前で同梱する」です。ライブラリ作者が放棄している場合や、社内 fork している場合の最終手段で、postinstall スクリプトで node_modules/<package>/ios/PrivacyInfo.xcprivacy をコピーします。実例として、私が古めの React Native アニメーションライブラリで使っているスニペットを置きます。
// scripts/inject-privacy-manifest.js
// SDK 提供元が同梱していない場合の最終手段
const fs = require ( "fs" );
const path = require ( "path" );
const targets = [
{
pkg: "react-native-some-old-lib" ,
iosDir: "ios" ,
manifest: "manifests/some-old-lib.xcprivacy" ,
},
];
for ( const t of targets) {
const dest = path. join ( "node_modules" , t.pkg, t.iosDir, "PrivacyInfo.xcprivacy" );
if (fs. existsSync (dest)) continue ;
fs. mkdirSync (path. dirname (dest), { recursive: true });
fs. copyFileSync (t.manifest, dest);
console. log ( `injected: ${ dest }` );
}
package.json の scripts.postinstall に node scripts/inject-privacy-manifest.js を追加しておけば、npm install の度に自動で復元されます。patch-package でも同じことができますが、私はファイル単位で完結するこちらのほうが追跡しやすいと感じています。
ステップ 4:頻出 SDK の対応状況早見表(2026 年 5 月時点)
私が壁紙系・癒し系・引き寄せ系のアプリで実際に使っている SDK の現状をまとめておきます。バージョンは記事公開時点の最新を想定しています。リリース直前は必ず最新の npm view <pkg>@latest を見るようにしてください。
react-native-google-mobile-ads(AdMob)— 13.x 系で同梱。UserDefaults: CA92.1 + SystemBootTime: 35F9.1 を要求。AdMob 単体で Manifest 対応が一番厄介な部類で、12.x 以下は要手動対応
react-native-purchases(RevenueCat)— 9.x 系で同梱。DiskSpace: E174.1 と UserDefaults: CA92.1。アプリ側の Privacy Nutrition Label と必ず突き合わせます
@sentry/react-native — 6.x 系で同梱。FileTimestamp: C617.1 と UserDefaults: CA92.1 が標準
@react-native-firebase/app 系 — 22.x 系以降で同梱、Auth/Analytics/Messaging/Crashlytics サブパッケージそれぞれにファイルあり
react-native-mmkv — 3.x 系で同梱、UserDefaults: CA92.1
@react-native-async-storage/async-storage — 2.x 系で同梱、UserDefaults: CA92.1
expo-notifications / expo-file-system / expo-secure-store — Expo SDK 53 以降で同梱、追加宣言不要
react-native-mixpanel — 系列によって状況がバラつくため、最新版同梱なら採用、そうでなければ純正の Mixpanel iOS SDK を直接組み込む方が早い
ここに載っていない SDK は、最初に npm view <pkg> files | grep -i privacy をたたいて、含まれていれば即採用、なければ第三段階の自前同梱に進む、というルートで処理しています。
ステップ 5:アプリ本体側の PrivacyInfo.xcprivacy を整える
依存 SDK の整理が終わったら、自分のアプリ本体の PrivacyInfo.xcprivacy を最終確認します。Rork で生成した Expo アプリの場合、配置場所はやや特殊です。
通常の Expo: app/PrivacyInfo.xcprivacy(app.json の ios.privacyManifests から取り込まれる)
bare workflow: ios/<AppName>/PrivacyInfo.xcprivacy
Rork が生成したカスタム構成: app.json の expo.ios.privacyManifests を読みます
app.json を使う場合のテンプレートはこんな形になります。私は壁紙系アプリで以下のテンプレートをそのまま使っており、AdMob・RevenueCat・Sentry を入れた状態でも追加宣言なしで通っています。
{
"expo" : {
"ios" : {
"privacyManifests" : {
"NSPrivacyAccessedAPITypes" : [
{
"NSPrivacyAccessedAPIType" : "NSPrivacyAccessedAPICategoryUserDefaults" ,
"NSPrivacyAccessedAPITypeReasons" : [ "CA92.1" ]
},
{
"NSPrivacyAccessedAPIType" : "NSPrivacyAccessedAPICategoryFileTimestamp" ,
"NSPrivacyAccessedAPITypeReasons" : [ "C617.1" ]
},
{
"NSPrivacyAccessedAPIType" : "NSPrivacyAccessedAPICategoryDiskSpace" ,
"NSPrivacyAccessedAPITypeReasons" : [ "E174.1" ]
},
{
"NSPrivacyAccessedAPIType" : "NSPrivacyAccessedAPICategorySystemBootTime" ,
"NSPrivacyAccessedAPITypeReasons" : [ "35F9.1" ]
}
],
"NSPrivacyTracking" : false ,
"NSPrivacyTrackingDomains" : [],
"NSPrivacyCollectedDataTypes" : []
}
}
}
}
NSPrivacyTracking を false で出している点に注意してください。ATT で追跡しないアプリ(壁紙系の多くがこの設計)はこれで十分ですが、AdMob のターゲティング広告を使う場合は true に変えて NSPrivacyTrackingDomains を追加します。私の癒し系アプリでは ATT を出していないので false、Web 連携ありのアプリでは true という分け方をしています。
ステップ 6:EAS Build のローカル検証で「最後の一押し」を確かめる
PrivacyInfo.xcprivacy の整理ができたら、EAS Build を回す前にローカルでビルドして .ipa の中身を確認します。クラウドビルドで失敗するとフィードバックループが長いため、ここでミスを刈り取っておきます。
# ローカルビルド(Mac 必須)
npx expo prebuild --platform ios --clean
cd ios && pod install && cd ..
# Xcode でアーカイブし、ipa を取り出す
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath /tmp/MyApp.xcarchive \
-destination "generic/platform=iOS"
xcodebuild -exportArchive \
-archivePath /tmp/MyApp.xcarchive \
-exportPath /tmp/MyApp \
-exportOptionsPlist ios/export-options.plist
# ipa 内の PrivacyInfo.xcprivacy を全部列挙
cd /tmp/MyApp
unzip -o MyApp.ipa -d MyApp-unpacked > /dev/null
find MyApp-unpacked -name "PrivacyInfo.xcprivacy"
ここで列挙されるファイル数が、ステップ 2 の has-manifest 行数 + アプリ本体 1 個 と一致していれば概ね正解です。ズレている場合は、Pod が古い Privacy Manifest を上書きしているか、Expo の Config Plugin が後段で削っている可能性があります。
最後に xcrun altool --validate-app で App Store Connect 側の自動チェックを通します。
xcrun altool --validate-app \
--type ios \
--file /tmp/MyApp/MyApp.ipa \
--apiKey < YOUR_API_KE Y > \
--apiIssuer < YOUR_ISSUER_I D >
No errors が返れば、提出してほぼそのまま通ります。私の運用では、この validate-app を通したかどうかでリリース当日の落ち着きがまるで違うので、必ず一度は走らせることにしています。
ひとつ補足しておきます。altool は「非推奨になった」と聞いて避ける方がいらっしゃいますが、非推奨になったのは公証(notarization)の用途で、そちらは notarytool へ移行するよう案内されています。App Store Connect 向けの --validate-app / --upload-app は別の相手(App Store Connect)に話しかけるコマンドで、Apple のアップロード手順 でも Transporter と並んで案内されています。公証の話と混ぜて、検証工程ごと省いてしまわないようにしてください。
Privacy Manifest と Privacy Nutrition Label のズレを潰す
ここで一度、混同しがちな 2 つの仕組みを整理しておきます。Privacy Manifest(PrivacyInfo.xcprivacy)は「コードレベルで何を触っているか」を Apple に伝える機械可読の宣言で、Privacy Nutrition Label(App Store Connect 上の App Privacy)は「ユーザーにどう開示しているか」を人間に見せる表示です。両者は内容が一致しているのが前提で、ズレていると審査で個別問い合わせが来ます。
私が壁紙系アプリで実際にあったケースを 1 つ書きます。Manifest 側では NSPrivacyTracking: false で出していたのに、App Store Connect の App Privacy 側で「Identifiers — Device ID」が「Used for Tracking」にチェックされたまま残っていたことがあり、レビュー担当者から「Manifest と Label の整合性を確認してください」と連絡が来ました。回答自体は 5 分でできたものの、その 1 回のラリーで 24 時間消えました。
提出前のチェックリストとしては、最低この 3 行を回せれば十分です。
App Store Connect の「App Privacy」を開き、収集データ種別ごとに「Used for Tracking」「Linked to User」「Linked to Identity」の値が app.json の NSPrivacyTracking および NSPrivacyCollectedDataTypes と矛盾しないか
AdMob を入れているなら、IDFA を取得する設計か(取得するなら NSPrivacyTracking: true、取得しないなら false)
RevenueCat を入れているなら、ユーザー ID をどう紐づけているか(匿名なら Linked to User: false、メアド紐づけなら true)
私の運用では、app.json と App Privacy の対応関係を _documents/privacy-mapping.md に 1 枚の表で書き残しています。1 度書けば次のリリースでも 5 分で確認できる資産になります。
Required Reason API の理由コードは「業務動機」で選ぶ
PrivacyInfo.xcprivacy に書く理由コード(CA92.1 など)は、API のリストごとに Apple が許容している番号が決まっています。SDK 提供元が同梱しているマニフェストをそのままコピーすれば動きますが、自分のアプリ本体が「設定保存」目的で UserDefaults を呼ぶ場合は CA92.1、ユーザー解析目的で呼ぶ場合は C56D.1 のように、業務動機側の番号を入れる必要があります。
頻出する API ごとの、個人開発で使う頻度の高い理由コードだけまとめます。番号の正本は Apple Developer の Required Reason API ドキュメント です。追加や整理が入ることがあるため、リリース前には必ず公式で確かめてください。
NSPrivacyAccessedAPICategoryUserDefaults(UserDefaults)— アプリ内設定の保存は CA92.1、共通設定の参照は 1C8F.1、CloudKit 連携は AC6B.1
NSPrivacyAccessedAPICategoryFileTimestamp(ファイル日時)— 自アプリ内のキャッシュ・同期管理は C617.1、ユーザーが選んだファイルの作成日表示は 3B52.1
NSPrivacyAccessedAPICategorySystemBootTime(システム起動時間)— 起動からの経過秒で UI 更新を判定するなら 35F9.1、計測目的なら 8FFB.1
NSPrivacyAccessedAPICategoryDiskSpace(ディスク容量)— アプリ内のファイル保存可否判断は E174.1、ダウンロード前の容量確認は 85F4.1
私が壁紙アプリで 35F9.1 を入れ忘れて、AdMob 由来のリジェクトを 1 回食らったことがあります。AdMob 側の Manifest には 35F9.1 が同梱されていても、自アプリ本体側にも別の理由で SystemBootTime を読む箇所があり、そちらの宣言が漏れていたためでした。「アプリ本体は SDK と独立して理由を宣言する」という Apple のルールに沿って、_documents/required-reason-mapping.md のような対応表を持っておくと、新しい SDK を入れたときの判断ミスが減ります。
ITMS-91053 と ITMS-91061 は、直し方の違う別々の警告です
ここまでは ITMS-91053、つまり Required Reason API の宣言漏れの話でした。ところが提出メールにはもう一つ、番号のよく似た ITMS-91061 が並ぶことがあります。私は最初、この二つを同じ棚に入れて考えていて、PrivacyInfo.xcprivacy をいくら書き足しても消えない警告を前に、半日ほど手が止まりました。
二つは原因も、直す相手も違います。
ITMS-91061 の対象は、Apple が Third-party SDK requirements のページで名前を挙げている SDK です。ここに載っている SDK を含む新規アプリ、あるいは新しくその SDK を追加する更新を提出するときは、その SDK が privacy manifest を同梱していることが求められます。バイナリ依存として使う場合には署名も必要です。しかも一覧の SDK は、どのバージョンであっても、またそれを再パッケージしたライブラリであっても対象に含まれます。
ここが運用上いちばん効きます。ITMS-91053 は、気づいた日の夜に自分で片づけられます。けれども ITMS-91061 は、相手の次のリリースを待つ時間が読めません。91053 は自分の宿題、91061 は取引先への発注。 リリース 1 週間前に監査を置く理由の半分は、この待ち時間を発注として扱えるようにするためです。
npm の名前では届きません — Podfile.lock で Apple の一覧と突き合わせる
そしてもう一つ、私がしばらく気づかずにいた穴があります。ステップ 2 の監査スクリプトは node_modules を見ています。一方で Apple の一覧に並んでいるのは npm パッケージ名ではなく、Pod やフレームワークの名前です。この二つは、そもそも語彙が違います。
いちばん分かりやすい例が hermes です。React Native の JavaScript エンジンで、Expo の既定構成にそのまま入っています。npm の依存を何度眺めても hermes という行は出てきませんが、Apple の一覧には載っています。つまり Rork が生成した Expo アプリは、何も追加しない状態でも一覧の SDK を 1 つ積んでいるわけです。
Expo / React Native の構成で実際に顔を出しやすいものを、入ってくる経路と一緒に並べます。
照合の相手が違うと分かってしまえば、やることは単純です。ios/Podfile.lock の PODS: セクションから Pod 名だけを抜き、公式ページからコピーした一覧と突き合わせます。npx expo prebuild を一度走らせたあとであれば、Rork が生成した構成でも Podfile.lock は出来ています。
#!/usr/bin/env bash
# audit-apple-sdk-list.sh
# Apple が manifest と署名を求めている SDK の一覧と Podfile.lock を突き合わせる
set -euo pipefail
LOCK = " ${1 :- ios / Podfile . lock } "
LIST = " ${2 :- _tools / apple-sdk-list . txt } " # 公式ページからコピーした 1 行 1 名の一覧
# PODS: セクションから Pod 名だけを取り出す(サブスペックは親名に丸める)
awk '/^PODS:/{f=1;next} /^[A-Z]/{f=0} f && /^ - /{print $2}' " $LOCK " \
| tr -d '"' | sed 's|/.*||' | sort -u > /tmp/pods.txt
hit = 0
while IFS = read -r name ; do
[ -z " $name " ] && continue
# hermes-engine のような表記ゆれを拾うため部分一致・大文字小文字無視で照合
found = $( grep -i -- " $name " /tmp/pods.txt || true )
if [ -n " $found " ]; then
hit = $(( hit + 1 ))
printf "%-28s <= %s\n" " $name " "$( echo " $found " | paste -sd, -)"
fi
done < " $LIST "
echo "----"
echo "一覧に該当する Pod: ${ hit } 件"
出てきた Pod については、二つを確かめます。一つは PrivacyInfo.xcprivacy が入っているかどうかです。
# 該当 Pod ごとに manifest の有無を見る
for pod in hermes-engine FirebaseCore GoogleUtilities nanopb ; do
found = $( find "ios/Pods/ $pod " -name "PrivacyInfo.xcprivacy" 2> /dev/null | head -1 || true )
printf "%-20s %s\n" " $pod " "${ found :- NO-MANIFEST }"
done
もう一つは署名です。署名が求められるのはバイナリ依存として使う場合なので、ソースからビルドされる Pod は対象外になります。.xcframework や .framework の形で降ってくるものだけ、codesign で確かめれば足ります。
# 事前ビルド済みのフレームワークだけ拾って署名の有無を見る
find ios/Pods -name "*.xcframework" -maxdepth 3 | while read -r fw ; do
name = $( basename " $fw " )
if codesign -dv " $fw " > /dev/null 2>&1 ; then
printf "%-40s signed\n" " $name "
else
printf "%-40s UNSIGNED (提供元に確認)\n" " $name "
fi
done
私はこの audit-apple-sdk-list.sh を、先の audit-privacy-manifest.sh の直後に置いています。npm 側から見た穴と、Pod 側から見た穴は重ならないためです。突き合わせる相手は package.json ではなく Podfile.lock です。 この一行に気づくのが遅れたぶん、私は原因の分からない差し戻しを何度か引き受けることになりました。
なお、一覧そのものは Apple 側で更新されます。_tools/apple-sdk-list.txt をリポジトリに置いておき、四半期に一度だけ公式ページと見比べて差分を取り込む、という頻度で私は運用しております。
個人開発者として、この監査をどこまで「自動化」するか
ここまでの手順は半日で回せるとはいえ、毎リリースで手動でやるのは現実的ではありません。私は壁紙アプリと癒し系アプリの両方で、GitHub Actions の release-prep ワークフローに以下を組み込んでいます。
リリースタグを切る前に audit-privacy-manifest.sh を自動実行
NO-MANIFEST 行が 1 つでもあれば、Slack へ通知して人間の判断を待たせます
すべて has-manifest ならそのまま EAS Build へ進ませます
NO-MANIFEST が出たら、その時点で 30 分から 1 時間を確保して、ステップ 3 のフローを走らせます。ワークフローに組み込んでからは、ITMS-91053 で差し戻された記憶がありません。
ただ、ここまで書いておいて言うのも妙ですが、私はこの工程を完全な無人運転にはしていません。Apple のポリシーは年ごとに細かくなっており、機械に任せられるのは「一覧と突き合わせる」ところまでです。どの理由コードを名乗るかは、アプリが何のためにその API を触っているかという話で、書けるのは書いた本人だけなのだと思います。
照合は機械に、宣言の理由は自分の言葉で。 この線引きだけは、締め切りが迫っている日ほど守るようにしています。
次のリリースでは、audit-privacy-manifest.sh と audit-apple-sdk-list.sh のどちらか一方だけでかまいませんので、手元に置いてみていただければと思います。差し戻し 1 回分の時間で書けますし、以後のリリースの手触りが変わります。お読みいただきありがとうございました。