Guide - CI/CD

Runner GitLab CI/CD sur Mac Mini M4 : guide complet

Installez et configurez un runner GitLab CI/CD sur un Mac Mini M4 dédié. Compilez des applications iOS nativement sur Apple Silicon, exécutez des tests sur de vrais simulateurs et déployez sur TestFlight -- le tout depuis votre pipeline GitLab.

30 min de lecture Mis à jour en janvier 2025

1. Pourquoi des runners GitLab auto-hébergés sur Mac ?

GitLab propose des runners partagés sur Linux, mais compiler des applications iOS nécessite macOS sur du matériel Apple. Les runners macOS partagés de GitLab sont limités et coûteux. Un runner Mac Mini M4 auto-hébergé vous offre :

Minutes CI/CD illimitées

L'offre gratuite de GitLab inclut 400 minutes CI/CD sur les runners partagés. En auto-hébergé, il n'y a aucune limite.

Apple Silicon natif

Compilez sur la même puce M4 que celle des appareils de vos utilisateurs. Aucune surcharge de traduction Rosetta.

Contrôle total de l'environnement

Installez n'importe quelle version de Xcode, des simulateurs, des outils et des dépendances dont vous avez besoin.

Caches persistants

DerivedData, les paquets SPM et les caches CocoaPods survivent entre les exécutions de pipeline.

2. Installer le runner GitLab

Connectez-vous en SSH à votre Mac Mini M4 et installez le runner GitLab avec Homebrew :

# Connect to your Mac Mini M4
ssh admin@your-server-ip

# Install Homebrew (if not already installed)
/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 GitLab Runner
brew install gitlab-runner

# Verify installation
gitlab-runner --version
# Version:      17.7.0
# Git revision:  ...
# Git branch:    17-7-stable
# GO version:    go1.22.10
# Built:         ...
# OS/Arch:       darwin/arm64

Installer en tant que service macOS

# Install the runner as a launchd service
# This ensures it starts automatically on boot
brew services start gitlab-runner

# Verify the service is running
brew services list | grep gitlab-runner
# gitlab-runner started admin ~/Library/LaunchAgents/homebrew.mxcl.gitlab-runner.plist

# Check runner status
gitlab-runner status
# gitlab-runner: Service is running

3. Enregistrer le runner

Accédez à votre projet (ou groupe) GitLab et naviguez vers Settings > CI/CD > Runners > New project runner. Copiez le jeton d'enregistrement.

Enregistrer avec le nouveau flux d'enregistrement (GitLab 16+)

# Register the runner using the authentication token from GitLab UI
# (GitLab 16+ uses authentication tokens instead of registration tokens)
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "YOUR_RUNNER_AUTHENTICATION_TOKEN" \
  --executor "shell" \
  --description "mac-mini-m4-runner" \
  --tag-list "macos,apple-silicon,m4,ios,xcode"

Enregistrer avec le jeton d'enregistrement legacy (GitLab 15 et antérieur)

# For older GitLab instances using registration tokens
gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --registration-token "YOUR_REGISTRATION_TOKEN" \
  --executor "shell" \
  --description "mac-mini-m4-runner" \
  --tag-list "macos,apple-silicon,m4,ios,xcode" \
  --run-untagged="false"

Vérifier la configuration du runner

# View the runner config file
cat ~/.gitlab-runner/config.toml

# Expected output:
# concurrent = 2
# check_interval = 0
#
# [session_server]
#   session_timeout = 1800
#
# [[runners]]
#   name = "mac-mini-m4-runner"
#   url = "https://gitlab.com/"
#   token = "..."
#   executor = "shell"
#   [runners.cache]
#     MaxUploadedArchiveSize = 0

# Adjust concurrency based on your hardware:
# Mac Mini M4 (16GB): concurrent = 2
# Mac Mini M4 Pro (24GB): concurrent = 3
# Mac Mini M4 Pro (48GB): concurrent = 4

Éditez ~/.gitlab-runner/config.toml pour ajuster la concurrence :

# Edit the config
nano ~/.gitlab-runner/config.toml

# Set concurrent to match your hardware capacity
concurrent = 2

# Restart the runner to apply changes
gitlab-runner restart

Le runner devrait maintenant apparaître comme En ligne dans votre projet GitLab sous Settings > CI/CD > Runners.

4. Créer un .gitlab-ci.yml pour iOS

Créez un fichier .gitlab-ci.yml à la racine de votre dépôt. Ce pipeline complet compile, teste et déploie votre application iOS :

# .gitlab-ci.yml

stages:
  - setup
  - build
  - test
  - deploy

variables:
  SCHEME: "MyApp"
  WORKSPACE: "MyApp.xcworkspace"
  DESTINATION: "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2"
  DERIVED_DATA: "${CI_PROJECT_DIR}/DerivedData"

# Only run on our Mac runner
default:
  tags:
    - macos
    - m4

# ---- SETUP ----

setup:
  stage: setup
  script:
    - sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer
    - xcodebuild -version
    - swift --version
    # Install CocoaPods if using Podfile
    - |
      if [ -f "Podfile" ]; then
        pod install --repo-update
      fi
  cache:
    key: pods-${CI_COMMIT_REF_SLUG}
    paths:
      - Pods/
      - .spm-cache/

# ---- BUILD ----

build:
  stage: build
  needs: ["setup"]
  script:
    - |
      xcodebuild build \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        -clonedSourcePackagesDirPath ".spm-cache" \
        CODE_SIGNING_ALLOWED=NO \
        | xcbeautify
  cache:
    key: derived-data-${CI_COMMIT_REF_SLUG}
    paths:
      - DerivedData/
      - .spm-cache/
  artifacts:
    paths:
      - DerivedData/
    expire_in: 1 hour

# ---- TEST ----

unit_tests:
  stage: test
  needs: ["build"]
  script:
    - |
      xcodebuild test \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        -resultBundlePath "TestResults.xcresult" \
        -parallel-testing-enabled YES \
        | xcbeautify
  artifacts:
    when: always
    paths:
      - TestResults.xcresult/
    reports:
      junit: TestResults.xcresult/report.junit
    expire_in: 7 days
  after_script:
    - xcrun simctl shutdown all 2>/dev/null || true

# ---- DEPLOY ----

deploy_testflight:
  stage: deploy
  needs: ["unit_tests"]
  only:
    - main
  script:
    - |
      # Install or update Fastlane
      which fastlane || brew install fastlane

      # Run Fastlane beta lane
      fastlane beta
  environment:
    name: testflight
  variables:
    MATCH_PASSWORD: ${MATCH_PASSWORD}
    APP_STORE_CONNECT_API_KEY_ID: ${APP_STORE_KEY_ID}
    APP_STORE_CONNECT_API_ISSUER_ID: ${APP_STORE_ISSUER_ID}
    APP_STORE_CONNECT_API_KEY_CONTENT: ${APP_STORE_KEY_CONTENT}

Ajouter des variables CI/CD dans GitLab

Naviguez vers Settings > CI/CD > Variables dans votre projet GitLab et ajoutez ces variables en tant que "Masquées" et "Protégées" :

  • MATCH_PASSWORD - Mot de passe pour le chiffrement Fastlane Match
  • APP_STORE_KEY_ID - ID de clé API App Store Connect
  • APP_STORE_ISSUER_ID - Issuer ID App Store Connect
  • APP_STORE_KEY_CONTENT - Contenu du fichier de clé .p8

5. Optimiser les performances

Configuration du cache GitLab

Le runner GitLab prend en charge le cache local pour les exécuteurs shell. Puisque votre runner est persistant, les caches locaux sont extrêmement efficaces :

# In .gitlab-ci.yml, configure cache per branch:
cache:
  key: "${CI_COMMIT_REF_SLUG}"
  paths:
    - DerivedData/
    - .spm-cache/
    - Pods/
  policy: pull-push

# For test jobs that don't modify cache, use pull-only:
unit_tests:
  cache:
    key: "${CI_COMMIT_REF_SLUG}"
    paths:
      - DerivedData/
    policy: pull

Utiliser les artefacts pour les données inter-stages

# Pass build artifacts between stages efficiently
build:
  artifacts:
    paths:
      - DerivedData/Build/Products/
    expire_in: 2 hours

# The test stage receives the built products without rebuilding
test:
  needs: ["build"]  # only download artifacts from the build job
  script:
    - xcodebuild test-without-building \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}"

Exécution parallèle des tests

# Split tests across parallel jobs using GitLab's parallel keyword
unit_tests:
  stage: test
  parallel: 2
  script:
    - |
      # Use test plan partitioning or custom splitting
      xcodebuild test \
        -workspace "${WORKSPACE}" \
        -scheme "${SCHEME}" \
        -destination "${DESTINATION}" \
        -derivedDataPath "${DERIVED_DATA}" \
        -parallel-testing-enabled YES \
        -maximum-parallel-testing-workers 4

Nettoyage planifié du cache

# On the Mac Mini, set up a weekly cleanup cron job
crontab -e

# Add these lines:
# Clean DerivedData older than 7 days every Sunday at 3 AM
0 3 * * 0 find ~/builds/*/DerivedData -maxdepth 0 -mtime +7 -exec rm -rf {} + 2>/dev/null

# Clean old GitLab Runner builds older than 14 days
0 4 * * 0 find ~/builds -maxdepth 2 -mtime +14 -type d -exec rm -rf {} + 2>/dev/null

# Clean Homebrew cache monthly
0 5 1 * * /opt/homebrew/bin/brew cleanup --prune=30 2>/dev/null

6. Résolution des problèmes

Le runner apparaît "hors ligne" dans GitLab

Vérifiez l'état du service du runner et les journaux :

# Check service status
brew services list | grep gitlab-runner

# View logs
cat /usr/local/var/log/gitlab-runner.log

# Restart the service
brew services restart gitlab-runner

# Verify connectivity to GitLab
gitlab-runner verify

Erreurs de permission refusée pendant les builds

Le runner peut avoir besoin d'accéder au répertoire développeur Xcode :

# Ensure the runner user has Xcode access
sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer
sudo xcodebuild -license accept

# If using simulators, ensure the user can access them
xcrun simctl list devices

Le cache n'est pas restauré

Assurez-vous que les clés de cache sont cohérentes et que les chemins existent :

# Check cache directory permissions
ls -la ~/builds/

# The shell executor stores caches locally by default
# Verify the cache directory in config.toml:
cat ~/.gitlab-runner/config.toml

# Ensure [runners.cache] section has the right settings
# For local caching (most efficient for persistent runners):
# [runners.cache]
#   Type = ""  # empty = local cache

Le build Xcode se bloque ou expire

C'est souvent causé par des invites d'accès au trousseau ou des problèmes de simulateur :

# Unlock the keychain before builds
security unlock-keychain -p "YOUR_PASSWORD" ~/Library/Keychains/login.keychain-db

# Kill stuck simulators
xcrun simctl shutdown all
pkill -f "Simulator.app" 2>/dev/null || true

# Set a build timeout in .gitlab-ci.yml
build:
  timeout: 30 minutes

7. FAQ

Puis-je utiliser un exécuteur Docker sur macOS ?

Docker sur macOS exécute des conteneurs Linux dans une VM, qui ne peut pas accéder aux API macOS, à Xcode ni aux simulateurs iOS. Pour les builds iOS, vous devez utiliser l'exécuteur shell. Docker est adapté pour le Swift côté serveur ou d'autres tâches Linux exécutées en parallèle de votre runner Mac.

Comment enregistrer le runner pour un groupe GitLab ?

Allez dans Settings > CI/CD > Runners > New group runner de votre groupe GitLab. Utilisez le jeton de runner de groupe au lieu d'un jeton de projet. Cela rend le runner disponible pour tous les projets du groupe.

Dois-je utiliser l'exécuteur shell ou l'exécuteur SSH ?

Utilisez l'exécuteur shell. Il exécute les commandes directement sur le Mac, ce qui donne un accès complet à Xcode, aux simulateurs et au trousseau. L'exécuteur SSH est destiné aux machines distantes, ce qui est inutile lorsque le runner est déjà sur le Mac.

Puis-je exécuter les runners GitLab et GitHub Actions sur le même Mac ?

Oui. Les deux runners sont légers et peuvent coexister sur le même Mac Mini M4. Assurez-vous simplement de prendre en compte l'utilisation combinée des ressources lors de la définition des niveaux de concurrence pour chaque runner.

Comment mettre à jour le runner GitLab ?

# Update via Homebrew
brew upgrade gitlab-runner

# Restart the service
brew services restart gitlab-runner

# Verify the new version
gitlab-runner --version

Guides connexes

Prêt à alimenter vos pipelines GitLab ?

Obtenez un Mac Mini M4 dédié pour votre runner GitLab CI/CD. Builds illimités à partir de 75 $/mois avec un essai gratuit de 7 jours.