エンジニアリングガイド

クラウドMacで行うiOS成果物の整合性検証と原子的公開

クラウドMacで行うiOS成果物の整合性検証と原子的公開

パイプラインが成功と表示されていても、ダウンロードした IPA が、そのビルドをトリガーしたコミットと対応していないことがあります。さらに危険なのは、アップロードが中断された後、後続ジョブが書き込み途中のファイルを取得してしまうケースです。クラウド Mac では、ビルド、エクスポート、アップロードを別々のスクリプトが担当することが一般的です。そのため、「コマンドの終了コードが 0 だった」ことだけを成果物の受け入れ基準にすると、こうした問題を同じジョブ内で検出するのは困難です。

必要なのは圧縮コマンドを追加することではありません。IPA、SHA-256 マニフェスト、ビルドコンテキスト、検証結果を分割できない一つの成果物セットとして定義し、すべてのチェックに合格した場合にのみ公開する仕組みです。

配布可能な成果物を先に定義する

各ジョブでは、一意の BUILD_ID を使用します。配布ディレクトリには、少なくとも次のファイルを含めます。

ファイル 用途 受け入れ条件
App.ipa インストールおよび配布用成果物 ハッシュが一致し、エクスポートコマンドが成功している
SHA256SUMS コンテンツ整合性マニフェスト shasum -a 256 -c が成功する
build-context.tsv ビルド元の追跡 コミット、Xcode、ジョブ識別子が空でない
export.log エクスポート失敗の調査 完全な出力を保存し、アクセス権限を制限する

ビルドコンテキストには、スクリプト内でブランチを推測した結果ではなく、パイプラインですでに確定している変数を使用します。少なくとも、完全なコミットハッシュ、ジョブ識別子、Scheme、Xcode のバージョン、生成時刻を記録してください。時刻はログを関連付けるためだけに使用し、成果物の同一性はコミットハッシュとチェックサムによって判断します。

ハッシュによって、二つのチェックポイント間でファイルが変更されていないことは確認できます。しかし、そのファイルが最初から信頼できるプロセスで生成されたことまでは証明できません。コード署名の検証とビルドコンテキストの両方が必要です。

エクスポート前にアーカイブを検証する

まずアーカイブが存在することを確認し、その中の .app を特定してコード署名を検証します。--deep を使って再署名してはいけません。ここでの目的は、既存の署名構造を検査することです。アプリに拡張機能が含まれている場合は、検証コマンドでネストされたコードも対象にし、失敗を処理続行不可の条件として扱います。

アーカイブ、エクスポートディレクトリ、最終ディレクトリは同じ作業ボリュームに配置してください。これにより、最後のディレクトリ名変更を原子的に実行できます。MiniRent のクラウド Mac で実行する場合も、並行する各ジョブに独立したルートディレクトリを割り当て、二つのジョブが out/latest のような固定パスを共有しないようにします。

そのまま応用できるスクリプト

set -euo pipefail

: "${BUILD_ID:?BUILD_ID is required}"
: "${GIT_COMMIT:?GIT_COMMIT is required}"
: "${SCHEME:?SCHEME is required}"

ROOT="${ARTIFACT_ROOT:-$PWD/out}"
ARCHIVE="$ROOT/input/App.xcarchive"
EXPORT_OPTIONS="$ROOT/input/ExportOptions.plist"
STAGE="$ROOT/.stage-$BUILD_ID"
FINAL="$ROOT/releases/$BUILD_ID"

rm -rf "$STAGE"
mkdir -p "$STAGE" "$(dirname "$FINAL")"
trap 'rm -rf "$STAGE"' EXIT

test -d "$ARCHIVE"
test -f "$EXPORT_OPTIONS"
test ! -e "$FINAL"

APP_PATH="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"

xcodebuild -exportArchive \
  -archivePath "$ARCHIVE" \
  -exportPath "$STAGE/export" \
  -exportOptionsPlist "$EXPORT_OPTIONS" \
  >"$STAGE/export.log" 2>&1

IPA_PATH="$(find "$STAGE/export" -maxdepth 1 -name '*.ipa' -print -quit)"
test -n "$IPA_PATH"
cp "$IPA_PATH" "$STAGE/App.ipa"

DIGEST="$(shasum -a 256 "$STAGE/App.ipa" | awk '{print $1}')"
printf '%s  %s
' "$DIGEST" "App.ipa" >"$STAGE/SHA256SUMS"

XCODE_VERSION="$(xcodebuild -version | paste -sd ' ' -)"
printf 'build_id	%s
commit	%s
scheme	%s
xcode	%s
' \
  "$BUILD_ID" "$GIT_COMMIT" "$SCHEME" "$XCODE_VERSION" \
  >"$STAGE/build-context.tsv"

(
  cd "$STAGE"
  shasum -a 256 -c SHA256SUMS
)

mv "$STAGE" "$FINAL"

(
  cd "$FINAL"
  shasum -a 256 -c SHA256SUMS
)

スクリプト内の ExportOptions.plist には、リポジトリで管理された設定を使用してください。パスワード、秘密鍵、一時トークン、環境変数の全内容をログやコンテキストファイルへ書き込んではいけません。

原子的な公開で未完成の成果物を隔離する

重要なのは、非表示の一時ディレクトリですべての書き込みと検証を完了してから、一度の mv で最終ディレクトリを公開することです。読み取り側は releases/ だけを走査し、.stage-* にはアクセスしません。そのため、生成途中のファイルを取得することはありません。

この保証が成立するのは、同一ファイルシステム内に限られます。公開先ディレクトリが別のボリュームにマウントされている場合、mv はコピーと削除に置き換わる可能性があります。正しい手順は、ターゲットボリューム上に一時ディレクトリを作成し、すべてのファイルをコピーして、ターゲット側でハッシュ検証を再実行した後、そのボリューム内で名前を変更することです。オブジェクトストレージやアーティファクトサービスにディレクトリ名変更の仕組みがない場合は、まずジョブ識別子を含む一時キーへアップロードし、検証後に小さな完了マーカーを書き込みます。利用側は成果物を読み取る前に、必ずそのマーカーを確認しなければなりません。

latest を上書きしない

latest を使用すると、再試行されたジョブが、より新しい成功済み成果物を上書きするおそれがあります。安定した方法は、不変の BUILD_ID ごとに成果物を保存し、対象ジョブの識別子だけを格納したポインターファイルを別途管理することです。ポインターを更新する前に、対象ディレクトリが検証に合格していることを確認してください。ロールバック時もポインターだけを変更し、過去の成果物には手を加えません。

失敗を正しい工程で止める

署名エラーはエクスポート前、エクスポートエラーは一時ディレクトリ内、ハッシュエラーは公開前に処理を停止させます。アップロード後の再検証は、転送、ディスク、またはスクリプトによるファイルの選択ミスで生じた変更を検出するために行います。

よくある誤りとして、拡張子を限定せずにエクスポートディレクトリの「最初のファイル」を取得すること、複数の Scheme で同じ出力パスを共有すること、再試行時に前回の一時ディレクトリを再利用することが挙げられます。スクリプトの開始時には現在のジョブ用ステージングディレクトリを削除する必要がありますが、広範なワイルドカードで他のジョブのディレクトリまで削除してはいけません。

ログにも明確な境界を設けます。xcodebuild の完全な終了ステータスと出力は保持しつつ、機密性の高い環境情報は出力しないようにします。サポートリクエストを送信する前に、関連する時間範囲だけを抜き出し、リポジトリトークン、署名素材のパスに含まれる内部識別子、接続認証情報を削除してください。

配布前チェックリスト

パイプラインを成功としてマークする前に、以下を一つずつ確認します。

  1. アーカイブ内の App が厳格なコード署名検証に合格している。
  2. IPA が古いディレクトリの再利用ではなく、現在のジョブのアーカイブからエクスポートされている。
  3. GIT_COMMIT が完全なコミットハッシュであり、トリガー記録と一致している。
  4. SHA256SUMS がステージングディレクトリと最終ディレクトリでそれぞれ一度ずつ検証されている。
  5. 最終ディレクトリが一意のジョブ識別子で命名され、再試行によって上書きされない。
  6. 利用側が、公開済みディレクトリまたは完了マーカー付きのオブジェクトだけを読み取る。
  7. ログから失敗した工程を特定でき、認証情報や署名素材の内容は含まれていない。

このフローですべてのビルド失敗をなくすことはできませんが、「ビルドが成功したか」と「配布物を信頼できるか」を、それぞれ検証可能な別の問題として扱えるようになります。アーカイブ、検証、コンテキスト、公開境界を固定すれば、再試行とロールバックでは明確に識別されたオブジェクトだけを扱えます。変化し続ける共有ディレクトリに依存する必要はありません。

よくある質問

SHA-256だけでiOS成果物の作成元まで証明できますか?

できません。SHA-256が示すのは検査点の間でファイルが変化していないことです。コード署名を検証し、コミット、Xcodeバージョン、ジョブ識別子も同じマニフェストに保存します。

公開前に一時ディレクトリを使う理由は何ですか?

同じファイルシステム内で完成したディレクトリをリネームすれば、利用側から途中状態が見えません。別ボリュームへ移す場合はコピー後に再検証し、移動先のボリューム内でリネームします。

専用クラウドMac

専用物理ノードでビルドタスクを実行

MiniRents M4またはMiniRents M4 Proを日単位、週単位、月単位、四半期単位から選択し、チームとコードリポジトリの場所に合わせてノードを選べます。物理マシンを専有利用でき、仮想マシンではありません。

モデルを選んで注文