Как подключить Mac к Jenkins как агент для сборок iOS
Чтобы добавить Mac в Jenkins, создайте постоянный узел, который подключается к контроллеру. На Mac установите Java 21 и запустите agent.jar с параметром -webSocket. Оберните эту команду в LaunchAgent и включите автоматический вход, чтобы агент подключался снова после перезагрузки. Затем назначьте узлу метку macos и отправляйте на него этапы iOS.
Что нужно заранее
- Контроллер Jenkins, до которого Mac может достучаться по HTTPS.
- Mac с Apple silicon, права администратора, Homebrew и Xcode.
- Правильная версия Java. Политика поддержки Java в Jenkins, которую мы читали в октябре 2026 года, требует Java 21 или 25 для LTS 2.555.1 и новее. Это правило касается и агентов, а не только контроллера.
- Сессия рабочего стола на Mac для последних шагов, через демонстрацию экрана.
1. Установите Java на Mac
brew install openjdk@21 /opt/homebrew/opt/openjdk@21/bin/java -version
Homebrew ставит этот JDK как keg-only, поэтому его нет в PATH. Везде используйте полный путь, как выше. Это ещё и удержит агента на Java 21, когда позже появится более новый JDK.
2. Создайте узел на контроллере
- Откройте Manage Jenkins, затем Nodes, затем New Node. Выберите Permanent Agent.
- Number of executors: 1. Документация Jenkins называет один executor на узел самой безопасной настройкой. Сборки Xcode и так используют все ядра.
- Remote root directory:
/Users/YOUR-USER/jenkins. - Labels:
macos xcode. - Usage: only build jobs with label expressions matching this node. Так задачи для Linux не попадут на ваш Mac.
- Launch method: Launch agent by connecting it to the controller.
Сохраните. На странице узла теперь видны команда запуска, имя агента и длинный секрет в hex. Секрет привязан к имени агента. Если он утёк, Jenkins советует больше не использовать это имя.
3. Скачайте agent.jar и проверьте вручную
Jenkins отдаёт подходящий для вашего контроллера agent.jar по адресу /jnlpJars/agent.jar. Сохраните секрет в файл и передайте его через @, чтобы он никогда не появлялся в списке процессов.
mkdir -p ~/jenkins && cd ~/jenkins curl -sO https://jenkins.example.com/jnlpJars/agent.jar echo 'PASTE-THE-SECRET' > secret-file chmod 600 secret-file /opt/homebrew/opt/openjdk@21/bin/java -jar agent.jar \ -url https://jenkins.example.com/ \ -name mac-mini-1 \ -secret @secret-file \ -workDir "$HOME/jenkins" \ -webSocket
Страница узла должна показать, что он подключён. Нажмите Control C, чтобы остановить агента. С -webSocket агент открывает одно HTTPS-соединение. Без него агенту нужен ещё и отдельный входящий TCP-порт контроллера.
4. Запускайте агента из LaunchAgent
launchd запускает агента при входе в систему и перезапускает его, если он завершился. Выполните это из Terminal на рабочем столе Mac. Heredoc сам подставит путь к вашей домашней папке.
mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/local.jenkins-agent.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>local.jenkins-agent</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/openjdk@21/bin/java</string>
<string>-jar</string><string>$HOME/jenkins/agent.jar</string>
<string>-url</string><string>https://jenkins.example.com/</string>
<string>-name</string><string>mac-mini-1</string>
<string>-secret</string><string>@$HOME/jenkins/secret-file</string>
<string>-workDir</string><string>$HOME/jenkins</string>
<string>-webSocket</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key><string>/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>LANG</key><string>en_US.UTF-8</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>ThrottleInterval</key><integer>30</integer>
<key>StandardOutPath</key><string>$HOME/jenkins/agent.log</string>
<key>StandardErrorPath</key><string>$HOME/jenkins/agent.log</string>
</dict>
</plist>
EOF
plutil -lint ~/Library/LaunchAgents/local.jenkins-agent.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.jenkins-agent.plist
launchctl print gui/$(id -u)/local.jenkins-agent | head -20Мы проверили этот plist через plutil -lint. Почему LaunchAgent, а не LaunchDaemon? Демон работает вне сессии пользователя. А симуляторы и связка ключей входа живут внутри неё. GitLab и Buildkite объясняют выбор для своих агентов на Mac так же.
Держите plist в ~/Library/LaunchAgents. На macOS 27 мы видели, как plist в /Library/LaunchAgents ломает автовход при каждой загрузке. Подробности в статье об автовходе на headless Mac.
5. Сделайте так, чтобы он переживал перезагрузку
Включите автоматический вход для учётной записи агента в System Settings, в разделе Users and Groups. Запретите Mac засыпать. Затем перезагрузите его и следите за страницей узла.
sudo pmset -a sleep 0 sudo shutdown -r now # then, after it is back: tail -n 20 ~/jenkins/agent.log
6. Jenkinsfile с этапом для iOS
pipeline {
agent { label 'macos' }
options { timeout(time: 30, unit: 'MINUTES') }
stages {
stage('Test') {
steps {
sh 'xcodebuild -version'
sh 'rm -rf build/TestResults.xcresult'
sh "xcodebuild test -project MyApp.xcodeproj -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5' -resultBundlePath build/TestResults.xcresult"
}
}
}
post {
always {
sh 'ditto -c -k --keepParent build/TestResults.xcresult build/TestResults.xcresult.zip || true'
archiveArtifacts artifacts: 'build/TestResults.xcresult.zip', fingerprint: true
}
}
}Result bundle это папка, поэтому перед архивированием мы сжимаем её через ditto. Откройте zip на любом Mac и дважды щёлкните по bundle, чтобы посмотреть его в Xcode.
Две версии Xcode на одном агенте? Выбирайте нужную для каждого пайплайна через DEVELOPER_DIR. Man-страница Apple для xcode-select говорит, что эта переменная переопределяет общесистемный выбор, не меняя его. Задайте её в блоке environment, чтобы не задеть другие задачи на Mac:
environment {
DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer'
}Поместите этот блок внутрь pipeline, рядом с agent. Сменить глобальную настройку через sudo xcode-select -s тоже можно, но это сразу меняет её для всех задач на машине.
Частые ошибки и их решения
- Агент подключается, но после перезагрузки пропадает. Никто не вошёл в систему, поэтому LaunchAgent так и не запустился. Проверьте автоматический вход.
launchctl bootstrapпадает с ошибкой домена по SSH. Выполните команду из Terminal на рабочем столе. GitLab и Buildkite описывают ту же ошибку для своих LaunchAgent.- Агент не запускается и упоминает Java. Jenkins проверяет версию Java при запуске. Используйте версию из политики поддержки.
xcodebuild: error: Existing file at -resultBundlePath. Рабочая папка сохраняется между сборками. Сначала удалите старый bundle, как в Jenkinsfile.
Чем помогает выделенный Mac
Постоянный агент сохраняет рабочую папку и DerivedData от Xcode между сборками. Выигрыш дают инкрементальные сборки. В нашем тесте мы взяли iOS-приложение Wikipedia на Xcode 26.6 и медиану из 3 запусков. После небольшого изменения пересборка заняла 27 секунд на прогретом M6. Свежий hosted раннер macos-26 у GitHub потратил на ту же задачу 269 секунд.
Когда это вам не нужно
Если вы ещё не используете Jenkins, не начинайте ради одного iOS-приложения. Hosted сервис с раннерами Mac требует меньше обслуживания. При ставке GitHub $0.062 за минуту macOS (проверено в сентябре 2026 года) Mac за фиксированные $139 окупается примерно после 2,242 минут в месяц. Ниже этого поминутная оплата дешевле. MacRun также не подойдёт, если ваш контроллер принимает агентов только с фиксированного IP. Статического IP у нас нет.
Страница о других CI-системах кратко описывает Jenkins на Mac от MacRun. Руководство по пайплайну CI/CD для iOS объясняет, что положить в этапы.
Частые вопросы
Что использовать для агента Jenkins на Mac: SSH или inbound-запуск?
+
Inbound подходит, когда Mac может достучаться до контроллера, а контроллер до Mac нет. С -webSocket ему нужен только HTTPS до контроллера.
Какая версия Java нужна агенту Jenkins на macOS?
+
Та же линейка, что нужна контроллеру. Политика поддержки Jenkins, которую мы читали в октябре 2026 года, требует Java 21 или 25 начиная с LTS 2.555.1.
Сколько executor должно быть у агента на Mac?
+
Начните с одного. Jenkins называет один executor на узел самой безопасной настройкой, а сборка Xcode и так занимает все ядра.
Почему агент Jenkins на Mac не подключается снова после перезапуска?
+
LaunchAgent запускается только после того, как его пользователь вошёл в систему. Включите автоматический вход для этой учётной записи и держите plist в ~/Library/LaunchAgents.