Инженерное руководство MiniRent

Как настроить инкрементальный контроль стиля Swift на Cloud Mac

Как настроить инкрементальный контроль стиля Swift на Cloud Mac

При полной проверке форматирования среднего репозитория Swift на облачном Mac проблема обычно не в низкой скорости самих инструментов. Гораздо больше времени уходит на то, что при каждом запросе на слияние повторно сканируются неизменённые исходные файлы, каталоги сгенерированного кода и внешние зависимости. Ситуацию усложняет использование разных версий инструментов локально и в CI: одна и та же строка кода может проходить проверку в одном окружении и отклоняться в другом. Решение — не отключать правила, а хранить в репозитории версии инструментов, конфигурационные файлы и логику вычисления изменений.

Разделите зоны ответственности инструментов

SwiftFormat подходит для детерминированных и автоматически исправляемых аспектов форматирования: отступов, переносов строк и лишних пробелов. SwiftLint лучше использовать для выявления рискованных конструкций, таких как принудительное приведение типов, слишком длинные функции и нарушения правил именования. Если оба инструмента контролируют одно и то же правило, разработчик может сначала отформатировать код, а затем получить отказ от статического анализатора.

Для начала полезно составить простую таблицу распределения правил:

Тип проверки Ответственный инструмент Поведение в запросе на слияние
Отступы, пробелы, переносы параметров SwiftFormat Только проверка, без автоматического изменения
Принудительное приведение и принудительное извлечение SwiftLint Завершение с ошибкой и указанием расположения файла
Сгенерированный код и внешние зависимости Исключаются из обоих инструментов Не попадают в список изменённых файлов
Унаследованные предупреждения Базовый уровень SwiftLint Блокируются только новые проблемы

Не запускайте в CI команды, которые напрямую изменяют исходный код. Проверка должна только сообщать о расхождениях. Разработчик исправляет их локально и затем отправляет изменения — так состояние репозитория остаётся воспроизводимым.

Храните версии и правила в репозитории

Не полагайтесь на «текущие установленные» версии инструментов на облачном Mac. Сначала выберите версии, проверенные командой, а затем укажите ожидаемые значения в скрипте. Ниже используются SwiftFormat 0.54.3 и SwiftLint 0.55.1; в реальном проекте их следует заменить версиями, проверенными вашей командой.

Файл .swiftformat может оставаться компактным:

--swiftversion 5.10
--indent 4
--wraparguments before-first
--exclude .build,DerivedData,Vendor,Generated

В .swiftlint.yml явно задаются области исходного кода и исключаемые каталоги:

included:
  - Sources
  - Tests
excluded:
  - .build
  - DerivedData
  - Vendor
  - Generated
only_rules:
  - force_cast
  - force_try
  - trailing_whitespace
  - unused_import

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

Корректно извлекайте изменённые Swift-файлы из запроса на слияние

Команда git diff HEAD^ подходит только для веток с одним коммитом. Если запрос на слияние содержит несколько коммитов или ветка была перебазирована, такой подход может пропустить файлы. Надёжнее сначала определить общего предка текущего коммита и целевой ветки, а затем получить список добавленных, скопированных, изменённых и переименованных файлов.

set -euo pipefail

expected_format="0.54.3"
expected_lint="0.55.1"

[[ "$(swiftformat --version)" == "$expected_format" ]]
[[ "$(swiftlint version)" == "$expected_lint" ]]

base_ref="${LINT_BASE_REF:-origin/main}"
merge_base="$(git merge-base HEAD "$base_ref")"
changed=("${(@0)$(git diff --name-only -z --diff-filter=ACMR "$merge_base" HEAD)}")

swift_files=()
for file in "${changed[@]}"; do
  [[ "$file" == *.swift ]] || continue
  [[ "$file" == .build/* ]] && continue
  [[ "$file" == DerivedData/* ]] && continue
  [[ "$file" == Vendor/* ]] && continue
  [[ "$file" == Generated/* ]] && continue
  swift_files+=("$file")
done

(( ${#swift_files[@]} > 0 )) || exit 0

swiftformat --lint "${swift_files[@]}"
swiftlint lint --strict --force-exclude "${swift_files[@]}"

Здесь используется список файлов с разделением нулевыми символами, поэтому пути с пробелами не разбиваются на части. Параметр --diff-filter=ACMR исключает удалённые файлы, и инструменты проверки не получают несуществующие пути. Имя целевой ветки передаётся через переменную окружения, благодаря чему один и тот же скрипт можно запускать как для основной, так и для релизных веток.

Учитывайте неглубокое клонирование

Если git merge-base не может найти общего предка, проблема обычно не в скрипте, а в том, что CI загрузил слишком короткую историю коммитов. Сначала увеличьте глубину клонирования и убедитесь, что ссылка на целевую ветку доступна, а уже затем запускайте скрипт. Не возвращайтесь к HEAD^: при разной структуре веток такая проверка будет давать непоследовательные результаты.

Сделайте ошибки локализуемыми и воспроизводимыми

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

Для локального воспроизведения разработчику достаточно получить ссылку на целевую ветку и запустить тот же скрипт:

git fetch origin main
LINT_BASE_REF=origin/main zsh Scripts/lint-changed-swift.zsh

Распространённая ошибка — помещать команду проверки в конвейер, а затем считывать код завершения другой команды. Ещё одна проблема возникает, когда ради красивого журнала добавляют || true, тем самым скрывая реальный сбой. При включённом set -euo pipefail задача завершается, как только любой инструмент возвращает ненулевой статус. Если CI должен дополнительно обработать журнал, сначала сохраните исходный код завершения, выведите сводку, а затем завершите процесс с тем же кодом.

Контролируйте долгосрочные затраты с помощью двухуровневой стратегии

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

  1. Для каждого запроса на слияние запускайте проверку изменений. Она должна быть быстрой, стабильной и полностью воспроизводимой локально.
  2. По расписанию запускайте полную проверку репозитория, чтобы выявлять смещение базового уровня, неработающие исключения и старые предупреждения, которые долго остаются без внимания.
  3. Обновления правил оформляйте отдельными коммитами и не смешивайте их с функциональными изменениями, чтобы масштабные правки форматирования было проще проверять.
  4. У каталогов сгенерированного кода должен быть явно определённый источник. Исключайте их и в конфигурации инструментов, и в скрипте вычисления изменений, чтобы порядок задач генерации не влиял на результат.

Перед запуском ещё раз проверьте, существует ли ссылка на целевую ветку, точно ли совпадают версии инструментов, учитываются ли переименованные файлы, исключены ли удалённые файлы, безопасно ли обрабатываются пути с пробелами и корректно ли завершается скрипт при отсутствии изменений в Swift-файлах. После этого контроль стиля кода перестанет быть «скриптом, который иногда выдаёт ошибку» и станет надёжной границей приёма изменений.

Часто задаваемые вопросы

Можно ли полностью отказаться от проверки всего репозитория?

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

Должны ли SwiftFormat и SwiftLint проверять одинаковые правила?

Нет. SwiftFormat отвечает за автоматически исправляемое форматирование, а SwiftLint — за опасные конструкции и правила команды. Дублирование лучше исключить.

Выделенное физическое устройство

Запустите следующую задачу разработки или сборки на облачном Mac mini

Выберите одну из двух конфигураций M4, четырех сроков аренды и пяти доступных площадок. Фактический статус доступности отображается в консоли в реальном времени.

Выбрать конфигурацию и арендовать