Fastlane avec .NET MAUI 10 : Automatiser les Releases iOS et Android (2026)

Guide DevOps opérationnel pour brancher Fastlane sur un projet .NET MAUI 10 : Match pour les certificats iOS, Supply pour Google Play, Pilot pour TestFlight, et un Fastfile complet à copier-coller.

Fastlane .NET MAUI 10 : Releases iOS/Android

Mis à jour : 3 septembre 2026

Fastlane s'intègre à .NET MAUI 10 en pilotant les étapes de release après dotnet publish : signature avec Match, upload iOS via Pilot vers TestFlight et upload Android via Supply vers Google Play. La chaîne est identique à Xamarin (seul le point d'entrée change), puisque MAUI produit un .ipa pour iOS et un .aab pour Android que Fastlane consomme directement. Comptez une demi-journée de mise en place propre, moins de deux minutes par release ensuite.

  • Fastlane 2.223+ prend en charge sans plugin les artefacts .ipa et .aab générés par dotnet publish en .NET 10.
  • Utilisez Match pour synchroniser certificats et provisioning profiles iOS via un dépôt Git chiffré. Plus jamais de « profil expiré » la veille d'une release.
  • Deux lanes suffisent : beta (TestFlight + piste interne Play) et release (App Store + piste production).
  • Stockez APP_STORE_CONNECT_API_KEY et SUPPLY_JSON_KEY dans les secrets CI, jamais dans le dépôt.
  • Le versionning MAUI 10 se pilote via ApplicationVersion (build number) et ApplicationDisplayVersion (version marketing). Fastlane peut les incrémenter automatiquement.
  • La signature Android ne passe plus par un keystore local en 2026 : activez Play App Signing et laissez Supply gérer l'upload key.

Pourquoi Fastlane pour un projet .NET MAUI en 2026

Honnêtement, la plupart des équipes que j'accompagne partent du principe que dotnet publish -f net10.0-ios et dotnet publish -f net10.0-android suffisent. C'est vrai jusqu'au moment de la signature et de l'upload. Et là, tout le monde ouvre Xcode ou glisse un .aab dans le navigateur. Ça marche une fois. Ça ne marche plus quand un nouveau développeur arrive, quand un certificat expire, ou quand vous devez livrer trois builds par jour à la QA.

Fastlane, maintenu par Google depuis le rachat en 2017, résout exactement ce problème : orchestrer la signature, l'upload et les notifications de manière reproductible et scriptable. Le projet MAUI produit déjà les bons artefacts, Fastlane n'a plus qu'à les prendre en charge. Contrairement à Xamarin, où msbuild et Fastlane cohabitaient mal, la CLI dotnet retourne des codes de sortie propres et écrit ses artefacts à des chemins prévisibles. L'intégration en devient triviale.

Concrètement, Fastlane vous donne : Match (gestion Git des certificats), Pilot (TestFlight), Supply (Google Play), Gym (build Xcode, utile pour les projets iOS-only mais optionnel pour MAUI), Scan (tests), et un système de plugins pour Slack, Teams, ou n'importe quel webhook. Vous décrivez vos releases en Ruby dans un Fastfile, vous les exécutez en local ou en CI, et le comportement est identique.

Prérequis et installation propre

Avant de toucher au moindre Fastfile, vérifiez cette checklist. Sauter une étape ici, c'est perdre deux heures de debug plus tard (vécu, plus d'une fois).

  • Ruby 3.3+ installé via rbenv ou asdf. Jamais le Ruby système de macOS.
  • Xcode 16.2+ et Command Line Tools (pour la lane iOS uniquement).
  • Android SDK avec build-tools 35 minimum et un ANDROID_HOME exporté.
  • .NET SDK 10.0.100 ou supérieur avec la charge de travail maui installée : dotnet workload install maui.
  • Un compte App Store Connect avec une clé API générée (rôle Admin ou App Manager).
  • Un compte de service Google Cloud avec le rôle « Service Account User » sur votre projet Play Console.

Installez Fastlane avec Bundler pour verrouiller la version. C'est non négociable si vous voulez que le CI reproduise votre environnement local.

# Gemfile à la racine du dépôt
source "https://rubygems.org"
gem "fastlane", "~> 2.226"
gem "fastlane-plugin-versioning_android", "~> 0.1"
bundle config set --local path 'vendor/bundle'
bundle install
bundle exec fastlane init

La commande init crée un dossier fastlane/ avec un Fastfile, un Appfile et un Pluginfile. Committez le tout sauf vendor/, qui va dans .gitignore.

Structure de projet recommandée

Pour un projet MAUI single-project, voici l'arborescence que j'utilise sur tous mes clients. Elle sépare clairement l'app, les tests et les scripts de release.

MyApp/
├── src/
│   ├── MyApp/                    # projet MAUI (.csproj)
│   │   └── MyApp.csproj
│   └── MyApp.Tests/
├── fastlane/
│   ├── Fastfile
│   ├── Appfile
│   ├── Matchfile
│   ├── Pluginfile
│   └── metadata/                 # screenshots, textes App Store
├── .github/workflows/
│   └── release.yml
├── Gemfile
├── Gemfile.lock
└── global.json                   # verrouille la version du SDK .NET

Le fichier global.json est souvent oublié. Sans lui, votre CI installera peut-être un SDK plus récent que votre poste et cassera un build reproductible. Verrouillez la version comme vous verrouillez vos gems.

Builder l'IPA et l'AAB depuis Fastlane

C'est ici que .NET MAUI se distingue de Xamarin : plutôt que d'appeler xcodebuild via Gym, on invoque directement dotnet publish depuis une action Fastlane sh. C'est plus simple, plus rapide, et ça évite les problèmes de compatibilité entre Xamarin.iOS et les versions récentes de Xcode.

desc "Build the iOS .ipa via dotnet publish"
lane :build_ios do
  sh(
    "cd .. && dotnet publish src/MyApp/MyApp.csproj " \
    "-c Release -f net10.0-ios " \
    "/p:ArchiveOnBuild=true " \
    "/p:RuntimeIdentifier=ios-arm64 " \
    "/p:CodesignKey=\"Apple Distribution: My Company (TEAMID)\" " \
    "/p:CodesignProvision=\"MyApp AppStore\""
  )
end

desc "Build the Android .aab via dotnet publish"
lane :build_android do
  sh(
    "cd .. && dotnet publish src/MyApp/MyApp.csproj " \
    "-c Release -f net10.0-android " \
    "/p:AndroidPackageFormat=aab " \
    "/p:AndroidKeyStore=false"   # géré par Play App Signing
  )
end

Notez le cd .. : Fastlane s'exécute depuis fastlane/, or le .csproj est à la racine du dépôt. Notez aussi AndroidKeyStore=false. Depuis août 2021, Google exige les AAB et gère la signature en son nom via Play App Signing. Vous fournissez juste une upload key, que Fastlane peut créer une fois pour toutes.

Fastlane Match : gérer les certificats iOS sans douleur

Match est la fonctionnalité pour laquelle j'installe Fastlane, même sur les petits projets. Le principe est simple : les certificats et provisioning profiles sont stockés dans un dépôt Git privé (ou un bucket S3, ou GCS), chiffrés avec une passphrase, et synchronisés sur toutes les machines qui en ont besoin (postes de dev, runners CI, tout).

Créez un dépôt privé dédié, par exemple myapp-certificates, puis initialisez Match :

bundle exec fastlane match init

Répondez « git » comme storage mode, indiquez l'URL SSH du dépôt, puis générez les certificats de distribution :

bundle exec fastlane match appstore --app_identifier com.mycompany.myapp

Match va, dans l'ordre : créer le certificat sur App Store Connect si nécessaire, créer le provisioning profile, chiffrer les deux avec la MATCH_PASSWORD, les pousser dans le dépôt Git, et les installer dans votre trousseau. Sur le prochain poste, un simple bundle exec fastlane match appstore --readonly les récupère et les installe. Fini les « peux-tu m'exporter ton profil ? » sur Slack.

Pour la CI, générez une clé API App Store Connect dans App Store Connect > Utilisateurs et accès > Clés et exposez-la comme JSON via APP_STORE_CONNECT_API_KEY. Match l'utilisera plutôt que votre mot de passe Apple ID. C'est indispensable pour contourner la double authentification en environnement headless.

Pilot et TestFlight : livrer aux testeurs internes

Une fois l'IPA construit et signé, Pilot le pousse sur TestFlight et notifie les groupes de testeurs. La lane tient en cinq lignes :

desc "Ship a beta build to TestFlight"
lane :beta_ios do
  match(type: "appstore", readonly: true)
  build_ios
  pilot(
    ipa: "src/MyApp/bin/Release/net10.0-ios/ios-arm64/publish/MyApp.ipa",
    skip_waiting_for_build_processing: true,
    changelog: "Build automatisé du #{Time.now.strftime('%Y-%m-%d %H:%M')}",
    distribute_external: false,
    groups: ["QA Interne"]
  )
end

Le paramètre skip_waiting_for_build_processing: true est celui que la plupart des équipes ratent. Sans ça, la lane attend qu'Apple traite le build (jusqu'à 30 minutes), ce qui fait exploser la facture CI. Envoyez et sortez : les testeurs recevront la notification quand le traitement sera fini.

Si vous voulez notifier Slack, ajoutez à la fin de la lane :

slack(
  message: "Nouveau build iOS #{get_version_number} disponible sur TestFlight",
  slack_url: ENV["SLACK_WEBHOOK_URL"]
)

Supply : automatiser Google Play Console

Côté Android, Supply joue le même rôle que Pilot. Il faut d'abord créer un compte de service Google Cloud et lui donner accès à votre application dans Play Console.

  1. Dans Google Cloud Console, créez un compte de service dédié à Fastlane.
  2. Générez une clé JSON, c'est ce fichier que Supply consomme.
  3. Dans Google Play Console, allez dans « Utilisateurs et autorisations » et invitez l'adresse email du compte de service. Donnez-lui les droits « Release manager » sur votre app uniquement.
  4. Attendez 24h. Google met du temps à propager les droits, c'est documenté et pénible.

Validez la connexion avec :

bundle exec fastlane supply init \
  --package_name com.mycompany.myapp \
  --json_key ./fastlane/play-service-account.json

Puis créez la lane beta Android :

desc "Ship a beta build to Google Play Internal Testing"
lane :beta_android do
  build_android
  supply(
    package_name: "com.mycompany.myapp",
    aab: "src/MyApp/bin/Release/net10.0-android/publish/com.mycompany.myapp-Signed.aab",
    track: "internal",
    release_status: "draft",
    json_key: ENV["SUPPLY_JSON_KEY_PATH"],
    skip_upload_apk: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
end

Les cinq skip_upload_* évitent les erreurs silencieuses où Supply cherche des screenshots dans fastlane/metadata/ et échoue sans message clair. Activez-les uniquement quand vous êtes prêt à gérer les métadonnées via Fastlane.

Fastfile complet pour .NET MAUI 10

Voici le Fastfile que j'utilise en production, à copier-coller et adapter. Il combine tout ce qui précède, ajoute une lane release pour production, et une lane tests.

default_platform(:ios)

CSPROJ_PATH = "src/MyApp/MyApp.csproj"

platform :ios do
  desc "Beta iOS -> TestFlight"
  lane :beta do
    match(type: "appstore", readonly: true)
    increment_build_number_maui
    sh("cd .. && dotnet publish #{CSPROJ_PATH} -c Release -f net10.0-ios /p:ArchiveOnBuild=true")
    pilot(
      ipa: "../src/MyApp/bin/Release/net10.0-ios/ios-arm64/publish/MyApp.ipa",
      skip_waiting_for_build_processing: true,
      distribute_external: false,
      groups: ["QA Interne"]
    )
  end

  desc "Release iOS -> App Store"
  lane :release do
    match(type: "appstore", readonly: true)
    increment_build_number_maui
    sh("cd .. && dotnet publish #{CSPROJ_PATH} -c Release -f net10.0-ios /p:ArchiveOnBuild=true")
    deliver(
      ipa: "../src/MyApp/bin/Release/net10.0-ios/ios-arm64/publish/MyApp.ipa",
      submit_for_review: true,
      automatic_release: false,
      force: true,
      skip_screenshots: true,
      skip_metadata: false
    )
  end
end

platform :android do
  desc "Beta Android -> Play Internal"
  lane :beta do
    increment_build_number_maui
    sh("cd .. && dotnet publish #{CSPROJ_PATH} -c Release -f net10.0-android /p:AndroidPackageFormat=aab")
    supply(
      package_name: "com.mycompany.myapp",
      aab: "../src/MyApp/bin/Release/net10.0-android/publish/com.mycompany.myapp-Signed.aab",
      track: "internal",
      json_key: ENV["SUPPLY_JSON_KEY_PATH"],
      skip_upload_apk: true, skip_upload_metadata: true,
      skip_upload_images: true, skip_upload_screenshots: true
    )
  end
end

def increment_build_number_maui
  build = Time.now.strftime("%Y%m%d%H").to_i
  sh("cd .. && sed -i.bak 's|<ApplicationVersion>.*</ApplicationVersion>|<ApplicationVersion>#{build}</ApplicationVersion>|' #{CSPROJ_PATH}")
end

Versionning automatique : ApplicationVersion et ApplicationDisplayVersion

MAUI expose deux propriétés dans le .csproj : ApplicationVersion (le build number, entier, doit strictement croître à chaque upload sur les stores) et ApplicationDisplayVersion (la version visible : 1.4.2). Fastlane peut piloter les deux.

Pour le build number, la stratégie « timestamp » (YYYYMMDDHH) présentée dans increment_build_number_maui ci-dessus est celle que je recommande. Elle garantit la monotonie sans passer par un compteur partagé. Pour la version marketing, appuyez-vous sur votre pipeline CI/CD .NET MAUI 10 et git describe --tags pour dériver la version depuis le dernier tag Git.

Attention à un piège spécifique à iOS : Apple exige que CFBundleVersion (mappé à ApplicationVersion) soit strictement supérieur à celui de la dernière build acceptée sur TestFlight, pas juste sur le store. Un rebuild sans incrément déclenche l'erreur ITMS-90062. J'ai perdu une soirée entière là-dessus avant de basculer au timestamp ; il évite ça naturellement.

Gérer les secrets dans GitHub Actions

Aucune clé, aucun mot de passe, aucun certificat ne doit vivre dans le dépôt principal. Voici la liste minimale à déclarer dans Settings > Secrets and variables > Actions :

  • MATCH_PASSWORD : la passphrase de chiffrement des certificats iOS.
  • MATCH_GIT_URL : l'URL SSH du dépôt de certificats (ou HTTPS + token si vous préférez).
  • APP_STORE_CONNECT_API_KEY_ID, APP_STORE_CONNECT_API_ISSUER_ID, APP_STORE_CONNECT_API_KEY_CONTENT : la clé API Apple, décomposée pour éviter les problèmes de newlines.
  • SUPPLY_JSON_KEY : le contenu JSON du compte de service Google, en base64 idéalement.
  • SSH_PRIVATE_KEY : pour cloner le dépôt Match depuis le runner.

Un exemple de workflow qui invoque Fastlane, aligné avec le guide sur CI/CD .NET MAUI 10 avec GitHub Actions :

name: Release
on:
  push:
    tags: ['v*']

jobs:
  ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - run: dotnet workload install maui
      - uses: ruby/setup-ruby@v1
        with: { ruby-version: '3.3', bundler-cache: true }
      - name: Release iOS
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
        run: bundle exec fastlane ios beta

Pour le format exact des secrets et la gestion des clés API Apple, la documentation Fastlane sur l'App Store Connect API reste la source la plus à jour.

Pièges classiques et comment les éviter

La documentation officielle de Fastlane est excellente, mais elle ne couvre pas les spécificités MAUI. Voici les cinq erreurs que je vois le plus souvent en audit, dont plusieurs que j'ai commises moi-même en shippant la première version d'un projet MAUI l'an dernier.

  1. Match qui régénère les certificats à chaque run. Toujours utiliser readonly: true en CI. La lane locale peut être en lecture-écriture pour créer initialement les certificats, la CI jamais.
  2. Provisioning profile absent du .csproj. Il faut passer /p:CodesignProvision="Nom Exact" à dotnet publish, sinon l'archive iOS est bien créée mais non signée pour distribution.
  3. Play Console refuse l'AAB avec « Cette signature ne correspond pas ». Signe que vous n'utilisez pas Play App Signing correctement. Regénérez l'upload key et enrôlez l'app dans Play App Signing depuis la Play Console.
  4. Rate-limit App Store Connect. Apple limite les appels à environ 200 par heure. Si vous testez Match en boucle, vous serez bloqué. Utilisez --verbose pour voir les headers X-Rate-Limit et espacez les runs.
  5. Ruby système sur macOS. Le Ruby livré avec macOS ne peut pas installer de gems sans sudo. Passez par rbenv ou asdf ; la charge cognitive économisée vaut largement les cinq minutes d'installation.

Une fois ces cinq pièges connus, la maintenance devient triviale. Sur mes projets, une release iOS + Android complète prend environ 90 secondes de wall-clock CI pour la partie Fastlane, plus le temps de build MAUI (typiquement 6 à 10 minutes selon la taille de l'app et l'utilisation ou non de NativeAOT, voir le guide sur les performances .NET MAUI 10).

Questions fréquentes

Fastlane fonctionne-t-il avec .NET MAUI 10 sans plugin spécifique ?

Oui. Fastlane traite les artefacts .ipa et .aab produits par dotnet publish comme n'importe quel autre binaire. Aucun plugin dédié MAUI n'est nécessaire ; les actions standards Match, Pilot, Deliver et Supply couvrent tout le pipeline de release à partir de Fastlane 2.223.

Quelle est la différence entre Pilot et Deliver ?

Pilot pousse un build sur TestFlight (distribution beta interne ou externe). Deliver soumet une version pour review App Store et publie la version production. Un pipeline typique utilise Pilot dans la lane beta et Deliver dans la lane release.

Faut-il encore un keystore Android en 2026 ?

Oui, mais uniquement en tant qu'upload key. Depuis août 2021, Google Play exige les AAB et gère la clé de signature finale via Play App Signing. Votre upload key sert seulement à prouver à Google que vous êtes bien l'auteur de la build ; elle peut être stockée dans un secret CI et regénérée si perdue.

Comment gérer plusieurs environnements (dev, staging, prod) avec Fastlane ?

Utilisez des schemes Fastlane ou des variables d'environnement. La convention la plus lisible est un fichier .env.production, .env.staging, etc., chargé via --env staging. Chaque environnement peut avoir son bundle identifier, ses certificats Match et sa piste Play Console dédiée.

Peut-on lancer Fastlane sans macOS pour la partie Android ?

Oui. La lane Android tourne sur Linux ou Windows tant que le SDK Android et .NET 10 y sont installés. La lane iOS, en revanche, exige macOS pour Xcode et les outils de signature Apple. Séparer les jobs ios (macOS) et android (Linux) dans GitHub Actions divise significativement les coûts CI.

Sofia Rodriguez
À propos de l'auteur Sofia Rodriguez

Mobile DevOps engineer focused on the unglamorous stuff: build pipelines, signing, store releases, and the tooling that keeps teams shipping.