クラウドMac CIでGit Submoduleを再現可能に取得する

クラウドMac CIでGit Submoduleを再現可能に取得する

開発用Macでは問題なくクローンできるiOSプロジェクトでも、クラウドMac CIに移すとSubmoduleで処理が止まることがあります。メインリポジトリはチェックアウト済みなのにプライベートコンポーネントで権限エラーが発生する、ネストした依存関係が古いコミットのままになる、浅いクローンでは指定されたオブジェクトが見つからない、といったケースです。多くの場合、原因はXcodeではありません。チェックアウト処理で「メインリポジトリの現在のブランチ」と「メインリポジトリに記録されたSubmoduleのコミット」が明確に区別されていないことにあります。

まず再現可能性の範囲を明確にする

Git Submoduleでは、パス、リモートURL、厳密なコミットがメインリポジトリに保存されます。再現可能なビルドの目的は、メインリポジトリの同じコミットから常に同じSubmoduleオブジェクトを取得することです。実行のたびにSubmoduleブランチの最新状態を追跡することではありません。

まず開発環境で基準となる状態を記録します。

git submodule status --recursive
git config --file .gitmodules --get-regexp 'submodule\..*\.path'
git config --file .gitmodules --get-regexp 'submodule\..*\.url'

ステータス行の先頭にある - は未初期化、+ はワークツリーのコミットがメインリポジトリの記録と一致していない状態、U はマージ競合が存在する状態を示します。CIに入る前に、これら3つの状態を正常な結果として扱ってはいけません。

ビルドジョブでは git submodule update --remote を実行しないでください。設定されたブランチの移動に追従するため、メインリポジトリの同じコミットから、実行時期によって異なる依存関係が取得される可能性があります。

相対URLは、同じコードホスティング環境内でリポジトリを移行する場合に便利ですが、メインリポジトリのリモートURLを基準に解決されます。ジョブが一時的に origin を書き換える場合は、初期化前に git submodule sync --recursive を実行し、解決後の実際のURLをマスキングしたうえで出力して確認してください。

チェックアウト処理の入口を統一する

clone --recurse-submodules、ディレクトリへ手動で移動して行う取得処理、ビルドスクリプト内の応急処置コマンドを混在させないでください。CIでは、まずメインリポジトリを取得し、その後は1つの入口からすべての階層を処理する方法を推奨します。

set -euo pipefail

git submodule sync --recursive
git submodule update --init --recursive --jobs 4

expected_file="$(mktemp)"
actual_file="$(mktemp)"
trap 'rm -f "$expected_file" "$actual_file"' EXIT

git ls-tree -r HEAD |
  awk '$2 == "commit" { print $3, $4 }' |
  sort > "$expected_file"

git submodule status --recursive |
  sed -E 's/^[ +\-U]([0-9a-f]+) ([^ ]+).*/\1 \2/' |
  sort > "$actual_file"

diff -u "$expected_file" "$actual_file"

ここでの --jobs 4 は並列ネットワークリクエスト数を制限するだけであり、固定的な性能上の結論ではありません。ノードのネットワーク状況、リモート側のレート制限、Submoduleの数は環境ごとに異なるため、低い並列数から始め、失敗率を見ながら調整してください。

スクリプトでは、ビルド前に git diff --submodule=log --exit-code も実行する必要があります。インストール処理がSubmoduleのワークツリーを密かに変更した場合は、出所の不明なソースコードでアーカイブ処理を続けるのではなく、ジョブを直ちに失敗させるべきです。

浅いクローンは慎重に使用する

浅いクローンは転送量を削減できますが、「リモートには確かにコミットがあるのに、CIでは見つからない」という問題の典型的な原因でもあります。メインリポジトリに記録されたSubmoduleオブジェクトが現在の浅い履歴境界より古い場合や、削除済みブランチの履歴からしか到達できない場合があります。

状況 推奨方針 失敗時の対応
Submoduleのコミットがブランチ先端に近い 深さを制限してチェックアウトする 深さを増やして再試行する
必要な履歴範囲が一定でない Submoduleを完全に取得する 固定の深さに依存した推測をしない
Submoduleが複数階層にネストしている 各階層でコミットを検証する 失敗したパスとオブジェクトIDを出力する
リモートオブジェクトに到達できない リポジトリの参照を修正する ブランチの切り替えで問題を隠さない

浅いクローンを維持する必要がある場合は、まず次を試します。

git submodule update --init --recursive --depth 50

失敗しても、すぐに --remote へ変更してはいけません。エラーが発生したパスに移動し、git fetch --deepen=100 を実行してから、git cat-file -e <commit>^{commit} で確認します。それでもオブジェクトが存在しない場合は、無作為な再試行を増やすのではなく、リモート側のオブジェクト保持状況またはメインリポジトリの記録を確認する必要があります。

プライベートSubmoduleの認証情報を分離する

Submoduleごとに必要な権限の境界が異なる場合があります。最も安全なのは、ジョブごとに一時的なGit設定を作成し、プロセスの実行中だけ渡す方法です。ノードのグローバル設定は変更しません。

git_config="$(mktemp)"
chmod 600 "$git_config"
trap 'rm -f "$git_config"' EXIT

git config --file "$git_config" credential.helper ""
git config --file "$git_config" protocol.version 2

GIT_CONFIG_GLOBAL="$git_config" \
git submodule update --init --recursive

認証情報は、CIの保護された変数または短期ファイルから提供する必要があります。.gitmodules、リモートURL、コマンド引数、ビルドログには書き込まないでください。ジョブ終了後は、少なくとも次の項目を確認します。

git config --global --list
git remote -v
git submodule foreach --recursive 'git remote -v'

出力を確認する前に、機密情報をマスキングしてください。認証情報をURLに埋め込んだことがある場合、一時ファイルを削除するだけでは不十分です。リモートURLを元に戻し、Shell履歴、診断パッケージ、キャッシュディレクトリも確認する必要があります。

失敗情報を原因特定に使える証拠へ変える

Submoduleの失敗について、「終了コード128」という情報だけを残しても十分ではありません。メインリポジトリのコミット、失敗したパス、期待されるオブジェクト、実際のオブジェクト、解決後のホスト名、チェックアウトの深さ、失敗した段階を記録することを推奨します。ただし、ユーザー名、トークン、認証情報を含む完全なURLは記録しないでください。

調査手順は次のように固定できます。

  1. git ls-tree HEAD <path> で、メインリポジトリが期待するオブジェクトを確認します。
  2. .gitmodules とローカルのGit設定から、最終的に使用されるURLを確認します。
  3. git cat-file -e で、オブジェクトがローカルに存在するか確認します。
  4. 読み取り専用の認証情報を使用して、リモートへのアクセス権を検証します。
  5. 浅いクローンの境界と、ネストしたSubmoduleの状態を確認します。
  6. ワークツリーをクリーンアップし、同じチェックアウトスクリプトを再実行します。

最後に、git submodule status --recursive をビルドメタデータとしてアーカイブします。データ量はごくわずかですが、そのビルドで実際に使用された依存関係のコミットを確認できます。SDKMacノード上のジョブでも同じ原則に従ってください。まずソースコードの入力を固定し、その後でXcodeやその他のツールチェーンを起動することで、チェックアウトの問題とコンパイルの問題を明確に切り分けられます。

よくある質問

Submoduleのコミットを固定しても取得に失敗するのはなぜですか?

親リポジトリが保持するのはオブジェクトIDだけです。URL、権限、クローン深度、対象オブジェクトがリモートに存在するかを別途確認します。

CIでgit submodule update --remoteを使うべきですか?

再現可能なビルドでは使いません。設定ブランチを追跡するため、同じ親コミットでも実行時期によって異なる依存コミットが選ばれます。

ジョブ後にSubmoduleの認証情報を残さない方法はありますか?

ジョブ専用の一時設定または保護ファイルから注入し、trapで削除します。最後にGit設定、remote URL、ログへ機密値が残っていないか確認します。

専有物理ノード

継続的インテグレーションにクラウドMacを選ぶ

SDKMac M4とSDKMac M4 Proの構成、リージョン、4種類の料金サイクルを比較して、注文を作成できます。

レンタルプランを選ぶ