Руководство

Как подключить 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.

Похожие руководства