Cómo añadir un Mac como agente de Jenkins para builds de iOS
Para añadir un Mac a Jenkins, crea un nodo permanente que se conecte al controlador. En el Mac, instala Java 21 y ejecuta agent.jar con la opción -webSocket. Mete ese comando en un LaunchAgent y activa el inicio de sesión automático, para que el agente vuelva a conectarse tras un reinicio. Luego ponle al nodo la etiqueta macos y envíale tus stages de iOS.
Lo que necesitas antes
- Un controlador de Jenkins al que el Mac pueda llegar por HTTPS.
- Un Mac con Apple silicon, acceso de administrador, Homebrew y Xcode.
- El Java correcto. La política de soporte de Java de Jenkins, leída en octubre de 2026, dice que LTS 2.555.1 y posteriores necesitan Java 21 o 25. La regla también vale para los agentes, no solo para el controlador.
- Una sesión de escritorio en el Mac para los últimos pasos, por compartir pantalla.
1. Instala Java en el Mac
brew install openjdk@21 /opt/homebrew/opt/openjdk@21/bin/java -version
Homebrew instala este JDK como keg-only, así que no está en tu PATH. Usa la ruta completa de arriba en todas partes. Así el agente sigue en Java 21 aunque más adelante llegue un JDK más nuevo.
2. Crea el nodo en el controlador
- Abre Manage Jenkins, luego Nodes, luego New Node. Elige Permanent Agent.
- Number of executors: 1. La propia documentación de Jenkins dice que un executor por nodo es lo más seguro. Un build de Xcode ya usa todos los núcleos.
- Remote root directory:
/Users/YOUR-USER/jenkins. - Labels:
macos xcode. - Usage: solo jobs de build con expresiones de etiqueta que coincidan con este nodo. Así los jobs de Linux no llegan a tu Mac.
- Launch method: Launch agent by connecting it to the controller.
Guarda. La página del nodo muestra ahora el comando de arranque, el nombre del agente y un secreto hexadecimal largo. El secreto va ligado al nombre del agente. Si se filtra, Jenkins dice que no vuelvas a usar ese nombre.
3. Descarga agent.jar y pruébalo a mano
Jenkins sirve el agent.jar adecuado para tu controlador en /jnlpJars/agent.jar. Guarda el secreto en un archivo y pásalo con @, para que nunca aparezca en la lista de procesos.
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
La página del nodo debería pasar a conectado. Pulsa Control C para pararlo. Con -webSocket, el agente abre una sola conexión HTTPS. Sin él, el agente también necesita el puerto TCP de entrada aparte del controlador.
4. Ejecuta el agente desde un LaunchAgent
launchd arranca el agente al iniciar sesión y lo reinicia si se cierra. Ejecuta esto desde Terminal en el escritorio del Mac. El heredoc rellena la ruta de tu carpeta de inicio.
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 -20Comprobamos este plist con plutil -lint. ¿Por qué un LaunchAgent y no un LaunchDaemon? Un daemon se ejecuta fuera de cualquier sesión. Los simuladores y el llavero de inicio de sesión viven dentro de una. GitLab y Buildkite dan la misma razón para sus agentes de Mac.
Deja el plist en ~/Library/LaunchAgents. En macOS 27 vimos que un plist en /Library/LaunchAgents rompía el inicio de sesión automático en cada arranque. Los detalles están en inicio de sesión automático en un Mac sin pantalla.
5. Haz que sobreviva a un reinicio
Activa el inicio de sesión automático para la cuenta del agente en Ajustes del Sistema, en Usuarios y grupos. Impide que el Mac se duerma. Luego reinicia y mira la página del nodo.
sudo pmset -a sleep 0 sudo shutdown -r now # then, after it is back: tail -n 20 ~/jenkins/agent.log
6. Un Jenkinsfile con un stage de 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
}
}
}Un result bundle es una carpeta, así que lo comprimimos con ditto antes de archivarlo. Abre el zip en cualquier Mac y haz doble clic en el bundle para verlo en Xcode.
¿Dos versiones de Xcode en un mismo agente? Elige una por pipeline con DEVELOPER_DIR. La página man de xcode-select de Apple dice que anula la elección global sin cambiarla. Ponlo en un bloque environment, para no afectar a otros jobs del Mac:
environment {
DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer'
}Pon ese bloque dentro de pipeline, junto a agent. Cambiar el ajuste global con sudo xcode-select -s también funciona, pero cambia a la vez todos los jobs de la máquina.
Errores comunes y soluciones
- El agente se conecta y desaparece tras un reinicio. Nadie inició sesión, así que el LaunchAgent nunca arrancó. Revisa el inicio de sesión automático.
launchctl bootstrapfalla por SSH con un error de dominio. Ejecútalo desde Terminal en el escritorio. GitLab y Buildkite documentan el mismo error para sus LaunchAgents.- El agente no arranca y menciona Java. Jenkins comprueba la versión de Java al arrancar. Usa la versión que indica la política de soporte.
xcodebuild: error: Existing file at -resultBundlePath. El workspace se conserva entre builds. Borra antes el bundle anterior, como en el Jenkinsfile.
Por qué ayuda un Mac dedicado
Un agente permanente conserva su workspace y el DerivedData de Xcode entre builds. La recompensa son los builds incrementales. En nuestro benchmark usamos la app de Wikipedia para iOS con Xcode 26.6, mediana de 3 ejecuciones. Un cambio pequeño se recompiló en 27 segundos en un M6 en caliente. Un runner macos-26 recién creado de GitHub tardó 269 segundos en el mismo job.
Cuándo no te hace falta esto
Si no usas Jenkins ya, no empieces por una sola app iOS. Un servicio alojado con runners de Mac da menos trabajo de mantenimiento. Con la tarifa de GitHub de $0.062 por minuto de macOS (comprobada en septiembre de 2026), un Mac a $139 fijos se amortiza a partir de unos 2,242 minutos al mes. Por debajo, pagar por uso sale más barato. MacRun tampoco encaja si tu controlador solo acepta agentes desde una IP fija. No tenemos IP fija.
Nuestra página de otros sistemas de CI resume Jenkins en un Mac de MacRun. La guía del pipeline de CI/CD para iOS explica qué poner en los stages.
Preguntas frecuentes
¿Un agente de Jenkins en Mac debe usar SSH o conexión entrante?
+
La conexión entrante sirve cuando el Mac puede llegar al controlador pero no al revés. Con -webSocket solo necesita HTTPS hacia el controlador.
¿Qué versión de Java necesita un agente de Jenkins en macOS?
+
La misma familia que necesita el controlador. La política de soporte de Jenkins, leída en octubre de 2026, exige Java 21 o 25 a partir de LTS 2.555.1.
¿Cuántos executors debe tener un agente de Mac?
+
Empieza con uno. Jenkins dice que un executor por nodo es lo más seguro, y un build de Xcode ya usa todos los núcleos.
¿Por qué mi agente de Jenkins en Mac no se reconecta tras un reinicio?
+
Un LaunchAgent solo se ejecuta cuando su usuario inicia sesión. Activa el inicio de sesión automático para esa cuenta y deja el plist en ~/Library/LaunchAgents.