Fastlane pentru .NET MAUI 2026: Automatizare Semnare, TestFlight și Google Play
Ghid complet pentru automatizarea semnării și publicării unei aplicații .NET MAUI cu fastlane: match pentru certificate iOS, pilot pentru TestFlight, supply pentru Google Play, plus integrare GitHub Actions.
Fastlane pentru .NET MAUI este un set de instrumente Ruby open-source care automatizează semnarea, versionarea și publicarea aplicațiilor iOS și Android generate de dotnet publish, eliminând pașii manuali din Xcode și Google Play Console. Cele mai multe echipe sar peste configurarea fastlane la începutul proiectului, iar apoi ajung să piardă zile întregi la fiecare release cu certificate expirate, provisioning profiles greșite și build numbers duplicate. Am pățit-o eu însumi pe un proiect MAUI 8 anul trecut, când un CFBundleVersion duplicat mi-a blocat un release critic cu 30 de minute înainte de o demonstrație pentru client. În ghidul de față arăt pipeline-ul complet: match pentru certificate, pilot pentru TestFlight, supply pentru Google Play și integrarea cu GitHub Actions.
Fastlane 2.222+ funcționează perfect cu .NET MAUI 9 dacă apelezi dotnet publish înainte de gym sau gradle.
fastlane match stochează certificatele iOS într-un repo Git privat criptat, eliminând nevoia sincronizării manuale între developeri și CI runner-e.
App Store Connect API Key (issuer ID + key ID + .p8) înlocuiește parola Apple ID și evită prompt-urile 2FA în pipeline.
fastlane supply încarcă AAB-ul pe Google Play folosind un service account JSON, așa că nu mai e nevoie să deschizi consola manual.
Într-un GitHub Actions runner macOS, un release complet iOS plus Android durează aproximativ 14 minute pentru un proiect MAUI mediu.
Versioning automat prin increment_build_number previne coliziunile de CFBundleVersion care blochează upload-ul în App Store.
Ce este fastlane și cum funcționează cu .NET MAUI?
Fastlane este un CLI Ruby creat inițial de Felix Krause și menținut acum de Google, care wrap-ează peste 400 de „actions" pentru automatizarea sarcinilor de mobile release engineering. Filozofia lui e simplă: fiecare pas manual pe care îl faci în Xcode, Transporter, App Store Connect sau Play Console poate fi descris într-un fișier Fastfile și rulat identic pe laptopul unui developer, într-un runner de CI, sau pe un Mac mini vechi din colț.
Pentru .NET MAUI 9, fastlane nu înlocuiește dotnet publish. Îl completează. Fluxul arată așa: dotnet publish -f net9.0-ios -c Release produce un fișier .ipa (sau folder .app), iar apoi acțiunile fastlane (match, gym, pilot, deliver) preiau artefactul, îl semnează și îl încarcă. Pentru Android, dotnet publish -f net9.0-android -c Release generează AAB-ul semnat cu keystore-ul tău, iar supply îl trimite la Google Play. E mult mai flexibil decât alternativele integrate în IDE. De exemplu, publish direct din Visual Studio nu îți permite să programezi rollout gradual sau să gestionezi mai multe tracks în paralel.
Concurenții principali sunt .NET MAUI publish targets integrate, Bitrise Steps și Codemagic workflows. Fastlane câștigă când vrei portabilitate: același Fastfile merge pe GitHub Actions, Azure Pipelines, sau local, fără să depinzi de un vendor. Dacă deja rulezi pipeline CI/CD MAUI pe GitHub Actions, fastlane se integrează în 3-4 pași în workflow-ul YAML existent.
Instalarea și configurarea inițială
Prerequisite: Ruby 3.1+ (folosește rbenv sau chruby, nu Ruby-ul de sistem macOS), Xcode Command Line Tools, .NET 9 SDK cu workload-urile maui-ios și maui-android, plus Bundler. Cele mai multe echipe sar peste bundler și instalează fastlane global, iar apoi când CI-ul e pe altă versiune de gem obții surprize la fiecare release. Sincer, este cel mai simplu detaliu care îți salvează o săptămână de „merge la mine, nu merge pe CI".
La fastlane init alege opțiunea „Manual setup", pentru că variantele automate presupun un proiect Xcode nativ și nu detectează corect structura MAUI. Vei termina cu două fișiere: Appfile (bundle ID plus team ID) și Fastfile (lane-urile efective). Adaugă și un .env.default pentru variabilele publice și un .env.secret pentru chei (adăugat obligatoriu la .gitignore).
# fastlane/Appfile
app_identifier "com.acme.mauishop"
apple_id ENV["APPLE_ID"]
team_id ENV["APPLE_TEAM_ID"]
itc_team_id ENV["APP_STORE_CONNECT_TEAM_ID"]
for_platform :android do
package_name "com.acme.mauishop"
json_key_file "fastlane/google-play-service-account.json"
end
Fastlane match: certificate iOS partajate
Match rezolvă cea mai enervantă problemă în signing iOS: sincronizarea certificatelor și a provisioning profiles între developeri și mașini CI. În loc să exportați manual din Keychain, match creează un repo Git privat (sau bucket S3/GCS) care stochează certificatele criptate cu OpenSSL AES-256-CBC. Fiecare mașină rulează bundle exec fastlane match și primește exact ce are nevoie.
Setup-ul inițial (o singură dată per organizație):
# Creează un repo privat separat: acme-certificates.git
bundle exec fastlane match init
# Alegi „git" ca storage și pui URL-ul repo-ului
Adaugă în Fastfile lane-urile pentru development și App Store:
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Sync development certificates"
lane :sync_dev_certs do
match(
type: "development",
app_identifier: "com.acme.mauishop",
readonly: is_ci
)
end
desc "Sync App Store certificates"
lane :sync_appstore_certs do
match(
type: "appstore",
app_identifier: "com.acme.mauishop",
readonly: is_ci,
api_key_path: "fastlane/appstore_connect_key.json"
)
end
end
Parametrul readonly: is_ci este critic. Pe CI, runner-ul nu are voie să creeze certificate noi (ar cauza revocarea celor active). Doar developer-ul care rulează lane-ul fărăreadonly pe laptop poate crea sau regenera. Aceasta e o convenție de siguranță pe care echipele o descoperă mereu prea târziu.
Pentru App Store Connect API, generează o cheie din App Store Connect → Users and Access → Keys, descarcă fișierul .p8, notează Issuer ID și Key ID. Împachetează-le într-un JSON:
Aici apare bucla specifică pentru .NET MAUI: gym (aka build_app) e scris pentru proiecte .xcodeproj sau .xcworkspace, dar MAUI generează .ipa-ul direct prin MSBuild. Soluția pe care o folosesc în producție este să sar peste gym și să apelez dotnet publish printr-o acțiune sh, apoi să încarc rezultatul cu upload_to_testflight direct.
# fastlane/Fastfile (continuă în platform :ios do)
desc "Build .NET MAUI iOS Release IPA"
lane :build_ios do
sync_appstore_certs
# Calculează build number din commit count
build_num = number_of_commits
sh(
"cd .. && dotnet publish src/AcmeShop/AcmeShop.csproj " \
"-f net9.0-ios " \
"-c Release " \
"/p:ArchiveOnBuild=true " \
"/p:ApplicationVersion=#{build_num} " \
"/p:ApplicationDisplayVersion=1.4.0 " \
"/p:CodesignKey='Apple Distribution: Acme Inc (TEAMID)' " \
"/p:CodesignProvision='match AppStore com.acme.mauishop' " \
"/p:RuntimeIdentifier=ios-arm64"
)
# IPA-ul apare aici după publish
ipa_path = "../src/AcmeShop/bin/Release/net9.0-ios/ios-arm64/publish/AcmeShop.ipa"
ENV["IPA_OUTPUT_PATH"] = ipa_path
end
Cheia sunt cei doi parametri MSBuild. CodesignKey trebuie să conțină numele exact al identității din Keychain (rulează security find-identity -v -p codesigning pentru a-l lista), iar CodesignProvision trebuie să corespundă cu numele profile-ului instalat de match (formatul e mereu match {Type} {BundleId}). Dacă folosești un bundle ID diferit între dev și prod (frecvent pentru build-uri interne), fiecare va avea propriul provisioning profile.
Publicare TestFlight cu pilot
Odată ce ai IPA-ul, pilot (aka upload_to_testflight) încarcă build-ul în TestFlight, așteaptă procesarea Apple, și opțional îl distribuie unui grup de tester-i externi. Timpul de procesare la Apple e undeva între 5 și 15 minute și e complet asincron. Pilot poate aștepta sau poate ieși imediat, tu decizi.
desc "Upload to TestFlight"
lane :beta do
build_ios
upload_to_testflight(
api_key_path: "fastlane/appstore_connect_key.json",
ipa: ENV["IPA_OUTPUT_PATH"],
skip_waiting_for_build_processing: false,
changelog: "Bug fixes and stability improvements.\n\nBuild #{number_of_commits}",
distribute_external: true,
groups: ["QA Team", "Beta Testers"],
notify_external_testers: true,
beta_app_review_info: {
contact_email: "[email protected]",
contact_first_name: "Sofia",
contact_last_name: "Release Manager",
contact_phone: "+40712345678",
demo_account_name: "[email protected]",
demo_account_password: ENV["DEMO_ACCOUNT_PASSWORD"],
notes: "Login demo cu credențialele de mai sus. Aplicația necesită internet pentru sync."
}
)
slack(
message: "TestFlight upload OK — build #{number_of_commits}",
channel: "#mobile-releases",
default_payloads: [:git_branch, :last_git_commit_message]
)
end
Publicare App Store cu deliver
Pentru release-uri publice, deliver (aka upload_to_app_store) încarcă metadata (descrieri, screenshot-uri, keywords, „What's New"), asset-urile, și trimite build-ul spre review. Este disproporționat de puternic pentru cât de puțin e folosit. Echipele care nu-l adoptă redactează același „What's New" în fiecare limbă manual, la 3 dimineața.
desc "Submit to App Store review"
lane :release do
build_ios
upload_to_app_store(
api_key_path: "fastlane/appstore_connect_key.json",
ipa: ENV["IPA_OUTPUT_PATH"],
skip_screenshots: true,
skip_metadata: false,
metadata_path: "./fastlane/metadata",
force: true,
submit_for_review: true,
automatic_release: false,
submission_information: {
add_id_info_uses_idfa: false,
export_compliance_uses_encryption: false,
content_rights_contains_third_party_content: false
},
precheck_include_in_app_purchases: false
)
end
Structura folderului metadata este strictă: câte un subfolder per locale (en-US, ro, de-DE) cu fișiere description.txt, keywords.txt, release_notes.txt, promotional_text.txt. Poți popula inițial cu bundle exec fastlane deliver download_metadata care aduce ce e deja în App Store Connect. De aici încolo, orice modificare vine prin PR în Git, deci ai audit trail complet.
Android: keystore și supply pentru Google Play
Partea Android este mai simplă pentru că .NET MAUI generează AAB-ul semnat direct dacă îi dai keystore-ul. Începe prin crearea unui keystore de producție (o dată per aplicație, backup-uit obligatoriu într-un password manager sau HSM):
Pentru service account-ul Google Play, mergi la Google Play Console → Setup → API access, creează un nou service account în Google Cloud, dă-i rolul „Release Manager" în Play Console, și descarcă JSON key-ul. Trebuie să aștepți aproximativ 24h până când permisiunile propagă înainte de primul upload. Da, exact 24 de ore. Am încercat să grăbesc procesul și n-a mers.
# fastlane/Fastfile
platform :android do
desc "Build .NET MAUI Android AAB"
lane :build_android do
build_num = number_of_commits
sh(
"cd .. && dotnet publish src/AcmeShop/AcmeShop.csproj " \
"-f net9.0-android " \
"-c Release " \
"/p:AndroidPackageFormats=aab " \
"/p:ApplicationVersion=#{build_num} " \
"/p:ApplicationDisplayVersion=1.4.0 " \
"/p:AndroidKeyStore=true " \
"/p:AndroidSigningKeyStore=#{ENV['KEYSTORE_PATH']} " \
"/p:AndroidSigningKeyAlias=acme " \
"/p:AndroidSigningKeyPass=#{ENV['KEYSTORE_PASSWORD']} " \
"/p:AndroidSigningStorePass=#{ENV['KEYSTORE_PASSWORD']}"
)
ENV["AAB_OUTPUT_PATH"] =
"../src/AcmeShop/bin/Release/net9.0-android/publish/com.acme.mauishop-Signed.aab"
end
desc "Upload to Google Play internal track"
lane :beta do
build_android
upload_to_play_store(
package_name: "com.acme.mauishop",
aab: ENV["AAB_OUTPUT_PATH"],
track: "internal",
release_status: "completed",
skip_upload_apk: true,
skip_upload_metadata: false,
skip_upload_changelogs: false,
skip_upload_images: true,
skip_upload_screenshots: true,
json_key: "fastlane/google-play-service-account.json"
)
end
desc "Promote from internal to production with staged rollout"
lane :promote do
upload_to_play_store(
package_name: "com.acme.mauishop",
track: "internal",
track_promote_to: "production",
rollout: "0.1", # 10% rollout inițial
json_key: "fastlane/google-play-service-account.json"
)
end
end
Pattern-ul cu track-uri (internal → alpha → beta → production) plus track_promote_to îți permite să promovezi același AAB între canale fără să rebuilzi. Economisești timp și riști mai puțin să introduci diferențe între ce ai testat și ce ajunge la utilizatori. Vezi și strategia de testing pentru .NET MAUI pentru validarea build-urilor înainte de promovare.
Integrare completă în GitHub Actions
Aici pun cap la cap tot: pipeline-ul YAML care rulează pe fiecare merge în main, împarte build-urile pe două job-uri paralele (macOS pentru iOS, Ubuntu pentru Android), și publică în TestFlight plus Google Play internal track.
Toate secretele (MATCH_PASSWORD, service account JSON-uri, keystore base64) merg în GitHub → Settings → Secrets and variables → Actions. Pentru rotația certificatelor iOS (Apple obligă o dată/an), rulează local bundle exec fastlane match nuke distribution urmat de bundle exec fastlane match appstore. Documentează procedura într-un runbook. La 2 dimineața, când certificatele expiră, nu vrei s-o improvizezi.
Gotchas frecvente și cum le eviți
Cele mai multe echipe sar peste faza asta, iar apoi debuggează la 4 AM. Iată lista scurtă, din propria mea colecție de dureri de cap:
Duplicate CFBundleVersion la upload TestFlight. Rezolvi cu build number derivat din number_of_commits sau timestamp, nu manual în .csproj.
Match nu găsește profile-ul „match AppStore com.acme.mauishop". Verifică că CodesignProvision în MSBuild se potrivește exact. E ușor să confunzi MATCH_APPSTORE_COM_ACME (env var setat de match) cu string-ul literal, așa că folosește env-varul în MSBuild.
Google Play refuză AAB-ul cu „APK signing key mismatch". Play App Signing e activat și primul upload al app-ului a fost cu alt keystore. Trebuie să exporți keystore-ul original și să-l încarci ca „upload key" nou în Play Console.
Fastlane hang pe „Waiting for App Store Connect". Fișierul .p8 sau issuer ID greșit. Testează separat cu bundle exec fastlane run app_store_connect_api_key înainte să integrezi în lane.
Ruby version mismatch între dev și CI. Adaugă un fișier .ruby-version la root-ul repo-ului fastlane. GitHub Actions îl respectă automat cu ruby/setup-ruby@v1.
MSBuild nu găsește identitatea în Keychain pe runner-ul CI. Match instalează certificatele în Keychain-ul temporar creat automat, dar pe self-hosted runners trebuie să deblochezi Keychain-ul cu security unlock-keychain.
Pentru migrări dinspre Xamarin.Forms, vezi și ghidul de migrare Xamarin la .NET MAUI, pentru că exact configurarea signing și publish este una din diferențele majore care sparg pipeline-urile vechi. Fastlane rămâne compatibil, dar comenzile MSBuild și numele property-urilor se schimbă complet.
Întrebări frecvente
Pot folosi fastlane cu .NET MAUI fără Xcode instalat?
Nu pentru iOS, pentru că codesign, xcrun altool și SDK-urile iOS fac parte din Xcode. Poți însă folosi fastlane doar pentru Android pe Linux/Windows, unde nu ai nevoie de Xcode. Un runner Ubuntu în GitHub Actions e suficient pentru lane-urile Android.
Care e diferența între fastlane deliver și fastlane pilot?
pilot (upload_to_testflight) trimite build-uri pentru testare internă sau externă via TestFlight. deliver (upload_to_app_store) trimite build-ul plus metadata către review pentru release public în App Store. Un release complet le folosește pe ambele: pilot pentru staging, deliver pentru production.
Fastlane match îmi șterge certificatele existente?
Doar dacă rulezi fastlane match nuke explicit. Un fastlane match appstore normal reutilizează certificatele existente dacă sunt valide și în repo. Match revocă doar atunci când tu ceri asta explicit sau când limita Apple (3 certificate per team) e atinsă.
Cum stochez secretele fastlane pentru mai mulți developeri?
Pentru MATCH_PASSWORD și API keys folosește un password manager al echipei (1Password Teams, Bitwarden Organization) cu un „shared vault" pentru mobile team. Pentru CI, folosește secretele native (GitHub Actions Secrets, Azure Key Vault). Nu partaja niciodată prin Slack sau email.
Ce fac dacă App Store Connect API returnează 401 din CI?
În 90% din cazuri e problema de escape-uire a private key-ului din .p8: caracterele \n trebuie păstrate literal în JSON-ul cheii. Rulează local bundle exec fastlane run app_store_connect_api_key cu același JSON. Dacă merge local, dar nu în CI, e problemă de secret formatting (rescrie secretul base64-encoded și decodează în workflow).
Ghid pas cu pas pentru configurarea unui pipeline CI/CD cu GitHub Actions pentru aplicații .NET MAUI. Acoperă build-uri Android și iOS, semnare automată, upload în Google Play și TestFlight, plus optimizări practice pentru .NET 10.