Einen Mac als Jenkins-Agent für iOS-Builds hinzufügen
Um einen Mac zu Jenkins hinzuzufügen, legen Sie einen permanenten Node an, der sich mit dem Controller verbindet. Installieren Sie auf dem Mac Java 21 und starten Sie agent.jar mit der Option -webSocket. Packen Sie diesen Befehl in einen LaunchAgent und schalten Sie die automatische Anmeldung ein. Dann verbindet sich der Agent nach einem Neustart wieder. Geben Sie dem Node danach das Label macos und schicken Sie Ihre iOS-Stages dorthin.
Was Sie vorher brauchen
- Einen Jenkins-Controller, den der Mac über HTTPS erreicht.
- Einen Mac mit Apple Silicon, Admin-Zugriff, Homebrew und Xcode.
- Die richtige Java-Version. Laut der Java-Support-Richtlinie von Jenkins, gelesen im Oktober 2026, brauchen LTS 2.555.1 und neuer Java 21 oder 25. Die Regel gilt auch für Agents, nicht nur für den Controller.
- Eine Desktop-Sitzung auf dem Mac für die letzten Schritte, per Bildschirmfreigabe.
1. Java auf dem Mac installieren
brew install openjdk@21 /opt/homebrew/opt/openjdk@21/bin/java -version
Homebrew installiert dieses JDK als keg-only, es liegt also nicht in Ihrem PATH. Nutzen Sie überall den vollen Pfad von oben. So bleibt der Agent auch dann bei Java 21, wenn später ein neueres JDK kommt.
2. Den Node auf dem Controller anlegen
- Öffnen Sie Manage Jenkins, dann Nodes, dann New Node. Wählen Sie Permanent Agent.
- Number of executors: 1. Die Doku von Jenkins nennt einen Executor pro Node die sicherste Einstellung. Xcode-Builds nutzen ohnehin jeden Kern.
- Remote root directory:
/Users/YOUR-USER/jenkins. - Labels:
macos xcode. - Usage: Only build jobs with label expressions matching this node. So bleiben Linux-Jobs von Ihrem Mac fern.
- Launch method: Launch agent by connecting it to the controller.
Speichern Sie. Die Seite des Node zeigt jetzt den Startbefehl, den Agent-Namen und ein langes Hex-Secret. Das Secret ist an den Agent-Namen gebunden. Gelangt es nach außen, sollen Sie den Namen laut Jenkins nicht weiterverwenden.
3. agent.jar herunterladen und von Hand testen
Jenkins liefert unter /jnlpJars/agent.jar die passende agent.jar für Ihren Controller. Speichern Sie das Secret in einer Datei und übergeben Sie es mit @. Dann taucht es nie in einer Prozessliste auf.
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
Die Seite des Node sollte auf verbunden wechseln. Beenden Sie den Agent mit Control C. Mit -webSocket baut der Agent eine einzige HTTPS-Verbindung auf. Ohne braucht er zusätzlich den separaten eingehenden TCP-Port des Controllers.
4. Den Agent über einen LaunchAgent starten
launchd startet den Agent bei der Anmeldung und startet ihn neu, wenn er endet. Führen Sie das im Terminal auf dem Desktop des Mac aus. Das Heredoc setzt den Pfad zu Ihrem Home-Ordner ein.
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 -20Wir haben diese plist mit plutil -lint geprüft. Warum ein LaunchAgent und kein LaunchDaemon? Ein Daemon läuft außerhalb jeder Anmeldesitzung. Simulatoren und der Anmelde-Schlüsselbund leben in einer solchen Sitzung. GitLab und Buildkite nennen für ihre Mac-Agents denselben Grund.
Lassen Sie die plist in ~/Library/LaunchAgents. Unter macOS 27 haben wir gesehen, dass eine plist in /Library/LaunchAgents die automatische Anmeldung bei jedem Booten verhindert hat. Die Details stehen in automatische Anmeldung auf einem Headless-Mac.
5. Neustarts überstehen
Schalten Sie für das Konto des Agents die automatische Anmeldung ein, in den Systemeinstellungen unter Benutzer:innen & Gruppen. Verhindern Sie den Ruhezustand des Mac. Starten Sie dann neu und beobachten Sie die Seite des Node.
sudo pmset -a sleep 0 sudo shutdown -r now # then, after it is back: tail -n 20 ~/jenkins/agent.log
6. Ein Jenkinsfile mit einer iOS-Stage
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
}
}
}Ein Result Bundle ist ein Ordner. Deshalb packen wir es vor dem Archivieren mit ditto als Zip. Öffnen Sie das Zip auf einem beliebigen Mac und doppelklicken Sie das Bundle, um es in Xcode zu sehen.
Zwei Xcode-Versionen auf einem Agent? Wählen Sie mit DEVELOPER_DIR eine pro Pipeline. Laut der Manpage von xcode-select überschreibt die Variable die systemweite Auswahl, ohne sie zu ändern. Setzen Sie sie in einem environment-Block, damit andere Jobs auf dem Mac nicht betroffen sind:
environment {
DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer'
}Setzen Sie diesen Block in pipeline, neben agent. Die globale Einstellung mit sudo xcode-select -s zu ändern funktioniert auch. Das ändert aber jeden Job auf der Maschine auf einmal.
Häufige Fehler und Lösungen
- Der Agent verbindet sich und ist nach einem Neustart weg. Niemand hat sich angemeldet, also ist der LaunchAgent nie gestartet. Prüfen Sie die automatische Anmeldung.
launchctl bootstrapscheitert über SSH mit einem Domain-Fehler. Führen Sie den Befehl im Terminal auf dem Desktop aus. GitLab und Buildkite dokumentieren denselben Fehler für ihre LaunchAgents.- Der Agent startet nicht und erwähnt Java. Jenkins prüft die Java-Version beim Start. Nehmen Sie die Version aus der Support-Richtlinie.
xcodebuild: error: Existing file at -resultBundlePath. Der Workspace bleibt zwischen Builds erhalten. Löschen Sie zuerst das alte Bundle, wie im Jenkinsfile.
Warum ein dedizierter Mac hilft
Ein permanenter Agent behält seinen Workspace und die DerivedData von Xcode zwischen Builds. Der Lohn sind inkrementelle Builds. In unserem Benchmark haben wir die Wikipedia-iOS-App mit Xcode 26.6 genutzt, Median aus 3 Läufen. Nach einer kleinen Änderung dauerte der Build auf einem warmen M6 27 Sekunden. Ein frischer gehosteter macos-26-Runner von GitHub brauchte für denselben Job 269 Sekunden.
Wann Sie das nicht brauchen
Wenn Sie Jenkins noch nicht nutzen, fangen Sie nicht für eine einzige iOS-App damit an. Ein gehosteter Dienst mit Mac-Runnern macht weniger Arbeit. Beim Tarif von GitHub von $0.062 pro macOS-Minute (geprüft im September 2026) rechnet sich ein Mac zum Festpreis von $139 ab etwa 2,242 Minuten im Monat. Darunter ist die Abrechnung nach Minuten günstiger. MacRun passt auch nicht, wenn Ihr Controller nur Agents von einer festen IP annimmt. Wir haben keine statische IP.
Unsere Seite zu anderen CI-Systemen beschreibt Jenkins auf einem MacRun-Mac in Kurzform. Der Leitfaden zur iOS-CI/CD-Pipeline beschreibt, was in die Stages gehört.
Häufige Fragen
Sollte ein Jenkins-Agent auf dem Mac per SSH oder inbound starten?
+
Inbound passt, wenn der Mac den Controller erreicht, aber nicht umgekehrt. Mit -webSocket braucht er nur HTTPS zum Controller.
Welche Java-Version braucht ein Jenkins-Agent unter macOS?
+
Dieselbe Familie wie der Controller. Die Support-Richtlinie von Jenkins, gelesen im Oktober 2026, verlangt ab LTS 2.555.1 Java 21 oder 25.
Wie viele Executors sollte ein Mac-Agent haben?
+
Fangen Sie mit einem an. Jenkins nennt einen Executor pro Node die sicherste Einstellung, und ein Xcode-Build nutzt ohnehin jeden Kern.
Warum verbindet sich mein Jenkins-Agent auf dem Mac nach einem Neustart nicht wieder?
+
Ein LaunchAgent läuft erst, wenn sich sein Benutzer anmeldet. Schalten Sie für dieses Konto die automatische Anmeldung ein und lassen Sie die plist in ~/Library/LaunchAgents.