クラウドMacでiOS依存関係の供給網を監査する

クラウドMacでiOS依存関係の供給網を監査する

同じコミットが昨日は成功したのに、今日は異なる間接依存関係を取得してしまう――これは、クラウドMacでのビルドにおいて原因追跡が難しい問題の一つです。表面上は単なるコンパイルエラーに見えても、実際にはパッケージのリビジョン、ダウンロードされた内容、あるいは新たに追加されたスクリプトフェーズが変化している可能性があります。この問題は「再実行」だけでは解決できません。各ビルドについて、何が解決されたのか、内容に変化があったのか、その変化がレビュー済みかという3点を確認できる仕組みが必要です。

先に残すべき証跡を定義する

依存関係の監査では、少なくともSwiftPMとCocoaPodsの直接依存関係・間接依存関係を対象にします。証跡は次の3層に分けると管理しやすくなります。

レイヤー 証跡 確認できること
宣言 Package.swiftPodfile、Xcodeプロジェクトの参照 チームが導入しようとしているもの
解決 Package.resolvedPodfile.lock 実際に固定されたバージョンまたはリビジョン
ビルド 部品表、ファイルハッシュ、ビルドログ 今回のビルドで実際に使用されたもの

Package.resolvedPodfile.lockは、必ずバージョン管理リポジトリに含めます。CIで無視したり、パイプラインが自動更新したままリリースを続行したりしてはいけません。

ロックファイル自体は、安全性を保証する結論ではありません。ロックファイルは入力を安定させるだけであり、その入力をビルドに採用できる理由を示すのは、レビュー、ハッシュ、差分記録です。

ZoomMiniの専有ノードでは作業ディレクトリを継続して保持できますが、監査はあくまでリポジトリ内のファイルを基準にする必要があります。特定のマシンに残っているキャッシュの状態を、唯一の証跡として扱ってはいけません。

解決先ディレクトリを固定してクリーンに解決する

まずSwiftPMのダウンロード先をワークスペース内に固定し、runnerごとに比較不能なグローバルキャッシュが使われるのを防ぎます。workspaceを含むプロジェクトでは、次を実行します。

set -euo pipefail
ROOT="$(pwd)"
SPM_DIR="$ROOT/.audit/spm"
DERIVED_DIR="$ROOT/.audit/DerivedData"

rm -rf "$SPM_DIR" "$DERIVED_DIR"
mkdir -p "$SPM_DIR" "$DERIVED_DIR"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$SPM_DIR"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$DERIVED_DIR" \
  -clonedSourcePackagesDirPath "$SPM_DIR" \
  build

プロジェクトが.xcodeprojのみの場合は、-workspace-projectに置き換えます。依存関係の解決後、リポジトリに変更が加わっていないか直ちに確認します。

git diff --exit-code -- '**/Package.resolved' Podfile.lock

このコマンドが失敗した場合は、ビルドを停止します。主な原因としては、開発者が依存関係を更新した際にロックファイルをコミットし忘れた、ツールのバージョン差によってファイル形式が書き換えられた、依存関係の制約によって新しい間接依存バージョンが許可された、といったケースが考えられます。

ツールチェーンの境界を記録する

監査記録には、xcodebuild -versionswift --versionruby --version、および依存関係管理ツールのバージョンも含めます。これらは必ずしも部品表に含める必要はありませんが、ビルドログとともにアーカイブしてください。同じロックファイルを使用しているのに解決結果が異なる場合、この記録がなければ、差異がツールチェーンによるものかソースコードによるものかを判断するのが難しくなります。

ロックファイルから部品表を生成する

次のスクリプトは、リポジトリ内のPackage.resolvedを走査します。一般的なトップレベルのpins形式とobject.pins形式の両方に対応し、バージョン、ブランチ、リビジョン、ロックファイルのハッシュを記録します。

import glob
import hashlib
import json
import os

items = []

for path in glob.glob("**/Package.resolved", recursive=True):
    if "/.audit/" in f"/{path}":
        continue
    with open(path, "rb") as handle:
        raw = handle.read()
    data = json.loads(raw)
    pins = data.get("pins") or data.get("object", {}).get("pins", [])
    packages = []
    for pin in pins:
        state = pin.get("state", {})
        packages.append({
            "identity": pin.get("identity") or pin.get("package"),
            "location": pin.get("location") or pin.get("repositoryURL"),
            "version": state.get("version"),
            "branch": state.get("branch"),
            "revision": state.get("revision")
        })
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest(),
        "packages": sorted(packages, key=lambda item: item["identity"] or "")
    })

for path in glob.glob("**/Podfile.lock", recursive=True):
    with open(path, "rb") as handle:
        raw = handle.read()
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest()
    })

os.makedirs(".audit", exist_ok=True)
with open(".audit/dependency-manifest.json", "w") as handle:
    json.dump({"artifacts": items}, handle, indent=2, sort_keys=True)

実行後、dependency-manifest.jsonをビルド成果物として保存します。チームの方針としてこのファイルをリポジトリにコミットする場合は、依存関係を更新するコミットで必ず同時に更新してください。リリースジョブが暗黙的に上書きする運用は避けます。

差分チェックをリリースゲートにする

リリースゲートは、あらゆる変更を一律に禁止するものではありません。重要なのは、変更内容を説明できる状態にすることです。実行可能な手順は次のとおりです。

  1. 宣言ファイルとロックファイルが同時に変更されているか確認する。
  2. 隔離した依存関係ディレクトリを空にし、依存関係を再解決する。
  3. 解決処理によってロックファイルが書き換えられていないことを確認する。
  4. 新しい部品表を生成し、ベースラインと比較する。
  5. 新規依存関係のライセンス、メンテナンス元、スクリプト実行能力を確認する。
  6. コンパイルとテストの完了後、部品表、ロックファイルのハッシュ、ツールチェーンのバージョンを保存する。

まずは標準の差分ツールを使って、機械的なゲートを構成できます。

python3 tools/dependency_manifest.py
diff -u audit-baseline/dependency-manifest.json .audit/dependency-manifest.json

更新を許可する場合も、ゲートを単純に無効化してはいけません。新しいベースラインは独立したブランチでコミットし、レビュー担当者は、新たに追加されたリポジトリアドレス、バージョン指定からブランチ指定への切り替え、リビジョンハッシュの変更、バージョン番号のない依存関係を重点的に確認します。

スクリプトフェーズを重点的に確認する

依存関係は、ビルドフェーズを通じてshellスクリプトを実行する場合があります。Xcodeプロジェクトと生成された依存関係プロジェクトをレビューする際は、スクリプトがネットワークにアクセスするか、ワークスペース外の認証情報を読み取るか、ソースディレクトリを書き換えるか、環境変数をログへ出力するかを確認してください。CIアカウントには、チェックアウト、依存関係の解決、ビルドに必要な最小限の権限だけを付与します。

異常発生時の対応とデリバリーチェック項目

ロックファイルが変わっていないにもかかわらず、部品表のハッシュが変化した場合は、まずリリースを停止し、ワークスペース、解決ログ、ダウンロードディレクトリを保存します。その後、ファイルが再生成されていないか、依存関係が可変ブランチを使用していないか、内部ミラーの内容が差し替えられていないか、解決ツールのバージョンが変わっていないかを確認します。先にキャッシュを削除すると、最も価値のある現場情報が失われるため避けてください。

通常のデリバリー前には、次の項目を固定チェックとして確認できます。

  • 宣言ファイル、ロックファイル、部品表が同じコミットに属している。
  • SwiftPMの依存関係が、明示的なバージョンまたは不変のリビジョンに固定されている。
  • CocoaPodsの解決結果がPodfile.lockと一致している。
  • 新規依存関係について、ライセンスとスクリプトフェーズのレビューが完了している。
  • クリーンな依存関係解決によってGit差分が発生しない。
  • ビルド成果物に、部品表のハッシュとツールチェーンのバージョンが関連付けられている。
  • CIで依存関係スクリプトに不要なトークンや秘密鍵が公開されていない。

依存関係の供給網監査で目指すべきなのは、より長い一覧を作ることではありません。入力が変わらなければ結果を検証でき、入力が変わればリリースが停止し、レビュー担当者が変更箇所を正確に把握できる――そのような再現可能な判断基準を確立することが目的です。

よくある質問

Package.resolvedとPodfile.lockをコミットすれば部品表は不要ですか?

必要です。ロックファイルは解決結果を固定し、部品表は名前、バージョン、リビジョン、取得元、ハッシュを統一形式にして比較と保管を可能にします。

依存関係ゲートはXcodeの完全ビルド前と後のどちらで実行しますか?

クリーンな依存関係解決の直後、完全ビルドの前に実行します。未提出の差分をプロジェクトのスクリプトフェーズ実行前に検出できます。

間接依存関係のリビジョンが突然変わった場合はどうしますか?

リリースを止め、親となる依存関係、変更内容、ライセンスを確認します。分離したブランチで再解決とテストを行ってから監査記録と共に反映します。

ZoomMini クラウドMac

ビルド、テスト、実験に専用物理ノードを選択

2種類のM4構成を、シンガポール、東京、ソウル、香港のノードで提供しています。実際の利用可否は、注文時に返される結果をご確認ください。

モデルを選んで注文