同じiOSプロジェクトから開発、ステージング、本番の各サービスへ接続する場合、最も危険なのはコンパイルエラーではなく、誤った環境のままアーカイブに成功してしまうことです。開発用URL、詳細ログのフラグ、社内向け機能の設定がリリースビルドに混入しても、パイプラインは正常終了を示す可能性があります。より確実な方法は、公開してよい設定を階層化し、ビルドの入口を固定したうえで、誰かがXcodeで正しいメニューを選ぶことに頼らず、アーカイブの検査をリリース工程に組み込むことです。
設定とシークレットの境界を明確にする
xcconfigは、APIのベースURL、Bundle IDのサフィックス、ログレベル、機能フラグのデフォルト値など、クライアントとともに公開されても問題のないビルド設定の保存に適しています。ただし、シークレットを保護する金庫ではありません。クライアントのビルド工程に渡した値は、Info.plist、リソースファイル、コンパイル引数、実行ファイルなどに含まれる可能性があります。
判断基準は単純です。ユーザーがインストールパッケージを入手した後に知られてはならない値を、XcodeでAppへ組み込んではいけません。
サーバー管理キー、署名サービスの認証情報、書き込み権限を持つトークンは、サーバー側または管理されたCIの認証情報ストアに置きます。クライアントには、有効期間が短く、権限が限定され、失効可能な認可結果だけを渡します。CIが環境変数でシークレットを注入しても、最終成果物を解析できるという事実は変わりません。
まず設定項目の一覧を作成し、各項目について、公開可能かどうか、値の取得元、アーカイブ後に存在する想定箇所を記録します。チームがレビューすべきなのは、Build Settingsに散在する数十個の値ではなく、この一覧です。
レビュー可能なxcconfigの階層を作る
共通設定、環境ごとの差分、ローカルの上書きを分離します。
Config/
Base.xcconfig
Development.xcconfig
Staging.xcconfig
Production.xcconfig
LocalOverrides.xcconfig.example
Base.xcconfigには、すべての環境で共通する値だけを記述します。各環境のファイルでは、最初にこのファイルをインクルードし、少数の差分だけを上書きします。
#include "Base.xcconfig"
APP_ENVIRONMENT = staging
API_BASE_URL = https:/$()/staging-api.invalid
ENABLE_VERBOSE_LOGGING = YES
PRODUCT_BUNDLE_IDENTIFIER = com.example.product.staging
ここで使っている$()は、URL内の二重スラッシュがコメントとして解釈されるのを防ぐためのものです。例示したドメインは、名前解決できない文書用の値です。実際のプロジェクトでは、自分のサービスURLに置き換えてください。
ローカル上書き用ファイルはリポジトリへコミットしません。初回チェックアウト時の手間を減らすには、任意インクルードを利用できます。
#include? "LocalOverrides.xcconfig"
ただし、ローカル上書きは開発時の利便性のためだけに使用し、正式なアーカイブの暗黙的な入力にしてはいけません。本番設定は、このファイルがなくても完全に解決できる必要があります。Development、Staging、Releaseの3つのConfigurationをそれぞれ対応するファイルに割り当て、CIで使用するSchemeがSharedに設定されていることを確認します。
コマンドラインのビルド入口を固定する
クラウドMac上のアーカイブジョブが、以前のGUI操作で残った状態を引き継いではいけません。workspace、scheme、configuration、出力先のパスをすべて明示的に渡します。
set -euo pipefail
WORKSPACE="App.xcworkspace"
SCHEME="App"
CONFIGURATION="Release"
ARCHIVE_PATH="$PWD/build/App.xcarchive"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-showBuildSettings > build-settings.txt
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$ARCHIVE_PATH" \
clean archive
アーカイブ前に解決済み設定を検証する
ソースファイルだけを確認するのではなく、Xcodeが最終的に解決した設定を検査します。build-settings.txtから重要な項目を抽出し、本番環境の設定に対して厳密なアサーションを実行します。
grep -E "APP_ENVIRONMENT|API_BASE_URL|PRODUCT_BUNDLE_IDENTIFIER|ENABLE_VERBOSE_LOGGING" \
build-settings.txt
grep -q "APP_ENVIRONMENT = production" build-settings.txt
grep -q "ENABLE_VERBOSE_LOGGING = NO" build-settings.txt
複数のTargetを使用するプロジェクトでは、Targetごとに個別に検査してください。拡張機能、テストバンドル、メインAppが、それぞれ異なるBase Configurationを継承している可能性があるためです。差異が発生したときに状況を再現できるよう、スクリプトでは現在のGitコミット、Xcodeのバージョン、使用したSchemeも出力します。
実際に配布するアーカイブをスキャンする
Build Settingsが正しくても、成果物まで正しいとは限りません。Run Script、生成コード、リソースのコピー処理によって、テスト用設定がアーカイブへ混入する可能性があります。アーカイブ完了後は、少なくともメインAppのプロパティリスト、埋め込みリソース、実行ファイル内の文字列を検査します。
APP_PATH="$(find build/App.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"
plutil -p "$APP_PATH/Info.plist"
if grep -RInaE "staging-api|debug-token|INTERNAL_ONLY" "$APP_PATH"; then
echo "forbidden marker found in archive" >&2
exit 1
fi
EXECUTABLE_NAME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$APP_PATH/Info.plist")
if strings "$APP_PATH/$EXECUTABLE_NAME" | grep -E "staging-api|debug-token|INTERNAL_ONLY"; then
echo "forbidden marker found in executable" >&2
exit 1
fi
スキャン語句にはチームで定義したマーカーを使い、実際のシークレットをスクリプトやログへ直接記述しないでください。一般的な単語による誤検知を避けるため、「出現を許可する値」と「出現を禁止する値」の2つの短いリストを管理することを推奨します。スキャン結果にはファイルパスとルール名だけを残し、一致した機密内容全体は出力しません。
| 検査対象 | 確認する内容 | 失敗時の処理 |
|---|---|---|
| 最終的なBuild Settings | 環境名、Bundle ID、ログフラグ | アーカイブを直ちに停止 |
| Info.plist | サービスURL、URL Scheme、環境ラベル | エクスポートを阻止 |
| Appのリソースディレクトリ | デバッグ設定、テストデータ、一時ファイル | 混入元を削除して再ビルド |
| 実行ファイル | 内部URLの特徴、トークンのマーカー | 生成工程を特定し、関連する認証情報をローテーション |
よくある設定ドリフトと誤解に対処する
最もよくある設定ドリフトの原因は、「Schemeがローカルには存在するが共有されていない」ことです。対応するxcshareddataがリポジトリにない場合、CIがSchemeを見つけられなかったり、担当者が一時的に別の入口へ切り替えたりする可能性があります。もう一つの問題は、Configurationを唯一の情報源にせず、スクリプト内でも環境名による分岐を行うことです。時間がたつにつれて、環境を判断するロジックが二重に存在する状態になります。
変数を難読化、分割、またはバイナリへ埋め込めば安全になるという考えも誤りです。クライアント内の長期シークレットを確実に隠す方法はありません。過去のアーカイブへの混入が判明した場合は、まず認証情報を失効またはローテーションし、その後でビルド設定を修正します。現在のブランチから文字列を削除するだけでは、すでに配布されたビルドのリスクは解消できません。
この処理をMangoVMの固定物理ノードで実行する場合は、Runnerがクリーンな作業ディレクトリを使用するようにしてください。前回のジョブで生成されたDerivedData、一時設定、エクスポートディレクトリを再利用してはいけません。並行するジョブ同士がファイルを削除しないよう、クリーンアップ対象は今回のワークスペース内に限定します。
検査をマージとリリースのゲートにする
最終的には、処理を2つの段階に分けられます。マージリクエストの段階では設定を解決し、設定ファイルの構造を検査します。リリース段階では完全なアーカイブを作成し、成果物をスキャンします。前者は迅速なフィードバックを提供し、後者は実際に配布する成果物を基準に検証します。環境に関するアサーションが1つでも失敗した場合は、ログに警告を表示するだけでなく、必ず非ゼロのステータスを返します。
リリース前には、次のチェックリストで最終確認します。
- Schemeが共有され、Configurationとxcconfigファイルの対応関係がバージョン管理されている;
- 本番アーカイブでworkspace、scheme、configurationが明示的に指定されている;
showBuildSettingsの環境名、Bundle ID、ログフラグが想定どおりである;- メインAppとすべての拡張機能に対して、プロパティリストと文字列のスキャンが完了している;
- CIログに変数の完全な値が出力されていない;
- ジョブ終了時に一時上書きファイルが削除される;
- 漏えいが判明した場合は、先に認証情報をローテーションし、その後で再度アーカイブと検査を行う。
設定の取得元、解決済み設定、最終成果物という3層の証拠がそろえば、環境の選択が個人の操作習慣に左右されることはなくなります。リリースの問題は、ユーザーに発見されるのではなく、アーカイブがパイプラインを離れる前に検出されます。
よくある質問
APIの秘密鍵をxcconfigに保存してもよいですか?
保存すべきではありません。設定値はInfo.plist、コンパイル引数、リソース、実行ファイルへ入る可能性があります。配布するアーカイブは公開情報として扱い、権限を持つ秘密鍵はサーバー側に置きます。
ローカルとCIで異なる環境が選ばれるのはなぜですか?
Schemeが共有されていない、Configurationの対応が違う、対話Shellだけに変数がある、といった原因が一般的です。workspace、scheme、configurationを明示し、showBuildSettingsの結果を比較します。
開発パイプライン向けに固定クラウドMacを構成
MangoVMでは、M4とM4 Proの2種類のApple Silicon専有物理ノードを提供しています。利用期間、リージョン、ストレージの追加オプションを選択すると、注文内容をすべて確認できます。