Аудит цепочки зависимостей iOS на облачном Mac

Аудит цепочки зависимостей iOS на облачном Mac

Один и тот же коммит вчера мог успешно пройти сборку, а сегодня получить другую транзитивную зависимость — это одна из самых трудно расследуемых проблем при сборке на облачном Mac. Внешне сбой может выглядеть как обычная ошибка компиляции, хотя на самом деле изменились ревизия пакета, загруженное содержимое или добавленный этап со скриптом. Простого повторного запуска недостаточно: для каждой сборки должны быть доступны ответы на три вопроса — что было разрешено, изменилось ли содержимое и прошло ли изменение проверку.

Сначала определите, какие свидетельства нужно сохранять

Аудит зависимостей должен охватывать как прямые, так и транзитивные зависимости SwiftPM и CocoaPods. Свидетельства удобно разделить на три уровня:

Уровень Свидетельства На какой вопрос отвечают
Объявление Package.swift, Podfile, ссылки в проекте Xcode Что команда намерена подключить
Разрешение Package.resolved, Podfile.lock Какая версия или ревизия фактически зафиксирована
Сборка Ведомость зависимостей, хеши файлов, журналы сборки Что фактически использовалось в этой сборке

Файлы Package.resolved и Podfile.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 -version, swift --version, ruby --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 в репозитории?

Нет. Lock-файлы фиксируют результат разрешения, а отдельная ведомость приводит имена, версии, ревизии и хеши к единому формату для сравнения и архивирования.

Когда следует запускать проверку зависимостей?

После чистого разрешения, но до полной сборки Xcode. Так незаявленное изменение обнаруживается до выполнения скриптовых фаз проекта.

Что делать при неожиданном изменении транзитивной зависимости?

Остановите выпуск, определите родительскую зависимость, проверьте новую ревизию и лицензию, затем выполните разрешение и тесты в отдельной ветке.

ZoomMini Mac в облаке

Выберите выделенный физический узел для сборки, тестирования и экспериментов

Две конфигурации M4 доступны в узлах Сингапура, Токио, Сеула и Гонконга. Ориентируйтесь на результат, который система вернёт при оформлении заказа.

Выбрать модель и заказать