Удалённые команды часто воспринимают CI runner на облачном Mac как постоянно работающую инфраструктуру. Однако сам runner тоже необходимо обновлять. Самый опасный подход — перезаписать каталог программы и перезапустить её прямо во время выполнения задания. Процесс сборки может продолжить использовать старые файлы, тогда как процесс планировщика уже загрузит новые компоненты. В результате возникнет смешанное состояние, которое сложно воспроизвести. Надёжнее сначала прекратить приём новых заданий и дождаться завершения текущего, а затем выполнить переключение с помощью неизменяемых каталогов версий и атомарной замены символической ссылки.
Сначала определите безопасные границы обновления
Контролируемое обновление должно соответствовать как минимум четырём условиям: runner больше не принимает новые задания, текущее задание завершается естественным образом, старая и новая версии могут существовать одновременно, а после неудачного переключения можно немедленно восстановить прежнюю версию. Под «освобождением» здесь понимается не очистка рабочего каталога и не принудительное завершение сборки, а приостановка runner на границе между заданиями.
Во время обновления важнее всего не то, запущена ли служба, а то, завершилось ли старое задание и не успело ли начаться новое.
Перед началом запишите текущую версию, PID процесса runner, идентификатор выполняемого задания и рабочий каталог. Если задание включает сборку Xcode, также проверьте наличие xcodebuild, тестовых процессов и дочерних процессов экспорта архива. Нельзя ориентироваться только на состояние главного процесса runner: даже после его завершения некорректный скрипт может оставить дочерние процессы работающими в фоне.
Рекомендуется заранее оформить критерии обновления в виде чётких правил: сколько времени может выполняться текущее задание, через какой период ожидания нужно перейти к ручной проверке и какие smoke-тесты должны завершиться успешно до возобновления приёма заданий. Команда должна определить эти правила с учётом продолжительности проектов, а не принимать решения на месте после возникновения сбоя.
Разделите программу, конфигурацию и рабочий каталог
Программу runner следует размещать в отдельных каталогах для каждой версии, а постоянную конфигурацию и рабочий каталог сборок — за пределами каталогов версий. Простой вариант структуры выглядит так:
/opt/ci-runner/
├── current -> releases/runner-current
├── previous-target
├── drain
├── active.pid
├── config/
├── work/
└── releases/
├── runner-old/
└── runner-current/
В config хранятся регистрационные данные и закрытые настройки, доступ к которым следует ограничить пользователем runner. В work находятся извлечённые исходные файлы и временные данные сборки, а в releases — только программа. Не копируйте токены в каталог каждой версии и не позволяйте пакету обновления перезаписывать рабочий каталог.
При таком разделении обновление меняет только цель ссылки current. Старая версия остаётся в полной сохранности, поэтому для отката не потребуется повторное скачивание или установка. Удалять старые версии следует только после завершения периода наблюдения, сохраняя как минимум одну последнюю проверенную версию.
Освобождайте runner на границе заданий с помощью wrapper
Проще всего контролировать модель, при которой runner выполняет только одно задание, завершается, а затем wrapper решает, следует ли принимать следующее. Параметры однократного выполнения различаются у разных runner, поэтому сначала оформите фактическую команду запуска как $RUNNER_CMD, а затем используйте общий цикл:
#!/bin/zsh
set -u
root=/opt/ci-runner
lock="$root/wrapper.lock"
mkdir "$lock" 2>/dev/null || exit 1
cleanup() {
rm -f "$root/active.pid"
rmdir "$lock" 2>/dev/null
}
trap cleanup EXIT INT TERM
while [[ ! -e "$root/drain" ]]; do
"$root/current/bin/$RUNNER_CMD" run --once &
child=$!
print -r -- "$child" > "$root/active.pid"
if wait "$child"; then
status=0
else
status=$?
fi
rm -f "$root/active.pid"
if (( status != 0 )); then
sleep 10
fi
done
При подготовке к обновлению выполните touch /opt/ci-runner/drain. Wrapper позволит текущему дочернему процессу завершиться, но не запустит следующую итерацию. Затем прочитайте active.pid и с помощью kill -0 PID проверьте, существует ли процесс. Пустой PID-файл не означает, что машина свободна: необходимо также проверить связанные с заданием дочерние процессы сборки и тестирования.
Если runner не поддерживает завершение после одного задания, используйте его штатную функцию приостановки приёма новых заданий и убедитесь, что после приостановки задания не загружаются заранее. Если подтвердить это нельзя, сначала смоделируйте длительное задание в изолированной среде и проследите за поведением runner при освобождении.
Проверьте пакет обновления и выполните атомарное переключение
Распакуйте новую версию во временный каталог, проверьте её и только затем переименуйте каталог, переместив его в releases. Контрольная сумма скачанного пакета должна быть взята из доверенной командой записи о выпуске. Нельзя генерировать её на месте рядом с тем же непроверенным архивом.
root=/opt/ci-runner
release="$root/releases/runner-next"
archive=/tmp/runner-next.tar.gz
expected="$EXPECTED_SHA256"
actual="$(shasum -a 256 "$archive" | awk '{print $1}')"
[[ "$actual" = "$expected" ]] || exit 2
mkdir -p "$release"
tar -xzf "$archive" -C "$release"
"$release/bin/$RUNNER_CMD" --version || exit 3
old="$(readlink "$root/current")"
print -r -- "$old" > "$root/previous-target"
ln -sfn "$release" "$root/current.next"
mv -fh "$root/current.next" "$root/current"
До переключения символической ссылки убедитесь, что программа в новом каталоге исполняема, предназначена для правильной архитектуры и может читать внешнюю конфигурацию. Само переключение должно занимать как можно меньше времени. Не включайте в критическую секцию распаковку, установку зависимостей или скачивание данных по сети. Если обновление также требует миграции конфигурации, сначала скопируйте её во временный файл, проверьте формат и только затем выполните атомарную замену.
Не допускайте изменения прав при распаковке
Архив может содержать другие сведения о владельцах или правах доступа. До переключения убедитесь, что пользователи, отличные от пользователя runner, не могут изменять программные файлы. Для конфиденциальной конфигурации обычно устанавливают права 600, а для каталога конфигурации — 700. Также проверьте, что рабочий каталог по-прежнему принадлежит исходному пользователю runner, чтобы ошибка записи не обнаружилась лишь при первом запуске новой версии.
Smoke-тесты и быстрый откат
Не удаляйте drain сразу после переключения. Сначала вручную запустите одно контролируемое тестовое задание. Как минимум убедитесь, что runner может прочитать конфигурацию, создать временный рабочий каталог, вызвать инструменты разработки, выполнить минимальную сборку, сохранить артефакты, корректно очистить временные данные и завершиться.
При проверке журналов уделите особое внимание путям. Новый процесс должен запускаться из нового каталога, на который указывает current, но кеш, конфигурация и рабочие данные должны по-прежнему сохраняться по постоянным путям. Если в журналах одновременно встречаются пути старой и новой версий, значит, старый процесс ещё не завершился. Возобновлять приём заданий в таком состоянии нельзя.
В случае сбоя прочитайте прежнюю цель ссылки из previous-target, переключитесь обратно тем же способом с временной ссылкой и повторно запустите минимальное задание. Не исправляйте новую версию непосредственно в её каталоге, одновременно повторяя тесты: это нарушает принцип неизменяемых выпусков. После подтверждения восстановления старой версии удалите неудачную версию и заново подготовьте пакет выпуска.
После успешной проверки выполните rm -f /opt/ci-runner/drain, чтобы менеджер процессов снова запустил wrapper. В завершение проследите как минимум за одним реальным заданием: его получением, сборкой, архивированием артефактов и статусом завершения. Запишите в журнал обновления номер версии, результат переключения и цель отката. Обновление можно считать завершённым только после этого, а не сразу после появления у runner статуса «онлайн».
Часто задаваемые вопросы
Почему CI runner не следует обновлять поверх текущего каталога?
Активное задание может одновременно прочитать старые и новые файлы, а исправная версия перестанет быть готовой к откату. Отдельные каталоги версий исключают эту неопределённость.
Нужно ли останавливать текущую сборку при дренировании runner?
Нет. Маркер дренирования запрещает брать следующее задание, но активный дочерний процесс должен завершиться штатно. Перед принудительной остановкой сохраняют диагностику.
Что проверить сразу после обновления runner?
Проверьте версию, регистрацию, запись в рабочий каталог, минимальную сборку, сохранение артефакта и права конфиденциальных файлов. При сбое выполните откат.
Нужна выделенная машина для сборки на Apple Silicon?
Арендуйте выделенный физический узел на день, неделю, месяц или квартал для сборки в Xcode, iOS CI, удалённой разработки и автоматизации.