クラウドMac CIでSMB共有を使いビルド汚染を防ぐ

クラウドMac CIでSMB共有を使いビルド汚染を防ぐ

CIノードでアーカイブが完了したら、成果物をテスト、リリース、またはアーティファクト保管ジョブへ引き渡す必要があります。最も単純なのは、SMB共有ディレクトリをそのままワークスペースに設定し、複数のクラウドMacから同じプロジェクトを読み書きする方法です。小規模なリポジトリでは一時的に問題なく動作することもありますが、ネットワークの瞬断、ファイルロックの遅延、マウントの切断が発生すると、Xcodeの中間ファイルが破損する可能性があります。さらに、同名で作成されたローカルの空ディレクトリに結果が書き込まれることもあります。

より安全なのは、SMBの役割を入力スナップショットと最終成果物の受け渡しだけに限定することです。ソースコードの展開、依存関係の解決、DerivedData、 一時ファイルはすべてローカルディスクに配置します。

3種類のディレクトリを分離する

各ジョブには、少なくとも次の3つの独立したパスが必要です。

  • 読み取り専用の入力領域:ソースコードのスナップショットまたはバージョン固定済みの依存パッケージを保存します。
  • ローカルワークスペース:チェックアウト後のソースコード、DerivedData、テスト結果、一時ファイルを配置します。
  • リモート配布領域:検証済みのアーカイブ、ログの要約、マニフェストだけを保存します。

2つの並行ジョブでDerivedDataを共有しないでください。同じコミットをビルドする場合でも、インデックス、モジュールキャッシュ、ビルドデータベースが同時に書き換えられる可能性があります。~/ci-work/$RUN_ID のようにジョブIDをパスへ含め、ジョブ終了後に保持ポリシーに従って削除することを推奨します。

ネットワーク共有は成果物の受け渡し境界として扱い、ローカルディスクを透過的に置き換えるものと考えてはいけません。また、ビルド成功とアップロード成功は、必ず別々の状態として管理する必要があります。

SMBマウントを常駐プロセスで管理する

無人実行ジョブでは、コマンドライン引数にユーザー名やパスワードを埋め込むべきではありません。管理下の常駐プロセスが事前にSMBをマウントし、専用のキーチェーンまたはアクセス制限された設定から認証情報を読み取る方式が適しています。通常のビルドジョブには、対象ディレクトリへの必要最小限の書き込み権限だけを付与します。

ジョブ開始時に、ディレクトリの存在だけを確認してはいけません。SMB接続が切れた後も、マウントポイントが通常のディレクトリとして残ることがあります。少なくともファイルシステムの種類、書き込み可否、小さなファイルの書き込みと読み戻しを確認します。

set -euo pipefail

SHARE_ROOT="/Volumes/ci-artifacts"
PROBE="$SHARE_ROOT/.probe-${RUN_ID}"

fs_type="$(stat -f '%T' "$SHARE_ROOT")"
test "$fs_type" = "smbfs"
test -w "$SHARE_ROOT"

printf '%s\n' "$RUN_ID" > "$PROBE"
test "$(cat "$PROBE")" = "$RUN_ID"
rm -f "$PROBE"

いずれかの処理が失敗した場合は、ジョブを終了させます。マウントポイントを自動作成して処理を続行してはいけません。共有が一度切断されただけで、「ビルドは成功したが、成果物はローカルにしか残っていない」という状態になりかねません。

ビルド処理をローカルに限定する

固定されたコミットをローカルの一時ディレクトリへ同期してから、依存関係の解決とビルドを実行します。ジョブディレクトリ、DerivedData、結果バンドルには一意のジョブIDを含め、並行ジョブによる上書きを防ぎます。

LOCAL_ROOT="$(mktemp -d "$TMPDIR/vmcache-ci.XXXXXX")"
trap 'rm -rf "$LOCAL_ROOT"' EXIT

rsync -a --delete "$SOURCE_SNAPSHOT/" "$LOCAL_ROOT/repo/"

xcodebuild \
  -workspace "$LOCAL_ROOT/repo/App.xcworkspace" \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$LOCAL_ROOT/DerivedData" \
  -resultBundlePath "$LOCAL_ROOT/TestResults.xcresult" \
  build

ここで使用する SOURCE_SNAPSHOT は、他のジョブによって更新され続ける作業ディレクトリではなく、不変のコミットに対応している必要があります。入力が共有ボリューム上にある場合は、同期を始める前にコミットIDまたはマニフェストを照合します。

DerivedData全体をキャッシュしない

ジョブ間でDerivedData全体をコピーしても、多くの場合は利点より負担のほうが大きくなります。キャッシュ容量が大きいうえ、内部パスやビルド設定が変化する可能性もあるためです。再利用可能であることが明確な依存関係のダウンロードディレクトリだけをキャッシュし、Xcodeのバージョン、アーキテクチャ、ロックファイルのハッシュをキャッシュキーに含めるほうが管理しやすくなります。キャッシュキーが一致しない場合は再生成し、曖昧な条件では再利用しません。

一時ファイル名を使って原子的に公開する

最初から正式なファイル名へコピーすると、未完成の成果物が公開されてしまいます。下流ジョブはファイルが現れた時点で読み取りを開始する可能性がありますが、その時点ではまだ転送中かもしれません。正しい順序は、ローカルでのパッケージ作成、ローカルでのハッシュ計算、共有ボリューム上の一時ファイル名へのコピー、共有ボリューム上での検証、同じディレクトリ内での最終ファイル名への変更です。

ARTIFACT="$LOCAL_ROOT/App-release.zip"
ditto -c -k --norsrc "$LOCAL_ROOT/DerivedData/Build/Products/Release" "$ARTIFACT"

REMOTE_DIR="$SHARE_ROOT/releases/$GIT_COMMIT"
REMOTE_TMP="$REMOTE_DIR/.App-release.zip.${RUN_ID}.partial"
REMOTE_FINAL="$REMOTE_DIR/App-release.zip"

mkdir -p "$REMOTE_DIR"
cp "$ARTIFACT" "$REMOTE_TMP"

local_hash="$(shasum -a 256 "$ARTIFACT" | awk '{print $1}')"
remote_hash="$(shasum -a 256 "$REMOTE_TMP" | awk '{print $1}')"
test "$local_hash" = "$remote_hash"

mv "$REMOTE_TMP" "$REMOTE_FINAL"
printf '%s  %s\n' "$remote_hash" "App-release.zip" \
  > "$REMOTE_DIR/SHA256SUMS.${RUN_ID}"

原子的な名前変更を成立させるには、一時ファイルと正式ファイルが同じ共有ボリューム上にあり、できれば同じディレクトリ内に配置されている必要があります。ローカルへ書き込んでからボリュームをまたいで mv してはいけません。ボリューム間の操作は、実際にはコピーと削除になります。

失敗を判別可能かつクリーンアップ可能にする

アップロードに失敗した場合は、再転送できるようにローカルのビルド結果を管理された期間だけ保持します。ただし、ジョブを配布完了として扱ってはいけません。build_completedartifact_verifiedpublish_completed を個別に記録し、下流では最後の状態だけを受け入れることを推奨します。

保持期間を超えた .partial ファイルも定期的に削除する必要があります。クリーンアップジョブはファイル名の形式と更新日時を確認し、一時ファイル用の接尾辞を持つファイルだけを処理します。正式な成果物を検索したり削除したりしてはいけません。共有ボリュームが読み取り専用になった場合、ハッシュが一致しない場合、または切断が繰り返される場合は、公開キューを停止し、ローカルの成果物とマウント診断情報を先に保全します。

VMCacheのクラウドMacでこのフローを実装する際に重要なのは、特定のマウントコマンドではなく、責務の境界を明確にすることです。ネットワークボリュームはデータ交換を担当し、ローカルディスクはビルドを担当し、正式なファイル名は検証完了済みの成果物だけを表します。この構成ならSMBに一時的な問題が発生しても、ビルド環境と配布状態が混同されることはありません。

よくある質問

DerivedDataをSMB共有に置いてもよいですか?

推奨しません。DerivedDataは小さなファイル、ロック、メタデータ更新を大量に発生させるため、ネットワーク遅延の影響を受けます。ジョブごとのローカル領域を使います。

コピー途中の成果物を後続ジョブから隠すにはどうしますか?

SMB上の一時名へコピーし、チェックサムと完了マニフェストを作成します。検証後に同じディレクトリ内で正式名へ変更し、後続ジョブは正式名だけを監視します。

マウント先ディレクトリが存在すれば処理を続けてもよいですか?

いいえ。ファイルシステム種別と書き込み権限を確認し、小さなファイルの書き戻し試験を行います。失敗時は同じパスのローカルディレクトリへ誤出力しないよう終了します。

専用物理ノード

構築・テスト・MLX推論にクラウドMacを選ぶ

実際のタスクに合わせて、M4、メモリ、ストレージ、ノード、契約期間を選択できます。各レンタルには専用物理マシンが割り当てられ、仮想マシンではありません。

プランを選ぶ