Fastlane match : signature CI sur Mac distant en 2026

Fastlane match : signature CI sur Mac distant en 2026

La documentation officielle de setup_ci décrit trois effets essentiels : création d’un trousseau temporaire, passage de match en mode readonly et centralisation des journaux CI (documentation officielle de setup_ci). C’est donc le choix gagnant pour une signature CI sur Mac distant, à condition d’y ajouter une correspondance explicite entre les Bundle ID et les profils, puis de tester le redémarrage, la concurrence, la rotation des certificats et la reprise après échec.

Cet article s’adresse aux ingénieurs DevOps qui maintiennent une chaîne iOS ou macOS mais voient régulièrement la signature échouer sur un nœud distant. Il concerne également les équipes qui partagent un Mac entre plusieurs applications, Extensions ou Apple Teams, ainsi que les développeurs qui migrent une chaîne locale vers un runner distant permanent.

Le succès local ne garantit pas une signature reproductible

Un build qui fonctionne sur le Mac d’un développeur ne prouve pas que les quatre éléments nécessaires ont été transférés correctement :

  • le certificat de signature ;
  • la clé privée associée ;
  • le provisioning profile correspondant au Bundle ID ;
  • les réglages de signature du projet Xcode.

fastlane match synchronise les certificats et les profils depuis un stockage chiffré, mais il ne corrige pas automatiquement un mauvais PRODUCT_BUNDLE_IDENTIFIER, une configuration Release différente ou un profil associé au mauvais Target. La documentation officielle indique que le dépôt de stockage contient notamment les certificats avec leurs clés privées dans certs, ainsi que les profils dans profiles (structure de stockage de match).

Le deuxième piège est le trousseau. Une identité peut apparaître avec security find-identity alors que le processus CI ne peut pas accéder à sa clé privée. La différence vient souvent de la session utilisateur, du trousseau par défaut, de son état verrouillé ou des règles d’accès appliquées à la clé.

Le troisième piège est la persistance. Sur un Mac distant utilisé pendant plusieurs semaines, un profil obsolète, un ancien trousseau ou un dossier DerivedData peuvent contaminer les tâches suivantes. Le résultat est trompeur : le premier build passe, puis un autre Target échoue, ou bien le problème ne réapparaît qu’après un redémarrage.

Ce qui peut apparaître dans les journaux

Les journaux peuvent conserver :

  • le type de certificat utilisé ;
  • le nom du profil sélectionné ;
  • le Bundle ID ;
  • la version de Xcode ;
  • le chemin de l’archive ;
  • le résultat de codesign et security.

En revanche, ils ne doivent jamais contenir la valeur de MATCH_PASSWORD, une clé privée exportée, un jeton d’accès au dépôt de certificats, un mot de passe Apple ou une clé API. Les scripts doivent afficher des noms et des statuts, pas des secrets.

Attention. Une sortie BUILD SUCCEEDED ne suffit pas. Elle prouve seulement que la commande de compilation s’est terminée. L’archive doit encore être inspectée et son export doit utiliser les profils attendus.

Une architecture CI plus sûre : readonly, trousseau isolé et nettoyage

La séparation des responsabilités doit rester visible dans le Fastfile :

Élément Responsabilité À ne pas lui faire porter
setup_ci Préparer le trousseau temporaire et le contexte CI Créer une stratégie de profils par Target
match Récupérer les certificats et profils existants Résoudre silencieusement une mauvaise configuration Xcode
build_app Archiver et exporter l’application Régénérer les actifs de signature en production
Script de validation Vérifier identité, profils, archive et IPA Modifier les certificats pendant le contrôle

La recommandation officielle est d’utiliser readonly sur les systèmes CI afin d’éviter la création de nouveaux certificats ou profils. Cette option est documentée dans l’action match (documentation de match).

Un squelette de lane peut rester volontairement explicite :

platform :ios do
  lane :ci_appstore do
    setup_ci(
      keychain_name: "fastlane_ci_keychain",
      timeout: 3600
    )

    match(
      type: "appstore",
      readonly: true,
      app_identifier: [
        "com.exemple.app"
      ]
    )

    build_app(
      workspace: "Example.xcworkspace",
      scheme: "Example",
      configuration: "Release",
      export_method: "app-store",
      clean: true
    )
  end
end

Les paramètres disponibles et leur comportement doivent être revérifiés dans la documentation officielle au moment du déploiement. setup_ci prévoit notamment un nom de trousseau personnalisé et un délai d’expiration configurable.

Pour un runner distant permanent, le choix d’un trousseau temporaire est généralement préférable à l’usage du trousseau personnel du compte administrateur. Il limite les effets de bord entre projets et réduit la quantité d’actifs laissés sur le nœud après un échec.

fastlane match sur un Mac distant doit-il toujours utiliser readonly ?
Pour une tâche de build et de publication, oui dans la majorité des cas. La création ou la révocation d’un certificat doit être une opération contrôlée, exécutée depuis un poste ou une tâche d’administration séparée. Un job CI ne devrait pas modifier l’état partagé simplement parce qu’un profil manque.

Le cas minimal : une seule application App Store

Pour une application unique, la configuration peut être simple, mais la validation ne doit pas l’être. Le nœud doit disposer d’un compte de construction dédié, d’un accès limité au dépôt chiffré et des variables secrètes injectées par le système CI.

Les variables courantes peuvent inclure :

MATCH_PASSWORD=<secret_non_affiché>
FASTLANE_USER=<compte_de_service_si_requis>
LANG=en_US.UTF-8
LC_ALL=en_US.UTF-8

La documentation CI de fastlane décrit MATCH_PASSWORD, les variables d’authentification et les paramètres de locale comme des éléments fréquents d’un environnement automatisé (variables CI officielles).

Le contrôle de base commence par l’inventaire de l’identité :

security find-identity -v -p codesigning

Une sortie exploitable doit afficher une identité valide, sans exposer de clé privée. La vérification suivante concerne les profils installés :

find "$HOME/Library/MobileDevice/Provisioning Profiles" \
  -type f -name "*.mobileprovision" -print

Ensuite, le build doit produire une archive et un fichier d’export. build_app s’appuie sur les API d’export de Xcode et permet de fournir un dictionnaire provisioningProfiles lorsque la détection automatique n’est pas suffisamment déterministe (documentation officielle de build_app).

La validation doit couvrir quatre preuves :

  1. une identité de signature présente ;
  2. un profil correspondant au Bundle ID ;
  3. une archive .xcarchive créée ;
  4. un IPA exporté et vérifiable avec codesign.

Exemple de contrôle après export :

codesign --verify --deep --strict --verbose=2 "build/Example.app"

unzip -q "build/Example.ipa" -d "build/ipa_check"

codesign -dvvv "build/ipa_check/Payload/Example.app" 2>&1 \
  | grep -E "Identifier|TeamIdentifier|Authority"

Le nom de l’autorité, l’identifiant d’équipe et le Bundle ID doivent correspondre à l’environnement de publication. Un lane qui retourne zéro erreur mais exporte avec une autre identité n’est pas validé.

Plusieurs Target : rendre la correspondance explicite

Les Extensions rendent les erreurs de signature plus difficiles à diagnostiquer. Une application peut contenir un Target principal, un Widget et une Notification Service Extension. Chaque Target possède son propre Bundle ID et peut nécessiter un profil différent.

Target Bundle ID d’exemple Profil attendu Preuve à conserver
Application com.exemple.app Distribution de l’application Entrée application-identifier
Widget com.exemple.app.widget Distribution du Widget Signature du bundle de l’Extension
Notification Service com.exemple.app.notification Profil de l’Extension Journal d’archive et profil embarqué

Les valeurs ci-dessus sont des exemples de structure, pas des identifiants à copier. Dans un projet réel, les Bundle ID doivent être extraits du projet et contrôlés avant la synchronisation.

Un Matchfile peut centraliser les valeurs stables :

git_url("ssh://git@depot-interne.example/signing-assets.git")
storage_mode("git")
type("appstore")
app_identifier([
  "com.exemple.app",
  "com.exemple.app.widget",
  "com.exemple.app.notification"
])

Le Fastfile doit ensuite appeler match avec les mêmes identifiants et conserver readonly: true en CI. Enfin, build_app peut recevoir une table explicite :

build_app(
  workspace: "Example.xcworkspace",
  scheme: "Example",
  export_method: "app-store",
  export_options: {
    provisioningProfiles: {
      "com.exemple.app" => "match AppStore com.exemple.app",
      "com.exemple.app.widget" => "match AppStore com.exemple.app.widget",
      "com.exemple.app.notification" =>
        "match AppStore com.exemple.app.notification"
    }
  }
)

Comment éviter qu’un Target prenne le profil d’un autre ?
Il faut vérifier trois niveaux, dans cet ordre : les réglages de build du Target, la table d’export et le contenu de l’archive. La présence d’un profil dans le dossier local ne prouve pas qu’il a été utilisé.

Une inspection ciblée peut commencer ainsi :

xcodebuild -workspace Example.xcworkspace \
  -scheme Example \
  -configuration Release \
  -showBuildSettings \
  | grep -E "PRODUCT_BUNDLE_IDENTIFIER|CODE_SIGN_STYLE|PROVISIONING_PROFILE"

L’archive doit ensuite être ouverte comme un conteneur :

find "build/Example.xcarchive/Products/Applications" \
  -name "embedded.mobileprovision" -print

Pour un projet multi-Target, le journal d’archive et le contrôle de chaque bundle signé sont plus fiables qu’un simple message de succès global.

Plusieurs équipes et plusieurs applications sur le même Mac

Un Mac partagé entre plusieurs Apple Teams doit être traité comme une surface d’isolation, pas comme un poste de développement ordinaire. Les frontières minimales sont :

  • un dépôt ou une branche de signature par périmètre de confiance ;
  • des secrets séparés par équipe et par environnement ;
  • un trousseau différent pour chaque tâche ou groupe de tâches ;
  • des labels CI qui empêchent l’envoi accidentel d’un projet vers le mauvais runner ;
  • des répertoires de sortie propres à chaque job.

Pour un runner autogéré, le système CI permet généralement d’utiliser des labels et des groupes afin de diriger un job vers une machine précise (gestion officielle des runners autogérés).

Un exemple de routage peut ressembler à ceci :

jobs:
  release:
    runs-on: [self-hosted, macOS, arm64, ios-release]
    steps:
      - uses: actions/checkout@v6
      - run: bundle exec fastlane ci_appstore

Les labels self-hosted, macOS et ARM64 servent à identifier le système et l’architecture, tandis que les labels personnalisés doivent exprimer le périmètre fonctionnel (choix officiel d’un runner).

L’automatisation Xcode peut convenir au développement quotidien, notamment lorsque Xcode doit régénérer ou ajuster des profils. En publication CI, un réglage explicite est préférable : il est lisible dans le code, auditable dans les journaux et plus facile à restaurer après le départ d’un membre de l’équipe.

Expérience de terrain. La séparation par répertoire ne suffit pas si deux jobs utilisent le même trousseau par défaut. Le nom du trousseau, le répertoire de build et les secrets doivent être séparés ensemble.

Longue durée, redémarrage et concurrence

Un runner permanent présente une différence importante par rapport à un runner éphémère : il conserve un historique local. Cela concerne les trousseaux, les profils, les archives, les caches Ruby, les dépendances et les fichiers temporaires.

Le test minimal doit comporter trois passages :

Test Ce qu’il vérifie Critère de réussite
Premier job après redémarrage Session, service CI, trousseau et réseau Synchronisation et signature sans intervention graphique
Deux jobs consécutifs Résidus du job précédent Même résultat avec un espace de travail nettoyé
Échec forcé puis nouveau job Nettoyage après interruption Aucun profil ou trousseau incomplet réutilisé

Si le runner démarre comme service, son état doit être observable avec les outils du système. La documentation officielle décrit notamment l’usage de launchctl et du fichier .service pour inspecter un runner macOS (surveillance d’un runner macOS).

Avant le build :

set -euo pipefail

rm -rf "$RUNNER_TEMP/derived-data"
mkdir -p "$RUNNER_TEMP/derived-data"

security list-keychains -d user
security find-identity -v -p codesigning

Après le build, le pipeline doit supprimer les éléments temporaires qu’il a créés. Il ne faut toutefois pas supprimer indistinctement tous les profils de l’utilisateur si le Mac sert à plusieurs périmètres. Une stratégie plus sûre consiste à utiliser un compte de construction réservé et à reconstruire le trousseau temporaire à chaque exécution.

La concurrence mérite une règle séparée. Deux jobs qui écrivent dans le même DerivedData, le même dossier d’archive ou le même trousseau peuvent produire des erreurs impossibles à reproduire. Si la plateforme ne garantit pas l’exclusivité du runner, les jobs de signature doivent être sérialisés ou affectés à des nœuds distincts.

Rotation, révocation et reconstruction du nœud

Une expiration de certificat ne doit pas déclencher immédiatement une commande destructive. La séquence de récupération doit être progressive :

  1. identifier le certificat ou le profil réellement expiré ;
  2. vérifier l’état du stockage de signature ;
  3. confirmer l’impact sur les applications et les environnements ;
  4. régénérer ou mettre à jour les actifs depuis un poste d’administration contrôlé ;
  5. synchroniser le runner en readonly ;
  6. reconstruire une archive ;
  7. vérifier l’IPA, les Extensions et l’équipe de signature ;
  8. désactiver les anciennes autorisations si nécessaire ;
  9. consigner le résultat et la possibilité de retour arrière.

La documentation de match présente le stockage centralisé des certificats et profils comme un moyen de limiter les accès directs au portail de développement et d’éviter les révocations accidentelles (contrôle d’accès et stockage de match).

Apple documente séparément la création des certificats, des demandes de signature et des profils de provisioning dans l’espace d’aide développeur (aide officielle sur les certificats et profils).

Les commandes de réinitialisation totale doivent rester exceptionnelles. Avant toute suppression, il faut sauvegarder les références utiles, vérifier les canaux de distribution encore actifs et savoir quel membre de l’équipe peut restaurer l’état précédent. Une reconstruction propre du Mac est souvent plus vérifiable qu’une succession de corrections locales, mais elle exige un inventaire préalable.

La conclusion de mise en production ne devrait être validée qu’après :

  • un redémarrage complet ;
  • une exécution avec secrets réinjectés ;
  • une vérification d’identité ;
  • un build multi-Target ;
  • un test d’échec et de nettoyage ;
  • une simulation de remplacement ou de reconstruction du nœud.

Verdict : quelle configuration retenir ?

Pour une application unique, setup_ci avec match en lecture seule et build_app peut suffire. Pour plusieurs Targets, la table provisioningProfiles devient fortement recommandée. Pour plusieurs équipes, l’isolation des secrets, des trousseaux et des runners doit être traitée comme une exigence de sécurité.

La matrice de décision suivante permet d’éviter une configuration disproportionnée :

Situation Configuration recommandée Niveau de contrôle requis
Une application, un runner éphémère setup_ci + match readonly Vérification de l’identité et de l’IPA
Application avec Widget ou Extension Mapping explicite par Bundle ID Contrôle de chaque bundle signé
Plusieurs applications d’une même équipe Secrets et répertoires séparés Labels, nettoyage et journaux par projet
Plusieurs Apple Teams Runners ou trousseaux cloisonnés Accès minimal et audit des profils
Runner permanent Trousseau temporaire, nettoyage, test après redémarrage Reconstruction et reprise après incident

Le problème initial n’est donc pas simplement « installer fastlane match ». Il consiste à rendre la chaîne de signature observable et reproductible lorsque le nœud est distant, redémarré, partagé ou remplacé.

Un Mac local utilisé comme serveur de build présente plusieurs limites pour ce scénario : il immobilise du matériel, dépend d’une connexion et d’une session correctement maintenue, conserve souvent des actifs d’équipes différentes et rend la reconstruction plus longue lorsqu’une panne intervient. Une machine virtuelle ou un hôte Linux ajoute une autre contrainte : elle ne remplace pas toujours le comportement d’un macOS réel pour Xcode, les trousseaux et les outils Apple.

Pour une migration ponctuelle, un cycle de publication ou un test de reconstruction, louer un Mac distant avec des droits root complets peut être plus cohérent que d’acheter immédiatement un Mac dédié. SFTPMAC permet alors de préparer un nœud réel, d’exécuter les vérifications de redémarrage et de conserver la maîtrise de la configuration sans transformer un poste personnel en serveur permanent. Les options peuvent être comparées sur la page consacrée aux tarifs de location de Mac mini, tandis que la présentation des Mac mini disponibles pour les charges de développement aide à choisir un environnement adapté au cycle CI.

Pour les équipes qui veulent valider d’abord la sécurité du runner, la priorité devrait rester la même : déployer une configuration minimale, tester la signature après redémarrage, provoquer un échec contrôlé, puis vérifier qu’un nouveau job repart sans réutiliser un état corrompu. C’est cette preuve, et non le seul retour positif de fastlane, qui distingue une chaîne de signature locale d’un véritable environnement macOS CI exploitable.