1. Perché un Mac dedicato per Jenkins?
Jenkins è la piattaforma CI/CD più diffusa, e gestisce oltre il 50% delle pipeline di build aziendali. Tuttavia, compilare app iOS con Jenkins richiede macOS, e macOS può legalmente girare solo su hardware Apple. Un Mac Mini M4 dedicato ti offre:
Stesso hardware, stesso OS, stessi strumenti a ogni build. Nessuna build instabile dovuta al drift dell'ambiente.
Installa qualsiasi strumento, configura le impostazioni di sistema, gestisci i portachiavi per la firma del codice.
Il chip M4 offre build Xcode 2-3 volte più veloci rispetto ai Mac Mini Intel.
Le cache di DerivedData, CocoaPods e SPM sopravvivono tra una build e l'altra.
2. Prerequisiti
- Un Mac Mini M4 di MyRemoteMac (da 85 $/mese)
- Un controller Jenkins (può essere su Linux, Docker o qualsiasi VM cloud)
- Accesso SSH al tuo Mac Mini
- Xcode installato sul Mac Mini (vedi la nostra guida GitHub Actions per i passaggi di installazione di Xcode)
3. Passo 1: Installa Java JDK su macOS
Gli agenti Jenkins richiedono un Java Runtime Environment. Consigliamo di usare JDK 17 (LTS) o JDK 21 (LTS) installato tramite Homebrew per una gestione semplice.
# Install Homebrew if not already present
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
# Install Java JDK 17 (recommended for Jenkins)
brew install openjdk@17
# Create the symlink for system Java wrappers
sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk-17.jdk
# Add to PATH
echo 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"' >> ~/.zprofile
source ~/.zprofile
# Verify Java installation
java -version
# openjdk version "17.0.13" 2024-10-15
# OpenJDK Runtime Environment Homebrew (build 17.0.13+0)
# OpenJDK 64-Bit Server VM Homebrew (build 17.0.13+0, mixed mode, sharing)
4. Passo 2: Configura l'agente Jenkins
Esistono due metodi principali per connettere un agente Jenkins: JNLP (connessione in entrata dall'agente al controller) e SSH (connessione in uscita dal controller all'agente). Trattiamo entrambi.
Metodo A: Agente SSH (consigliato)
Questo è l'approccio più affidabile. Jenkins si connette al Mac via SSH e gestisce il processo dell'agente da remoto.
# On the Mac Mini: Create a dedicated Jenkins user
sudo dscl . -create /Users/jenkins
sudo dscl . -create /Users/jenkins UserShell /bin/zsh
sudo dscl . -create /Users/jenkins RealName "Jenkins Agent"
sudo dscl . -create /Users/jenkins UniqueID 550
sudo dscl . -create /Users/jenkins PrimaryGroupID 20
sudo dscl . -create /Users/jenkins NFSHomeDirectory /Users/jenkins
sudo mkdir -p /Users/jenkins
sudo chown jenkins:staff /Users/jenkins
# Set a password for the jenkins user
sudo dscl . -passwd /Users/jenkins "STRONG_PASSWORD_HERE"
# Create SSH directory and add the Jenkins controller's public key
sudo mkdir -p /Users/jenkins/.ssh
sudo sh -c 'echo "ssh-rsa YOUR_JENKINS_CONTROLLER_PUBLIC_KEY" > /Users/jenkins/.ssh/authorized_keys'
sudo chmod 700 /Users/jenkins/.ssh
sudo chmod 600 /Users/jenkins/.ssh/authorized_keys
sudo chown -R jenkins:staff /Users/jenkins/.ssh
# Create a workspace directory
sudo mkdir -p /Users/jenkins/workspace
sudo chown jenkins:staff /Users/jenkins/workspace
Ora configura l'agente in Jenkins:
- Vai su Manage Jenkins > Nodes > New Node
- Imposta il nome su
mac-mini-m4 - Imposta la Remote root directory su
/Users/jenkins/workspace - Label:
mac macos apple-silicon m4 ios - Metodo di avvio: Launch agents via SSH
- Host: L'indirizzo IP del tuo Mac Mini
- Credenziali: Aggiungi la chiave privata SSH corrispondente agli authorized_keys sul Mac
Metodo B: Agente JNLP (in entrata)
Usa questo metodo quando il controller Jenkins non può raggiungere direttamente il Mac (ad es. il Mac è dietro un firewall). L'agente avvia la connessione.
# First, create the node in Jenkins UI:
# Manage Jenkins > Nodes > New Node
# Launch method: "Launch agent by connecting it to the controller"
# Note the secret token from the node configuration page
# On the Mac Mini, download the agent JAR:
mkdir -p ~/jenkins-agent && cd ~/jenkins-agent
curl -sO https://your-jenkins-url/jnlpJars/agent.jar
# Test the agent connection
java -jar agent.jar \
-url https://your-jenkins-url \
-secret YOUR_AGENT_SECRET \
-name "mac-mini-m4" \
-workDir "/Users/jenkins/workspace"
Esegui l'agente JNLP come servizio launchd
# Create the launchd plist file
cat > ~/Library/LaunchAgents/com.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>com.jenkins.agent</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/openjdk@17/bin/java</string>
<string>-jar</string>
<string>/Users/jenkins/jenkins-agent/agent.jar</string>
<string>-url</string>
<string>https://your-jenkins-url</string>
<string>-secret</string>
<string>YOUR_AGENT_SECRET</string>
<string>-name</string>
<string>mac-mini-m4</string>
<string>-workDir</string>
<string>/Users/jenkins/workspace</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/jenkins/jenkins-agent/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/jenkins/jenkins-agent/stderr.log</string>
</dict>
</plist>
EOF
# Load the service
launchctl load ~/Library/LaunchAgents/com.jenkins.agent.plist
# Verify it's running
launchctl list | grep jenkins
5. Passo 3: Crea la pipeline di build iOS
Crea un Jenkinsfile nella radice del tuo repository. Questa pipeline dichiarativa compila, testa e, facoltativamente, archivia la tua app iOS.
pipeline {
agent { label 'mac && m4' }
environment {
SCHEME = 'MyApp'
WORKSPACE = 'MyApp.xcworkspace'
DESTINATION = 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2'
DERIVED_DATA = "${WORKSPACE_DIR}/DerivedData"
}
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
sh '''
# Select Xcode version
sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer
xcodebuild -version
# Install CocoaPods dependencies
if [ -f "Podfile" ]; then
pod install --repo-update
fi
'''
}
}
stage('Build') {
steps {
sh '''
xcodebuild build \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME}" \
-destination "${DESTINATION}" \
-derivedDataPath "${DERIVED_DATA}" \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify
'''
}
}
stage('Test') {
steps {
sh '''
xcodebuild test \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME}" \
-destination "${DESTINATION}" \
-derivedDataPath "${DERIVED_DATA}" \
-resultBundlePath "TestResults.xcresult" \
-parallel-testing-enabled YES \
| xcbeautify
'''
}
post {
always {
archiveArtifacts artifacts: 'TestResults.xcresult/**', allowEmptyArchive: true
}
}
}
stage('Archive') {
when {
branch 'main'
}
steps {
sh '''
xcodebuild archive \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME}" \
-archivePath "${DERIVED_DATA}/MyApp.xcarchive" \
-destination "generic/platform=iOS" \
| xcbeautify
'''
}
}
}
post {
success {
echo 'Build and tests passed!'
}
failure {
echo 'Build or tests failed.'
}
cleanup {
sh 'xcrun simctl shutdown all 2>/dev/null || true'
}
}
}
6. Passo 4: Integra Fastlane per il deployment
Fastlane semplifica la firma del codice e il deployment su TestFlight. Aggiungi una fase di deploy al tuo Jenkinsfile:
# Install Fastlane on the Mac Mini (one-time setup)
brew install fastlane
# Or via Ruby:
gem install fastlane -NV
Aggiungi una fase di deploy al tuo Jenkinsfile:
stage('Deploy to TestFlight') {
when {
branch 'main'
}
environment {
APP_STORE_CONNECT_API_KEY_ID = credentials('app-store-key-id')
APP_STORE_CONNECT_API_ISSUER_ID = credentials('app-store-issuer-id')
APP_STORE_CONNECT_API_KEY_CONTENT = credentials('app-store-key-content')
MATCH_PASSWORD = credentials('match-password')
}
steps {
sh '''
fastlane beta
'''
}
}
E crea un fastlane/Fastfile corrispondente:
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
setup_ci
# Fetch code signing certificates via match
match(type: "appstore", readonly: true)
# Increment build number
increment_build_number(
build_number: ENV["BUILD_NUMBER"]
)
# Build the app
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
export_method: "app-store",
derived_data_path: "DerivedData"
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
api_key: app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"]
)
)
end
end
7. Best practice di sicurezza
Non eseguire mai l'agente Jenkins come root. Crea un utente dedicato con permessi minimi.
Non scrivere mai certificati, chiavi API o password direttamente nei Jenkinsfile. Usa il plugin Jenkins Credentials.
Disabilita l'autenticazione con password per SSH. Usa chiavi Ed25519 o RSA a 4096 bit.
Limita le connessioni in entrata solo all'IP del controller Jenkins. MyRemoteMac fornisce un firewall gestito con accesso API.
Applica tempestivamente le patch di sicurezza. Usa softwareupdate -l per verificare gli aggiornamenti.
8. FAQ
Posso eseguire il controller Jenkins sullo stesso Mac?
Sì, ma consigliamo di separarli. Esegui il controller su una VM Linux o un container Docker e usa il Mac esclusivamente come agente di build. In questo modo eviti conflitti di risorse durante le build più pesanti.
Quante build simultanee può gestire un Mac Mini M4?
Un Mac Mini M4 con 16 GB di RAM può gestire comodamente 2 build Xcode simultanee. Con 24 GB puoi arrivare a 3. L'M4 Pro con 48 GB gestisce facilmente 4+ build simultanee. Configura di conseguenza il "Number of executors" nelle impostazioni del tuo nodo Jenkins.
Ho bisogno di una sessione GUI per i simulatori iOS?
No. I simulatori iOS funzionano in modalità headless via SSH. Tuttavia, se ti serve una GUI (ad es. per gli screenshot dei test UI), usa VNC per accedere al Mac e assicurati che una sessione GUI sia attiva.
Come gestisco la firma del codice su un server headless?
Usa Fastlane Match per sincronizzare i certificati da un repo Git o dallo storage cloud. In alternativa, importa i certificati nel portachiavi di macOS e sbloccalo nella tua pipeline usando security unlock-keychain.