Сбой сборки GitHub Actions macos-latest не следует лечить повторным запуском или массовым обновлением зависимостей: сначала зафиксируйте фактический образ, архитектуру, Xcode и состояние кеша в журнале Set up job. Если среда изменилась — временно закрепите проверенный runner; если вам постоянно нужны определённый Xcode, долговечный кеш, приватная сеть или непрерывный процесс, перенесите задачу на управляемый удалённый Mac self-hosted runner.

Эта инструкция предназначена для:

  • разработчиков, поддерживающих сборку, тестирование и публикацию iOS- или macOS-приложений;
  • DevOps-инженеров, выбирающих между GitHub-hosted runner и собственным узлом;
  • платформенных команд, которым нужно контролировать подпись, приватные инструменты и кеши.

Последнее обновление: 21 августа 2026 года. Состояние macOS 26, соответствие macos-latest и содержимое образов сверены с официальным объявлением GitHub о миграции образов, перечнем macOS 26 в репозитории runner-images и документацией Apple по выбору Command Line Tools.

Сначала сравните среду, а не код

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

Начните с последнего успешного запуска и первого неуспешного запуска. Для каждого сохраните:

  • фактическое значение runner.os, runner.arch и ImageOS;
  • название образа и его версию из блока Set up job;
  • выбранный Xcode и путь xcode-select;
  • версии Swift, Ruby, Node.js, Python, Homebrew и менеджера зависимостей;
  • состояние кеша и ключ, по которому он был восстановлен;
  • первый упавший шаг, а не только итоговое сообщение о завершении job.

Сведения о переменных окружения и контексте выполнения сверяйте с документацией GitHub Actions по переменным. Не полагайтесь на отображаемый ярлык macos-latest: он указывает канал образа, но не заменяет доказательство из конкретного запуска.

Разделите результат на две ветви:

  1. Среда совпадает, а код или входные данные отличаются. Сравните коммиты, lock-файлы, секреты, параметры workflow и артефакты. Дальнейшее расследование относится к проекту.
  2. Образ, архитектура, Xcode или установленный инструмент отличаются. Не обновляйте всё сразу. Переходите к диагностике источника изменения, иначе вы уничтожите исходное состояние, которое нужно сравнить.

У плавающего образа есть несколько скрытых издержек:

  • одинаковый YAML может запускаться с разным набором предустановленных инструментов;
  • восстановленный кеш способен содержать бинарники другой архитектуры;
  • подпись и доступ к связке ключей зависят не только от компилятора, но и от сессии, сертификатов и профилей;
  • повторяемость снижается, когда workflow использует «то, что уже установлено» вместо явной установки версии.

macos-latest и фактический образ: ярлык против журнала

Чтобы понять, какой образ получил GitHub-hosted runner, откройте самый ранний участок лога Set up job. Ищите строки с образом macOS, версией операционной системы, архитектурой и предустановленным программным обеспечением. Затем сопоставьте их с актуальным списком macOS 26 runner-images.

В диагностический шаг удобно добавить явный вывод:

- name: Record runner environment
  shell: bash
  run: |
    set -euxo pipefail
    sw_vers
    uname -a
    uname -m
    xcode-select -p
    xcodebuild -version
    xcrun --sdk macosx --show-sdk-path
    ruby --version || true
    node --version || true
    python3 --version || true
    brew --prefix || true

Команды не исправляют проблему — они создают снимок, который можно сравнить с успешным запуском. Для iOS-проекта дополнительно выведите доступные SDK и симуляторы:

xcodebuild -showsdks
xcrun simctl list runtimes

Если в логе обнаружен macOS 26, это ещё не означает, что любой проект обязан работать на нём без изменений. Сверяйте требования конкретной версии Xcode и поддерживаемые SDK с официальной документацией Apple по настройке инструментов Xcode. Список runner-images — это описание текущего образа, а не бессрочная гарантия его состава.

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

ARM против Intel: бинарник, пакет и кеш должны совпадать

Переход между ARM и Intel проявляется не всегда в первой строке ошибки. Частые признаки — bad CPU type in executable, невозможность загрузить расширение, падение нативного Node-модуля, ошибка Ruby Gem или линковка с библиотекой, собранной под другую архитектуру.

Проверяйте цепочку из трёх уровней:

uname -m
file path/to/binary
brew --prefix

uname -m показывает архитектуру текущего процесса. file помогает увидеть архитектуры конкретного исполняемого файла или библиотеки. brew --prefix показывает, откуда загружаются пакеты Homebrew. Если эти сведения не согласуются с содержимым кеша, ярлык runner сам по себе ничего не доказывает.

Особенно внимательно проверяйте:

  • Ruby Gem с нативными расширениями;
  • пакеты Homebrew, от которых зависят OpenSSL или другие библиотеки;
  • Node.js-модули с предварительно собранными бинарниками;
  • Swift Package Manager и сторонние XCFramework;
  • кеши DerivedData, Pods, Carthage или промежуточных артефактов;
  • скрипты, которые выбирают пакет по архитектуре машины.

Выбор между ARM и Intel делайте по зависимости, а не по предположению, что «новее всегда лучше».

  • Фиксируйте архитектуру, если проект использует закрытый бинарный SDK, плагин или инструмент, доступный только в одной сборке.
  • Пересобирайте зависимости и кеши, если обе архитектуры поддерживаются, но старые артефакты были перенесены между средами.
  • Не смешивайте кеши ARM и Intel одним ключом.
  • Если переход неизбежен, проведите отдельный прогон без кеша, затем создайте новый кеш с архитектурой и версией инструментария в ключе.

Пример более безопасного ключа:

key: >
  ${{ runner.os }}-${{ runner.arch }}-
  xcode-${{ matrix.xcode }}-
  ${{ hashFiles('**/Package.resolved', '**/Podfile.lock') }}

Формат ключа зависит от вашего workflow, но логика обязательна: операционная система, архитектура, Xcode и lock-файлы должны участвовать в идентификации артефакта. Возможности и ограничения кеширования сверяйте с документацией GitHub по dependency caching.

Xcode: плавающий выбор против явно заданного инструментария

Если сборка началась падать после изменения образа, проверьте три разные ситуации:

  1. требуемого Xcode действительно нет в образе;
  2. Xcode присутствует, но выбран другой Developer Directory;
  3. Xcode выбран правильно, однако проект или зависимость ещё не совместимы с новым SDK.

Не объединяйте их в одну ошибку «Xcode сломан». Сначала выведите доступные приложения и текущий выбор:

ls -1 /Applications | grep -i Xcode || true
xcode-select -p
xcodebuild -version

Когда нужная версия установлена, выбирайте её явно:

sudo xcode-select -s /Applications/Xcode_XX.X.app
xcodebuild -runFirstLaunch
xcodebuild -version

Вместо XX.X подставьте версию, которую вы проверили в официальном списке образа и в требованиях проекта. Не оставляйте в production workflow путь, существование которого не подтверждается текущим образом.

Второй вариант — закрепить runner-метку, на которой уже прошли контрольные проверки. Это временная мера: она уменьшает риск следующего автоматического перехода, но не заменяет установку зависимостей и проверку Xcode внутри workflow. Когда образ перестаёт соответствовать требованиям или нужная версия удаляется, фиксированная метка лишь переносит проблему на более позднюю дату.

Надёжнее разделить этапы:

  • явно выбрать Xcode;
  • проверить xcodebuild -version до установки зависимостей;
  • вывести SDK и simulator runtime;
  • выполнить чистую сборку;
  • только после этого разрешить кеширование;
  • отдельно проверить архив и подпись.

Так вы отличите отсутствие инструмента от несовместимости проекта. Вторая причина часто скрывается за сообщением компилятора, тогда как первая подтверждается списком /Applications и ошибкой выбора Developer Directory.

Предустановленные пакеты против воспроизводимой установки

Ruby, Node.js, Python, Homebrew, OpenSSL и системные утилиты могут измениться вместе с образом. Даже если основной Xcode остался прежним, установочный скрипт может получить другой путь к библиотеке, иной параметр компилятора или новый формат вывода команды.

Проверьте в логе:

ruby --version
gem env
node --version
npm --version
python3 --version
brew config
openssl version

Затем найдите, какие из этих версий реально используются сборкой, а какие просто присутствуют в системе. Наличие пакета в образе не означает, что он является частью стабильного контракта вашего проекта.

В workflow стоит явно:

  • выбрать версию Ruby через проверенный менеджер;
  • использовать lock-файл для Gem и JavaScript-зависимостей;
  • устанавливать нужную версию Node.js и Python, а не принимать системную;
  • фиксировать формулу Homebrew или заменять её артефактом с контролируемой версией;
  • проверять пути заголовков и библиотек OpenSSL;
  • очищать кеш после изменения архитектуры или Xcode.

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

Подпись и симулятор: отдельный слой отказа

Успешная компиляция не означает, что публикационный pipeline восстановлен. Архивирование, подписание, загрузка и запуск тестов на симуляторе имеют собственные точки отказа.

Архив собирается, но подпись не проходит

Соберите сведения о:

  • выбранной схеме и конфигурации;
  • идентификаторе команды и bundle identifier;
  • доступных сертификатах;
  • профиле provisioning;
  • состоянии временной связки ключей;
  • правах процесса, выполняющего xcodebuild.

Секреты, пароли и содержимое сертификатов не выводите в лог. Используйте только заполнители вроде ${{ secrets.SIGNING_PASSWORD }}. Если workflow создаёт временную keychain, проверьте её создание, разблокировку, импорт сертификата и удаление после job. Ошибка после смены образа может быть связана с разрешениями или способом работы с keychain, а не с исходным кодом.

Компиляция проходит, но тесты не видят симулятор

До тестов сохраните вывод:

xcodebuild -showsdks
xcrun simctl list devices
xcrun simctl list runtimes

Сопоставьте destination с реально установленным runtime. Не подставляйте идентификатор устройства, который существовал в старом образе. Если runtime отсутствует, это проблема состава среды; если runtime есть, но устройство не запускается, исследуйте состояние CoreSimulator и параметры job.

Командная сборка работает, а интерактивный шаг — нет

GitHub-hosted runner выполняет команды в автоматизированной сессии. Скрипт, рассчитывающий на открытый графический сеанс, ручное подтверждение или сохранённое состояние пользователя, может пройти локально и упасть в CI. Отдельно проверяйте доступ к keychain, переменные окружения и команды, требующие GUI.

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

План восстановления: контрольные точки от журнала до production

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

Этап 1. Зафиксировать исходные данные

Сохраните успешный и неуспешный логи, commit SHA, lock-файлы, параметры matrix, секреты по именам и ключ кеша. Выпишите первое отличие, а не все последующие ошибки.

Этап 2. Повторить без изменений

Запустите тот же commit с тем же workflow. Цель — понять, воспроизводится ли сбой. Не меняйте одновременно Xcode, кеш, зависимости и runner-метку: иначе результат невозможно интерпретировать.

Этап 3. Получить снимок среды

Добавьте вывод sw_vers, uname -m, xcode-select, xcodebuild, SDK, симуляторов и версий менеджеров пакетов. Сопоставьте его с официальным образом и предыдущим успешным запуском.

Этап 4. Изолировать архитектуру

Запустите чистую установку зависимостей без старого кеша. Проверьте бинарники через file, пути Homebrew и архитектуру процесса. После успешного прогона создайте новый архитектурный кеш.

Этап 5. Изолировать Xcode

Явно выберите проверенную версию и повторите чистую сборку. Если нужного Xcode нет, закрепите совместимый образ только как краткосрочное восстановление и запланируйте перенос проверки версии внутрь workflow.

Этап 6. Проверить подпись и симуляторы

Отдельными шагами проверьте keychain, сертификаты, provisioning, simulator runtime и destination. Не объединяйте диагностику подписи с диагностикой компилятора в один нечитаемый скрипт.

Этап 7. Прогнать четыре сценария

Перед возвратом в production выполните:

  • чистую сборку без кеша;
  • сборку с новым кешем;
  • тесты на требуемом simulator runtime;
  • архивирование и подпись.

Затем перезапустите узел или исполнитель и повторите критический сценарий. Это особенно важно для self-hosted runner: состояние после перезагрузки должно быть таким же, как до неё.

Для текущего workflow отметьте результаты чек-листом:

  • [ ] В Set up job записаны образ, версия macOS и архитектура.
  • [ ] Снимок среды сопоставлен с последним успешным запуском.
  • [ ] Xcode выбран явно, а версия подтверждена командой xcodebuild -version.
  • [ ] Кеш разделён по ОС, архитектуре, Xcode и lock-файлам.
  • [ ] Чистая установка зависимостей прошла без старых артефактов.
  • [ ] Доступные SDK и simulator runtime соответствуют destination.
  • [ ] Временная keychain создаётся и разблокируется без вывода секретов.
  • [ ] Проверены компиляция, кешированная сборка, тесты, архив и подпись.
  • [ ] После перезапуска runner критический сценарий повторён успешно.

Фиксированный runner против удалённого Mac: решение по ограничениям

Фиксированная метка подходит для временного восстановления, когда сбой появился недавно, задача не хранит состояние, а проект можно полностью подготовить внутри job. Этот путь требует последующей проверки: образ может измениться, а нужный Xcode — исчезнуть из доступного набора.

Управляемый удалённый Mac self-hosted runner оправдан, если одновременно важны несколько условий:

  • фиксированный Xcode должен оставаться доступным между запусками;
  • кеш DerivedData или зависимостей велик и его нерационально создавать заново;
  • сборке нужен приватный сервис, внутренний репозиторий или закрытая сеть;
  • подпись требует контролируемого доступа к физическому узлу и keychain;
  • pipeline должен оставаться доступным между job, а не начинаться с чистой среды;
  • платформа отвечает за обслуживание, обновления и аудит самого runner.

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

Если вам нужен именно такой изолированный этап проверки, аренда удалённого Mac для тестового runner позволяет сначала прогнать реальный workflow, а не переносить production вслепую. Для сравнения вариантов оплаты можно открыть условия аренды Mac, но решение принимайте после проверки Xcode, кеша, подписи и приватной сети на вашем проекте.

В документации GitHub по hosted runners отдельно учитывайте границы среды GitHub-hosted runner. Более крупные варианты имеют собственные характеристики и правила, поэтому нельзя переносить выводы о стандартном runner на любой другой тип исполнителя; доступные параметры проверяйте по официальному описанию larger runners.

На практике текущий плавающий вариант проигрывает управляемому удалённому Mac там, где нужны постоянный кеш, стабильный Xcode и доступ к внутренним ресурсам. У GitHub-hosted runner также нет гарантии, что следующий запуск будет иметь тот же локальный диск и тот же набор предустановленных инструментов. Поэтому разумный переход — не заменить всю систему в день сбоя, а сначала воспроизвести workflow на отдельном удалённом Mac, пережить перезапуск узла и сравнить все четыре контрольных сценария. Если результаты стабильны, перенос production будет техническим решением, а не реакцией на единичную ошибку.