エンジニアリング記事

クラウドMac CIでSwift生成コードのドリフトを防ぐ

クラウドMac CIでSwift生成コードのドリフトを防ぐ

同じコミットでも、開発者のMacでは変更が生じないのに、クラウドMac CIで実行すると Generated/ に数十行もの差分が現れることがあります。さらに厄介なのは、プロジェクト自体はそのままコンパイルできるため、古い定数、欠落したリソースキー、期限切れのmockが成果物に入り込んでしまう点です。この問題は生成コマンドを一度追加するだけでは解決できません。生成ツールのバージョン、入力セット、出力ディレクトリ、検証ルールをまとめてビルド契約に組み込む必要があります。

まずドリフトの発生源を特定する

SwiftGenやSourceryのようなツールの出力は、実行ファイルのバージョン、設定とテンプレート、入力ファイル、実行環境という4つの要素で決まります。設定ファイルだけを固定しても不十分です。開発環境のPATHにあるツールがCIより新しい場合もあれば、ディレクトリの状態によってファイルの走査順が変わる場合もあります。また、ロケール設定がソート順や日付形式に影響することもあります。

最初に、ローカル環境とCIの両方で必要最小限の実行状況を記録します。

set -euo pipefail

sw_vers
xcodebuild -version
swiftgen --version
sourcery --version
printf 'LANG=%s
' "${LANG:-unset}"
printf 'PWD=%s
' "$PWD"
git status --short

ここでの目的は、情報をできるだけ多く集めることではなく、生成処理が実際に何を読み込んだかを確認することです。テンプレートが現在時刻、絶対パス、ランダムな識別子を書き込む場合は、まずそれらの非決定的な入力を排除します。そうしなければ、実行するたびに理由は説明できても意味のない差分が発生します。

「もう一度生成できる」ことと、「同じ結果を生成できる」ことは同義ではありません。CIゲートが判定するのは、入力が同じときに成果物がバイト単位で一致するかどうかです。

再現可能な生成ベースラインを構築する

生成ディレクトリはツールが完全に管理し、手作業で保守するSwiftファイルを混在させないようにします。実行前に古いディレクトリを毎回削除すれば、入力から除外されたにもかかわらず、差分生成によって残存していた型を検出できます。設定、テンプレート、入力ディレクトリには、呼び出し元のカレントディレクトリに依存しない、リポジトリルート基準の確定したパスを使用します。

管理対象と責任範囲は、次のように整理できます。

項目 固定方法 CIでの検証
生成ツール バージョンを固定して出力する リポジトリで宣言したバージョンと一致
設定とテンプレート バージョン管理に含める ワークツリーに一時的な変更がない
入力 ディレクトリと拡張子を明示する ソート後のセットが安定している
出力 専用ディレクトリを使用する 生成前に空にする
環境 ロケールを固定する 時刻と絶対パスを書き込まない

ツールのインストール方法はチームごとに異なっても構いませんが、バージョン宣言の情報源は1つに限定する必要があります。スクリプトに1つ目のバージョン、開発者向けドキュメントに2つ目のバージョンを記載し、さらにクラウドMacがPATHから3つ目のバージョンを読み込むような状態は避けてください。

コンパイル前に差分ゲートを実行する

生成処理をコンパイルより前に配置すると、失敗をすばやく検出でき、コンパイルエラーによって本来の原因が隠れることもありません。次のスクリプトは、すべての生成ファイルが Generated/ にあり、それらがリポジトリにコミット済みであることを前提としています。

set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

export LANG=C
export LC_ALL=C

rm -rf Generated
mkdir -p Generated build

swiftgen config run --config swiftgen.yml
sourcery --config sourcery.yml

find Generated -type f -print |
  LC_ALL=C sort |
  while IFS= read -r file; do
    shasum -a 256 "$file"
  done > build/generated.sha256

git diff --exit-code -- Generated
git status --short --untracked-files=all Generated

git diff で確認できるのは追跡対象ファイルの変更だけなので、未追跡ファイルも別途確認する必要があります。実際のCIゲートでは、git status に出力があれば明示的に失敗させ、差分パッチと generated.sha256 をビルドアーティファクトとして保存します。ハッシュ一覧は差分チェックの代わりにはなりませんが、「今回のCIで実際にどのファイル群が生成されたのか」を確認する手がかりになります。

3種類の失敗を切り分ける

フォーマットだけが変わっている場合は、テンプレートの改行、ツールのバージョン、フォーマッターの実行順を確認します。ファイルが追加または削除されている場合は、入力セットとディレクトリの消去処理を確認します。内容の値が変わっている場合は、リソース、API記述、テンプレートパラメータを確認します。まず失敗を分類してから根本原因を修正し、CI上で成果物を上書きしたままコンパイルを続行しないでください。

Xcodeビルドフェーズの並行実行を避ける

複数のtargetが同じ生成ディレクトリを共有している状態で、それぞれのRun Script Phaseに生成コマンドを追加すると、同じファイルへ同時に書き込む可能性があります。単に処理が重複するだけの場合もありますが、ファイルが途中で切れたり、一時的に消えたりすることもあります。より確実なのは、生成処理をCIの独立した前処理として実行し、完了してからすべての xcodebuild タスクを開始する方法です。

Xcodeのビルドフェーズで実行しなければならない場合は、生成処理の所有者を1つに限定し、入力ファイルと出力ファイルの完全なリストを宣言します。生成スクリプト自体がプロジェクトファイルを変更したり、生成ディレクトリを読み込みながら同時に消去したりしてはいけません。並列ビルドごとに独立した出力が必要な場合は、タスク識別子別の一時ディレクトリを作成し、検証に合格した後、1回のアトミックな置換で固定の保存先に公開します。

もう1つのよくある問題は、生成直後にフォーマッターを実行する一方で、ローカルのpre-commit hookでは逆の順序で処理していることです。生成、フォーマット、差分チェックは常に同じ順序にそろえ、同じエントリースクリプトから実行する必要があります。

マージ前と障害発生時のチェックリスト

生成ツールや入力に変更を加える場合、コードレビューでは少なくとも次の項目を確認します。

  1. 生成ツールのバージョン宣言が同期して更新されているか。
  2. 設定、テンプレート、入力の変更が同じコミットに含まれているか。
  3. 入力を削除した際、対応する生成ファイルも確実に削除されているか。
  4. 成果物に時刻、ユーザー名、絶対パスが混入していないか。
  5. ローカルで2回連続実行したとき、2回目に差分が発生しないか。
  6. CIがコンパイル前に追跡対象ファイルと未追跡ファイルを確認しているか。
  7. 並列タスクが互いに分離されたディレクトリへ出力しているか。
  8. 失敗時にバージョン情報、差分パッチ、ハッシュ一覧が保存されるか。

調査を始める際は、最初から自動修正を実行して現場を上書きしないでください。まず git status、ツールのバージョン、差分を保存し、その後でクリーンなチェックアウトから生成エントリーポイントを再実行します。クリーンなチェックアウトでも結果が一致しない場合、原因は環境または固定されていない入力にあります。再利用されたワークスペースでのみ失敗する場合は、古いファイル、キャッシュ、並行書き込みを重点的に確認します。最終的な基準は単純です。同じコミットを任意のクラウドMacジョブで2回連続生成したとき、2回目には観測可能な差分が一切生じない状態にします。

よくある質問

生成したSwiftファイルはリポジトリへコミットすべきですか?

レビューやローカル開発で生成済みファイルを直接使うならコミットします。その場合もCIで再生成し、差分が残れば失敗させる運用が必要です。

ローカルとCIで生成結果が異なる主な原因は何ですか?

生成ツールの版、入力順、ロケール、改行、作業ディレクトリの違いです。コンパイル前に各条件を明示し、ログへ残してください。

VMMini M4

専有物理ノードで次のタスクを実行

ノードと課金期間を選択し、固定構成のM4、16GB RAM、256GB SSDでデプロイを開始できます。実際の利用可否はコンソールのリアルタイム表示をご確認ください。

クラウドMacを今すぐレンタル