クラウドMac CIジョブを安全にキャンセルする方法

クラウドMac CIジョブを安全にキャンセルする方法

リモートCIのキューで最も扱いにくいのは、失敗ではなく「キャンセル」です。開発者が画面上でジョブを停止すると、通常、ランナーは最初にスクリプトへ TERM を送信します。スクリプトがこのシグナルを処理しなければ、実行中の xcodebuild、テストプロセス、ログ収集プロセスがワークスペースを占有し続ける可能性があります。その結果、次のジョブでロックファイル、使用中のシミュレータ、不完全な結果バンドルに遭遇します。一見するとランダムな障害ですが、根本原因は前回のジョブが正常に終了しなかったことにあります。

キャンセル完了の基準を先に定義する

ジョブがキューから消えても、リソースの回収が完了したとは限りません。運用上の完了基準には、少なくとも次の4項目を含めます。

  1. メインのビルドプロセスが終了シグナルを受信して終了している。
  2. スクリプトに実際の終了理由が記録され、すべてが一律にビルド失敗として扱われていない。
  3. 生成済みのログと xcresult が保存されている。
  4. ワークスペースに、このジョブに属するバックグラウンドプロセスや一時マウントが残っていない。

キャンセル処理もパイプラインの通常経路の一部です。異常終了時にしかクリーンアップ処理をテストしないと、たいていは最も忙しいタイミングで、まったく機能しないことが判明します。

まず、ランナーが実際にどのシグナルを送信するか確認します。テストジョブには、機密性の高い環境変数を含めず、受信したシグナルだけを記録するプローブを追加できます。デフォルト動作を推測で決めつけないでください。また、即座に KILL を送る方法をキャンセル戦略にしてはいけません。データベースを閉じたり、ログをフラッシュしたり、結果バンドルを整理したりする時間がプロセスに与えられないためです。

トラップでTERMを受け取り、メインプロセスを追跡する

ビルドスクリプトでは、xcodebuild のPIDを明示的に保存します。TERM または INT を受信したら、現在のジョブが作成したプロセスだけを終了し、対象範囲を限定しない killall は使用しないでください。次のBashスクリプトのひな型では、ログと結果をジョブごとの実行ディレクトリへ保存します。

#!/bin/bash
set -u

run_id="${CI_RUN_ID:-manual-$(date +%s)}"
run_dir="$PWD/.ci-runs/$run_id"
result_path="$run_dir/TestResults.xcresult"
log_path="$run_dir/xcodebuild.log"
status_path="$run_dir/status.txt"
child_pid=""
cancelled=0

mkdir -p "$run_dir"

on_cancel() {
  cancelled=1
  if [[ -n "$child_pid" ]] && kill -0 "$child_pid" 2>/dev/null; then
    kill -TERM "$child_pid" 2>/dev/null || true
  fi
}

trap on_cancel TERM INT

xcodebuild \
  -workspace Example.xcworkspace \
  -scheme Example \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -resultBundlePath "$result_path" \
  test >"$log_path" 2>&1 &

child_pid=$!
wait "$child_pid"
build_status=$?

if [[ "$cancelled" -eq 1 ]]; then
  printf 'cancelled\n' >"$status_path"
  exit 130
fi

printf 'finished:%s\n' "$build_status" >"$status_path"
exit "$build_status"

この例では、トラップ内でディレクトリを直接削除していません。トラップはシグナルの転送だけを担当し、メイン処理では引き続き wait を実行するため、通常の失敗と手動キャンセルを区別できます。終了コード 130 は中断を表すためによく使われますが、使用しているCIシステムがキャンセル状態をどのようにマッピングするか、事前に確認してください。

プロセスの終了待機に上限を設ける

テストによっては TERM に応答しないことがあります。本番用スクリプトでは、シグナル送信後にPIDを1秒ごとに確認し、たとえば20秒待機してから、タイムアウト時に KILL を送信できます。猶予期間には必ず上限を設けてください。上限がなければ、キャンセル処理がランナーを永続的に占有する可能性があります。一方で、待機時間を1~2秒まで短縮するのも避けてください。xcodebuild が結果バンドルを書き込んでいる最中かもしれません。

診断情報の保存とキャッシュのクリーンアップを分離する

終了時のクリーンアップで最もよくある誤りは、trap cleanup EXIT の中で rm -rf "$run_dir" を直接実行することです。ディレクトリはきれいになりますが、原因の特定に必要なログまで削除されます。より堅牢な方法は、対象を次の2種類に分けることです。

内容 キャンセル後の処理
xcodebuild.logxcresult、ステータスファイル 保存してアーカイブ
このジョブが作成した一時ディレクトリ パスを確認してから削除
共有依存関係キャッシュ キャンセルトラップでは削除しない
シミュレータとDerived Data ジョブの分離方針に従って処理

ログのアーカイブ処理では、ファイルがまだ生成されていない場合も許容する必要があります。[[ -e "$result_path" ]] で存在を確認し、結果バンドルがないことを新たなエラーにしないでください。アップロードに時間がかかる可能性がある場合は、アップロード処理に個別のタイムアウトを設定し、アップロードの失敗で元のビルド終了コードが上書きされないようにします。

一時ディレクトリは、実行番号を付けた固定の親ディレクトリ配下に配置することを推奨します。削除前にプレフィックスと実際のパスを両方確認し、変数が空の場合に削除範囲が広がらないようにします。

safe_remove_run_dir() {
  local target="$1"
  local root="$PWD/.ci-tmp"

  [[ -n "$target" ]] || return 1
  [[ "$target" == "$root/"* ]] || return 1
  [[ -d "$target" ]] || return 0

  rm -rf -- "$target"
}

システム全体を一掃せず、残留プロセスを確認する

ジョブの終了後、まずプロセスのスナップショットを記録できます。

ps -axo pid,ppid,state,etime,command >"$run_dir/processes-after.txt"
pgrep -P "$$" >"$run_dir/direct-children.txt" 2>/dev/null || true

pgrep -P で確認できるのは直接の子プロセスだけであり、すべての子孫プロセスを網羅できません。そのため、各補助プロセスをスクリプトから起動し、それぞれのPIDを記録する方法がより確実です。たとえば、ログフォワーダー、テストエージェント、ポートフォワーダーのPIDを個別に配列へ追加し、クリーンアップ時に一つずつ確認します。プロセス名だけを基準に、マシン全体の同名プロセスを終了しないでください。専有の物理Mac miniであっても、開発者が明示的に起動した長時間実行プロセスが同時に動作している可能性があります。

パイプラインで並列実行を許可する場合は、ワークスペース、結果ディレクトリ、シミュレータのデバイスセット、一時ディレクトリのすべてにジョブ番号を付けます。安全なキャンセル処理で解決できるのはプロセスのライフサイクルだけです。複数のジョブが同じ可変ディレクトリを共有することで発生する上書きまでは防げません。

3種類の演習でキャンセル処理を検証する

本番導入前に、少なくとも3種類の演習を実施します。1つ目は依存関係の解決中にキャンセルし、パッケージマネージャーとログプロセスが終了することを確認します。2つ目はコンパイル中にキャンセルし、ビルドデータベースが次のジョブで誤って再利用されないことを確認します。3つ目はテスト実行中にキャンセルし、結果バンドルを読み取れること、シミュレータ関連プロセスがこのジョブのデバイスセットを占有し続けていないことを確認します。

各演習では、次の項目を確認します。

RentMiniのクラウドMacで長時間稼働するCIを運用する場合は、ランナー更新後の受け入れチェックリストにもキャンセル演習を追加してください。ノードと構成は、コンソールで現在選択可能なものを確認するだけで十分です。スクリプト自体を都市、マシン名、固定のワークスペースパスに依存させてはいけません。最終的な目標は、ディレクトリを見かけ上きれいにすることではありません。終了処理ごとに明確な境界と診断情報を確保し、次のビルドを既知の状態から開始できるようにすることです。

よくある質問

CIジョブのキャンセル時にすぐkill -9を使うべきですか?

推奨しません。最初に主ビルドプロセスへTERMを送り、ログや結果を書き終えるための制限時間を設けます。その時間を過ぎても終了しない場合だけKILLを使います。

キャンセルしたXcodeビルドでは何を保存すべきですか?

xcodebuildログ、生成済みのxcresult、最終状態ファイル、プロセス一覧を保存します。終了処理で診断情報を無条件に削除しないことが重要です。

必要なときにクラウドMacを利用

次回のビルドに備えて専用物理Mac miniを準備

M4の構成、利用期間、ノードを選択します。各注文には専用の物理デバイスが割り当てられ、実際の利用可能状況はコンソールにリアルタイムで表示されます。

構成を選んでレンタル