CI/CD за .NET MAUI с GitHub Actions: Автоматизиран билд, подпис и публикуване (2026)
Пълен pipeline за CI/CD на .NET MAUI с GitHub Actions: подпис на Android AAB, iOS IPA и автоматично качване в Google Play и App Store с готови примери.
CI/CD за .NET MAUI с GitHub Actions означава да билдвате Android, iOS, Windows и macOS от един workflow, който подписва артефактите автоматично и ги качва в Google Play и App Store Connect без ръчна намеса. Повечето екипи, с които съм работил, стартират проекта с локален dotnet publish, а месеци по-късно се чудят защо всяка нова версия иска половин ден и молитва към Xcode. Тази статия описва пълния pipeline (от подготовката на secrets до публикуването) така, както го изграждам за продукция.
За пълен CI/CD ви трябват три runner-а: ubuntu-latest за Android, macos-14 за iOS/macOS и windows-latest за WinUI. Всеки с инсталиран .NET 10 SDK и MAUI workload.
Android се подписва с keystore, съхранен като base64 GitHub secret и декодиран в job-а. iOS изисква P12 сертификат, provisioning profile и App Store Connect API key (не парола за Apple ID).
Cache-ването на NuGet пакетите и MAUI workload-а спестява 4–7 минути на билд. Без него всеки job тегли към 1.5 GB от нула.
Google Play качването минава през сервизен акаунт JSON и r0adkll/upload-google-play. App Store Connect работи най-надеждно с Fastlane pilot, а не с xcrun altool, който е deprecated от 2025.
macOS runner-ите струват 10× повече от Linux, така че минимизирайте iOS билдовете чрез trigger-и по tag или ръчен workflow_dispatch, а не при всеки push.
Failed release билдовете почти винаги се дължат на грешна версия на SDK-то, липсваща workload инсталация или изтекъл provisioning profile. Три неща, които pipeline-ът трябва да проверява явно.
Защо повечето екипи бъркат CI/CD за .NET MAUI
Честно казано, повечето екипи пропускат тази част и после се чудят защо release-ът в петък отнема осем часа. Типичният сценарий изглежда така: разработчикът има работещ dotnet build -f net10.0-android локално, копира командата в GitHub Actions workflow, добавя --configuration Release и очаква същия резултат. След това runner-ът гърми с „workload not installed", после с „keystore not found", после с „provisioning profile expired". И всяка грешка се появява едва в средата на 12-минутен билд.
Проблемът е, че локалната ви машина има три години акумулирано състояние: MAUI workload, инсталиран от Visual Studio, keystore в ~/.android, Apple Developer акаунт в Keychain, Xcode с автоматично управлявани сертификати. CI runner-ът е празен всеки път. Всяко от тези неща трябва да е явно декларирано в workflow-а, включително точната версия на .NET SDK, workload manifest-ът и таргет платформите. В моята практика съм виждал екипи, които правят release ръчно от лаптопа на един човек цели две години, защото CI никога не проработва напълно. Целта тук е обратното: pipeline, който не изисква „магическата машина на Иван".
Ако все още обмисляте архитектурата на самото приложение, започнете от MVVM с CommunityToolkit.Mvvm и след това се върнете тук. DevOps слоят предполага, че кодът вече е тестваем.
Какво ви трябва, преди да напишете първия workflow
Преди да отворите .github/workflows/, съберете следните артефакти. Ако някой липсва, спрете и го намерете. Да откриете, че нямате valid App Store Connect API key след 20 минути в CI, е болезнено (говоря от опит).
.NET SDK версия: .NET 10 SDK (10.0.100 или по-нова към август 2026). Заковете я в global.json, за да не се разминава между локална машина и CI.
MAUI workload:maui workload manifest, съвпадащ със SDK-то. Различните версии на workload-а хвърлят загадъчни MSB4062 грешки.
Android keystore:.jks или .keystore файл, който вече е използван за качване в Google Play. НЕ генерирайте нов за CI, защото Google Play отхвърля upload-и с различен ключ.
Apple certificates: Distribution сертификат (.p12) и App Store provisioning profile (.mobileprovision) за вашия bundle ID.
App Store Connect API key: JSON файл, генериран от App Store Connect → Users and Access → Keys. Има три полета: key_id, issuer_id, key (P8 съдържание).
Google Play service account: JSON, генериран от Google Cloud Console и upload-нат в Google Play Console → Settings → API access.
Всички чувствителни файлове отиват в GitHub Secrets. За бинарни файлове като .keystore и .p12 ги кодирайте в base64 с base64 -i keystore.jks -o keystore.b64 и поставете съдържанието като secret стойност. За JSON файлове поставяйте суровото съдържание, така избягвате escaping главоболията.
Как да настроите CI/CD за .NET MAUI с GitHub Actions
Скелетът на pipeline-а има три отделни job-а, по един за всяка платформа, плюс trigger, който решава кога да ги пуска. Всеки push към main билдва Android (евтино на Ubuntu). iOS билдва само при push на tag v*, защото macOS runner-ите струват. Ето базовия workflow, който после ще разширим:
Обърнете внимание на maui-android вместо цялостния maui workload. Инсталирането на пълния workload на Ubuntu тегли и iOS зависимости, които няма да използвате там, и губите 2–3 минути на билд. Инсталирайте само това, което ви трябва за конкретния runner.
За on: trigger-ите използвам pull_request за компилационна проверка, push към main за artifact качване към нашия staging Firebase App Distribution, и tag push за store release. Това разделение пази macOS минутите за реалните release билдове. Според официалната ценова политика на GitHub Actions, macOS минутите се таксуват с множител 10× спрямо Linux.
Билд и подпис на Android AAB стъпка по стъпка
Android подписването в CI изисква три secret-а: ANDROID_KEYSTORE_B64 (base64 на keystore файла), ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS и ANDROID_KEY_PASSWORD. Ето стъпките, които добавяте към build-android job-а след базовия билд:
Ключовият детайл: AndroidPackageFormat=aab, а не APK. Google Play изисква Android App Bundle от август 2021 за нови приложения и от октомври 2025 за обновления на съществуващи. Ако pipeline-ът ви още произвежда APK, upload-ът просто ще бъде отхвърлен.
Второ уточнение: $RUNNER_TEMP се изчиства автоматично след job-а, така че декодираният keystore не остава в кеша на runner-а. Никога не декодирайте в $GITHUB_WORKSPACE, защото Actions понякога persist-ват workspace-а между стъпки и рискувате keystore-ът да попадне в artifact.
Мога ли да билдна iOS без Mac и как става със сертификатите
Не, не можете да билдвате .NET MAUI iOS release без macOS машина, защото Apple изисква Xcode toolchain за подписване и ibtool компилацията на storyboards. GitHub Actions предоставя macos-14 runner-и с предварително инсталиран Xcode 16.2 (към август 2026), което е достатъчно за .NET 10 MAUI. Ето workflow-а за iOS:
Двете apple-actions стъпки заменят цялата стара практика с ръчно управление на Keychain и fastlane match. import-codesign-certs създава временен Keychain, който се изтрива в края на job-а. Това е важно, защото persistent Keychain на shared runner е security проблем. download-provisioning-profiles тегли профилите директно от App Store Connect през API, вместо да ги съхранявате като secret. Така profile-ът се обновява автоматично, когато го подновите в Apple Developer портала.
CodesignKey трябва да съвпадне точно с името на сертификата плюс team ID в скоби. Проверете го с security find-identity -v -p codesigning локално. За архитектурата, която ще подпишете, вижте и миграцията от Xamarin.Forms към .NET MAUI. Ако идвате от Xamarin, bundle ID може да изисква обновяване в App Store Connect.
Автоматично публикуване в Google Play и App Store
След като имате подписан AAB и IPA, upload-ът към store-овете е последната стъпка. За Google Play използвам r0adkll/upload-google-play, който работи със service account JSON:
- name: Upload to Google Play Internal
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SA_JSON }}
packageName: com.mycompany.myapp
releaseFiles: publish/android/*.aab
track: internal
status: completed
whatsNewDirectory: distribution/whatsnew
Track-ът internal е за първоначален smoke test с ограничена група тестъри. Качете там от CI, ръчно промотирайте към production, след като верифицирате. Никога не пускайте автоматичен upload директно към production track. При регресия имате часове, докато halt-нете разпространението.
За App Store Connect най-надеждният път през 2026 е Fastlane pilot (TestFlight) или deliver (App Store). xcrun altool беше премахнат от Xcode 16, а xcrun notarytool вече не поддържа iOS upload, само macOS notarization. Инсталирайте Fastlane и качете:
skip_waiting_for_build_processing е важен, защото Apple обработва билда 10–40 минути и без този флаг job-ът виси и харчи macOS минути напразно. Уведомете тестърите чрез отделен webhook, когато TestFlight изпрати email за готов билд.
Матрични билдове, кеширане и оптимизация
Веднъж работещ pipeline, следващата задача е да го направите бърз. Първият билд без cache отнема 14–18 минути. С правилно кеширане слизам до 6–8. Ключовете:
NuGet cache:actions/cache@v4 върху ~/.nuget/packages с ключ от hashFiles('**/*.csproj', '**/packages.lock.json'). Включете packages.lock.json, за да инвалидирате кеша, когато transitive зависимостите се променят.
MAUI workload cache: workload файловете живеят в ~/.dotnet. Кеширайте ~/.dotnet/sdk-manifests и ~/.dotnet/packs с ключ, зависещ от MAUI_VERSION.
Gradle cache (Android):~/.gradle/caches. Спестява около 2 минути при инкрементални билдове.
Xcode DerivedData: при iOS слагам ~/Library/Developer/Xcode/DerivedData в cache, но key-ът зависи от commit SHA, за да не се enable дефектни инкрементални билдове.
Матричните билдове са полезни, когато поддържате няколко target framework-а или конфигурации. Ето пример за multi-configuration Android билд:
За Native AOT в .NET MAUI билдовете отнемат 3–5 пъти повече време. Задължително ги пускайте само на release и никога на PR check.
Често срещани грешки и как да ги избегнете
Последният раздел е списък на нещата, които са ме будили в 2 през нощта. Проверявайте ги преди release, не след като pager-ът е звъннал.
„workload not installed" в средата на билд: обикновено означава, че dotnet workload install е успял, но с различна версия от очакваната от csproj-а. Заковете <MauiVersion> в Directory.Build.props и подавайте същата версия във workload install --version.
Android AAB се качва, но Play Console казва „unsigned": проверете дали използвате Play App Signing. От август 2021 Google съхранява истинския upload key на техните сървъри. Вашият keystore подписва само upload-а, а Google преподписва за разпространение. Ако сте генерирали нов keystore, upload-ът се отхвърля.
iOS „no matching profile found": bundle ID в csproj не съответства на profile-а. Отворете .mobileprovision с security cms -D -i profile.mobileprovision и сравнете application-identifier.
App Store Connect API ключ „invalid": P8 файлът трябва да включва header-ите -----BEGIN PRIVATE KEY-----. Ако сте копирали само base64 частта, ключът е технически валиден за base64 decoding, но невалиден като PKCS#8.
macOS runner-ът виси на „Restoring workloads": Apple периодично обновява network peering. Понякога NuGet.org е бавен от Azure macOS pool-а. Добавете --source https://api.nuget.org/v3/index.json --disable-parallel при упорити timeout-и.
Version code конфликт: Google Play изисква ApplicationVersion да бъде монотонно нарастващ integer. Използвайте ${{ github.run_number }} като -p:ApplicationVersion. Гарантирано уникален и нарастващ.
За детайлно проследяване на грешките в самото приложение след release, вижте препоръчания подход в REST API устойчивостта в .NET MAUI. Logging и telemetry са неразделна част от DevOps loop-а, а не отделна тема.
Често задавани въпроси
Колко струват macOS runner-ите в GitHub Actions за MAUI проект?
macOS runner-ите се таксуват с 10× множител спрямо Linux ($0.08/min срещу $0.008/min за standard runner-и към август 2026). Типичен iOS release билд от 15 минути струва около $1.20 срещу около $0.12 за Android. Личните и small team планове имат месечен безплатен лимит от 2000 минути (Linux еквивалент), което практически ограничава iOS билдовете до около 200 на месец.
Мога ли да използвам self-hosted Mac Mini за iOS билдове?
Да, self-hosted runner на M2 Mac Mini е около 3× по-бърз от macos-14 и се амортизира за около 6 месеца при активно ползване. Ограничения: сами управлявате Xcode обновления, security patches и Apple Silicon compatibility. За екипи под 5 души GitHub-hosted обикновено е по-лесно. За екипи с ежедневни iOS билдове self-hosted е икономически по-добър избор.
Как да подпиша .NET MAUI Windows приложение в CI?
За MSIX подпис ви трябва EV code signing сертификат в PFX формат, съхранен като base64 secret. Използвайте signtool sign /f cert.pfx /p $env:PFX_PASSWORD /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 MyApp.msix след dotnet publish -f net10.0-windows10.0.19041.0. За Microsoft Store допълнително ще ви трябва Microsoft.StoreServices API за автоматичен upload.
Защо build-ът гърми с „invalid provisioning profile" само в CI?
Най-честата причина е, че provisioning profile-ът е изтекъл (валиден е 1 година) или е бил преиздаден в Apple Developer портала след последното качване в GitHub Secrets. Използвайте apple-actions/download-provisioning-profiles@v3, което тегли актуалния profile при всеки билд, вместо да съхранявате статично копие в secrets. Това елиминира почти всички expiry проблеми.
Как да пусна тестове преди release билда, без да губя macOS минути?
Разделете workflow-а на два job-а: test на ubuntu-latest с dotnet test само за net10.0 (без mobile target-и) и build-ios, който има needs: test. Unit тестовете за MVVM, услуги и validation logic не изискват mobile runtime и се въртят за около 90 секунди на Linux. macOS билдът стартира само ако тестовете минат.