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

Надёжная нумерация сборок iOS в облачном Mac CI

Надёжная нумерация сборок iOS в облачном Mac CI

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

Цель управления номерами сборок — не просто «автоматически прибавлять единицу». Для каждого распространяемого артефакта должно быть можно установить, кто назначил номер, какому коммиту он соответствует и какое значение фактически записано в архиве.

Сначала разделите версию и номер сборки

MARKETING_VERSION — это видимая пользователю версия, например 3.8.0. CURRENT_PROJECT_VERSION — номер сборки, который должен быть возрастающим значением, состоящим только из цифр. У этих полей разные жизненные циклы, поэтому не следует одновременно записывать в них одну и ту же метку коммита.

Поле Пример Когда изменяется Основное назначение
MARKETING_VERSION 3.8.0 При изменении версии продукта Определение функциональной версии
CURRENT_PROJECT_VERSION 18427 При каждой распространяемой архивации Различение артефактов одной версии
Коммит Git a1b2c3d При каждом коммите Определение исходного кода
Номер конвейера 5821 При каждом запуске задания Определение записи о выполнении

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

Рассматривайте номер сборки как входной параметр конвейера, а не как результат изменения исходного кода. Исходный код определяет, как использовать этот номер, а система оркестрации гарантирует его уникальность.

Создайте единый источник номеров

Выберите монотонно возрастающую последовательность

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

  1. Не повторяться в пределах одной цели публикации.
  2. Быть больше всех ранее отправленных номеров.
  3. Позволять определить коммит, ветку и запись о запуске задания.

Команда git rev-list --count HEAD подходит для проектов с одной веткой, история которой не переписывается, но не годится для поверхностного клонирования, перебазирования или нескольких веток выпуска. Разные ветки могут получить одинаковый счётчик, а после переписывания истории значение может уменьшиться. Такой подход допустим для внутренних отладочных сборок, но не должен быть единственным источником истины в сложном процессе выпуска.

В параллельных заданиях OpsVM уровень оркестрации может сначала создать BUILD_SEQUENCE, а затем передать его конкретным узлам сборки. Узлы только используют номер, не конкурируя за него и не записывая его обратно.

Отклоняйте некорректные данные на входе в задание

#!/bin/zsh
set -euo pipefail

: "${BUILD_SEQUENCE:?BUILD_SEQUENCE is required}"
: "${RELEASE_VERSION:?RELEASE_VERSION is required}"

if [[ ! "$BUILD_SEQUENCE" =~ ^[0-9]+$ ]]; then
  print -u2 "BUILD_SEQUENCE must contain digits only"
  exit 64
fi

if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  print -u2 "RELEASE_VERSION must use major.minor.patch"
  exit 64
fi

Входную проверку нужно выполнять до установки зависимостей и компиляции. Тогда отсутствие переменной не приведёт через десятки минут к созданию архива, который нельзя распространить.

Передавайте номер в команду архивации

Конвейеру не нужно изменять project.pbxproj. Переопределите параметры сборки непосредственно в конце команды xcodebuild: это не создаёт изменённых файлов в репозитории и упрощает воспроизведение сборки по журналу.

archive_path="$PWD/output/App.xcarchive"
result_path="$PWD/output/Archive.xcresult"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "$archive_path" \
  -resultBundlePath "$result_path" \
  MARKETING_VERSION="$RELEASE_VERSION" \
  CURRENT_PROJECT_VERSION="$BUILD_SEQUENCE" \
  clean archive

Все переопределения должны передаваться в той же команде архивации. Иначе тесты могут использовать один номер, а при архивации снова будет считано значение проекта по умолчанию. Если проект содержит расширения, убедитесь, что основное приложение и расширения наследуют одинаковые параметры. Не создавайте отдельный номер для каждого target, если это явно не предписано правилами выпуска.

Перед архивацией можно проверить вычисленные параметры:

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings |
awk '/MARKETING_VERSION|CURRENT_PROJECT_VERSION/ { print }'

Эта проверка прежде всего выявляет случаи, когда скрипт, файл конфигурации или параметр уровня target неожиданно переопределяет аргументы командной строки.

Не доверяйте журналу — проверяйте сам архив

Успешное завершение команды означает лишь, что архив создан, но не гарантирует, что нужные значения попали в итоговое приложение. Проверочный скрипт должен прочитать Info.plist основного приложения внутри .xcarchive и сравнить значения с входными параметрами конвейера.

app_path="$(find "$archive_path/Products/Applications" \
  -maxdepth 1 -name '*.app' -type d -print -quit)"

if [[ -z "$app_path" ]]; then
  print -u2 "Application bundle not found"
  exit 65
fi

plist="$app_path/Info.plist"
actual_version=$(/usr/libexec/PlistBuddy \
  -c "Print :CFBundleShortVersionString" "$plist")
actual_build=$(/usr/libexec/PlistBuddy \
  -c "Print :CFBundleVersion" "$plist")

[[ "$actual_version" == "$RELEASE_VERSION" ]] || exit 66
[[ "$actual_build" == "$BUILD_SEQUENCE" ]] || exit 67

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

Обрабатывайте повторы, ветки и параллельное выполнение

Зафиксируйте правила повторного запуска на уровне конвейера

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

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

Минимальный контрольный список

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

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

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

Можно ли использовать число коммитов Git как номер iOS-сборки?

Да, если история линейна и никогда не переписывается. При rebase, неглубоком клонировании и нескольких ветках выпуска нужен централизованный возрастающий номер CI.

Следует ли повторному запуску использовать прежний номер сборки?

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

Выделенный физический узел

Развёртывание облачного Mac рабочего процесса в OpsVM

Выберите одну из трёх конфигураций Apple Silicon и 6 доступных узлов; актуальный статус доступности отображается в консоли в реальном времени.

Выбрать конфигурацию и оформить заказ