CI/CD für .NET MAUI mit GitHub Actions: iOS- und Android-Builds automatisieren (2026)

Kompletter Praxis-Leitfaden für .NET MAUI CI/CD mit GitHub Actions in 2026: Runner-Wahl, iOS-Signing per temporärer Keychain, Android-AAB und Upload zu TestFlight und Google Play.

.NET MAUI CI/CD mit GitHub Actions 2026

Aktualisiert: 11. Juli 2026

Eine CI/CD-Pipeline für .NET MAUI mit GitHub Actions automatisiert das Bauen, Signieren und Veröffentlichen deiner iOS- und Android-App aus einem einzigen Workflow – normalerweise mit einem macos-14-Runner für iOS, einem ubuntu-latest-Runner für Android und Secrets für Keystore, .p12-Zertifikat und App Store Connect API-Key. Die meisten Teams überspringen den Teil mit den Secrets und dem Provisioning Profile, und dann bricht der iOS-Build in Woche zwei mit „No signing certificate iOS Distribution found" ab. Dieser Leitfaden zeigt die komplette Pipeline für 2026, inklusive .NET 10, TestFlight- und Google-Play-Upload.

  • Für iOS-Builds ist ein macos-14- oder macos-15-Runner mit Xcode 16.4 Pflicht; Android läuft günstiger auf ubuntu-latest.
  • Ab .NET 10 heißt die Workload wieder dotnet workload install maui – die separaten maui-ios-/maui-android-Sub-Workloads aus .NET 8 sind zusammengeführt.
  • iOS-Code-Signing im CI funktioniert am zuverlässigsten mit einer temporären Keychain und einem App Store Connect API-Key (JWT-basiert, nicht mehr das Apple-ID-Passwort).
  • Android-Signing benötigt einen Base64-kodierten Upload-Keystore in den Secrets sowie einen Service Account mit „Release Manager"-Rolle für die Google Play Developer API.
  • Getrennte Jobs für iOS und Android in einem Workflow sparen teure macOS-Minuten; eine echte matrix-Strategie lohnt nur bei mehreren SDK-Versionen.
  • Visual Studio App Center wurde am 31. März 2025 abgeschaltet – GitHub Actions, Azure Pipelines oder Fastlane sind die praktischen Nachfolger.

Warum GitHub Actions für .NET MAUI in 2026?

Ich habe in den letzten drei Jahren Pipelines für vier .NET-MAUI-Produkte betreut, und die Landschaft hat sich 2025/2026 deutlich verschoben. Visual Studio App Center wurde am 31. März 2025 endgültig abgeschaltet, und Teams, die ihre Xamarin.Insights-Reste noch dort hatten, mussten migrieren. Für die meisten reicht GitHub Actions inzwischen völlig aus – vor allem, seit der macos-14-Runner mit Apple Silicon Standard ist und iOS-Builds nur noch etwa halb so lange dauern wie auf den alten Intel-Runnern.

Der entscheidende Punkt: GitHub Actions liegt neben deinem Code. Kein separater Login, keine getrennte Secret-Verwaltung, keine Diskussion mit dem IT-Team darüber, welchen Runner Azure DevOps nutzen darf. Wenn du bereits GitHub für Pull Requests nutzt, ist der Sprung zu einem CI-Workflow etwa 200 YAML-Zeilen weit weg. Für größere Teams, die kompliziertere Approval-Ketten oder Feature-Flag-Rollouts brauchen, ist Azure Pipelines oder Fastlane als lokaler Orchestrator noch immer die bessere Wahl. Für 90 % der MAUI-Teams unter zehn Entwicklern reicht GitHub Actions plus ein sauberer Keychain-Setup-Step aus.

Kosten sind der zweite Grund. Ein macos-14-Runner kostet zehnmal so viel wie ubuntu-latest. Wer Android-Builds versehentlich auf einem macOS-Runner laufen lässt, verbrennt Budget. Ein sauber getrennter Matrix-Build spart pro Push in main mehrere Minuten teure Zeit – das summiert sich schnell zu einem vierstelligen Betrag im Monat.

Voraussetzungen: Runner, Workloads und Secrets

Bevor der erste Workflow läuft, brauchst du drei Dinge sauber vorbereitet. Erstens: eine global.json im Repo-Root, die die .NET-Version festnagelt. Ohne sie zieht der Runner die neueste installierte SDK-Version, was zu Reproduzierbarkeitsproblemen führt, wenn Microsoft Preview-Bits vorinstalliert.

{
  "sdk": {
    "version": "10.0.100",
    "rollForward": "latestFeature"
  }
}

Zweitens: die MAUI-Workload. Ab .NET 10 gibt es die einheitliche maui-Workload wieder – die Aufsplittung in maui-ios, maui-android, maui-maccatalyst aus .NET 8 ist in .NET 10 zusammengeführt. Trotzdem installieren wir sie im CI explizit, weil hosted Runner die Workload nicht automatisch aktualisieren.

- name: Install MAUI workload
  run: dotnet workload install maui --source https://api.nuget.org/v3/index.json

Drittens: die Secrets. In den Repository Settings unter Secrets and variables → Actions legst du folgende Secrets an. Ich benenne sie konsequent mit dem Ziel-Präfix, weil ich mich schon mehrfach beim Copy-Paste zwischen Projekten vertan habe.

  • ANDROID_KEYSTORE_BASE64 – dein Upload-Keystore als Base64-String
  • ANDROID_KEYSTORE_PASSWORD – Keystore-Passwort
  • ANDROID_KEY_ALIAS – Alias des Signing-Keys
  • ANDROID_KEY_PASSWORD – Passwort für den Alias
  • GOOGLE_PLAY_SERVICE_ACCOUNT_JSON – Service-Account-JSON für die Google Play Developer API
  • IOS_P12_BASE64 – .p12-Zertifikat als Base64
  • IOS_P12_PASSWORD – Passwort für das .p12
  • IOS_PROVISIONING_PROFILE_BASE64 – .mobileprovision als Base64
  • APP_STORE_CONNECT_KEY_ID, APP_STORE_CONNECT_ISSUER_ID, APP_STORE_CONNECT_PRIVATE_KEY – für den App Store Connect API-Key (JWT-Auth)

Android-Pipeline: AAB signieren und zu Google Play hochladen

Für Android reicht ein ubuntu-latest-Runner, wenn du keine emulatorbasierten Tests brauchst. Der komplette Job sieht so aus – ich zeige ihn im Ganzen, weil das Zusammenspiel zwischen dotnet publish, Signing-Properties und dem Play-Upload sonst schwer zu debuggen ist.

jobs:
  android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET 10
        uses: actions/setup-dotnet@v4
        with:
          global-json-file: global.json

      - name: Install MAUI workload
        run: dotnet workload install maui

      - name: Decode keystore
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > $RUNNER_TEMP/upload.keystore
          echo "KEYSTORE_PATH=$RUNNER_TEMP/upload.keystore" >> $GITHUB_ENV

      - name: Restore
        run: dotnet restore src/MyApp/MyApp.csproj

      - name: Publish signed AAB
        run: |
          dotnet publish src/MyApp/MyApp.csproj \
            -c Release \
            -f net10.0-android \
            -p:AndroidPackageFormats=aab \
            -p:AndroidKeyStore=true \
            -p:AndroidSigningKeyStore=$KEYSTORE_PATH \
            -p:AndroidSigningStorePass='${{ secrets.ANDROID_KEYSTORE_PASSWORD }}' \
            -p:AndroidSigningKeyAlias='${{ secrets.ANDROID_KEY_ALIAS }}' \
            -p:AndroidSigningKeyPass='${{ secrets.ANDROID_KEY_PASSWORD }}'

      - name: Upload to Google Play (Internal Track)
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
          packageName: com.example.myapp
          releaseFiles: src/MyApp/bin/Release/net10.0-android/publish/*-Signed.aab
          track: internal
          status: completed

Die häufigste Falle: AndroidPackageFormats=aab vergisst man beim ersten Setup fast immer. Ohne diesen Property produziert MSBuild eine APK, und Google Play akzeptiert seit August 2021 nur noch Android App Bundles für neue Apps. Wenn du migrierst, willst du zudem AndroidSigningV2=true und AndroidSigningV3=true setzen – V4 macht MAUI standardmäßig, V1 solltest du für minSdk 24+ komplett deaktivieren.

Der Service-Account braucht in der Google Play Console die Rolle „Release Manager" plus API-Zugriff. Neu angelegte Service Accounts brauchen bis zu 24 Stunden, bis Google Play sie akzeptiert – ich habe schon Sonntagabende damit verbracht, einen 403-Fehler zu debuggen, der einfach nur eine „gib der Google-Infrastruktur noch drei Stunden"-Sache war.

iOS-Pipeline: Provisioning Profile und TestFlight

Der iOS-Job ist deutlich fummeliger, weil Apple die Signing-Kette streng validiert. Der Trick ist eine temporäre Keychain, die am Ende des Jobs automatisch gelöscht wird – so vermeidest du, dass Reste zwischen Läufen die Signaturen brechen.

jobs:
  ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET 10
        uses: actions/setup-dotnet@v4
        with:
          global-json-file: global.json

      - name: Select Xcode 16.4
        run: sudo xcode-select -s /Applications/Xcode_16.4.app

      - name: Install MAUI workload
        run: dotnet workload install maui

      - name: Import signing certificate
        env:
          P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
          P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
          echo "$P12_BASE64" | base64 -d > $RUNNER_TEMP/cert.p12
          security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security import $RUNNER_TEMP/cert.p12 \
            -P "$P12_PASSWORD" \
            -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH
          security set-key-partition-list \
            -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

      - name: Install provisioning profile
        run: |
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo "${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}" | base64 -d \
            > ~/Library/MobileDevice/Provisioning\ Profiles/build.mobileprovision

      - name: Publish signed IPA
        run: |
          dotnet publish src/MyApp/MyApp.csproj \
            -c Release \
            -f net10.0-ios \
            -p:ArchiveOnBuild=true \
            -p:RuntimeIdentifier=ios-arm64 \
            -p:CodesignKey="Apple Distribution: Meine Firma GmbH (ABCDE12345)" \
            -p:CodesignProvision="My App AppStore"

      - name: Upload to TestFlight
        env:
          APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
        run: |
          mkdir -p ~/private_keys
          echo "$APP_STORE_CONNECT_PRIVATE_KEY" > \
            ~/private_keys/AuthKey_${{ secrets.APP_STORE_CONNECT_KEY_ID }}.p8
          xcrun altool --upload-app \
            --type ios \
            --file src/MyApp/bin/Release/net10.0-ios/ios-arm64/publish/MyApp.ipa \
            --apiKey ${{ secrets.APP_STORE_CONNECT_KEY_ID }} \
            --apiIssuer ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}

Der CodesignKey-Wert muss exakt dem Common Name im Zertifikat entsprechen – inklusive der Team-ID in Klammern. Wenn dein Build mit „No signing certificate 'iOS Distribution' found" bricht, ist zu 90 % genau dieser String falsch. Öffne das .p12 lokal im Keychain Access und kopiere den Namen wortwörtlich. Für den CodesignProvision-Wert nimmst du den Namen aus dem Apple Developer Portal, nicht die UUID der .mobileprovision-Datei.

Der App Store Connect API-Key ersetzt seit 2021 die Apple-ID-plus-App-Passwort-Authentifizierung, die im Zuge der Zwei-Faktor-Pflicht praktisch unbenutzbar wurde. Den Key erzeugst du unter App Store Connect → Users and Access → Integrations → App Store Connect API. Die .p8-Datei bekommst du nur einmal – lade sie sofort herunter und lege sie in einen Passwort-Manager. Wer sich in Signing-Fallstricke und Zertifikatsketten einarbeitet, findet in unserem Leitfaden zum manuellen App-Store- und Google-Play-Release die schrittweise Anleitung als Referenz.

Wie richtet man einen Matrix-Build für iOS und Android ein?

Ein Matrix-Build fasst beide Zielplattformen in einem Job zusammen und ist wartungsärmer, aber teurer, weil jeder Schritt (auch die Android-relevanten) auf dem macOS-Runner läuft. Meine Empfehlung: getrennte Jobs in einem Workflow. So läuft Android auf Ubuntu, iOS auf macOS, und sie starten parallel.

name: Release Build

on:
  push:
    tags: [ 'v*' ]
  workflow_dispatch:

jobs:
  android:
    runs-on: ubuntu-latest
    # ... siehe Android-Pipeline oben

  ios:
    runs-on: macos-14
    # ... siehe iOS-Pipeline oben

  notify:
    runs-on: ubuntu-latest
    needs: [android, ios]
    if: always()
    steps:
      - name: Slack notification
        uses: slackapi/[email protected]
        with:
          payload: |
            {
              "text": "MAUI Release ${{ github.ref_name }}: Android ${{ needs.android.result }}, iOS ${{ needs.ios.result }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Falls du wirklich eine matrix-Strategie willst – etwa weil du für jede Plattform mehrere .NET-Versionen testen musst – nutzt du sie so:

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: net10.0-android
            os: ubuntu-latest
          - target: net10.0-ios
            os: macos-14
    runs-on: ${{ matrix.os }}
    steps:
      # gemeinsame Schritte

fail-fast: false ist wichtig, damit ein iOS-Signing-Fehler nicht den Android-Build abbricht – sonst weißt du am Ende nicht, ob nur iOS oder beide Plattformen kaputt sind.

Secrets sicher verwalten: Keystore, .p12 und OAuth-Tokens

Secrets in GitHub Actions sind AES-256-verschlüsselt und werden in Logs automatisch als *** maskiert – aber die Maskierung funktioniert nur, wenn der Wert wortwörtlich im Secret steht. Wenn du ein Zertifikat als Base64 speicherst und im Log versehentlich mit echo ausgibst, ist die Maskierung weg, sobald du den String zeilenweise splittest. Nie set -x in einem Schritt aktivieren, der Secrets berührt.

Für die Base64-Encodings unter macOS/Linux nutze ich immer:

base64 -i upload.keystore | pbcopy   # macOS
base64 -w 0 upload.keystore          # Linux, ohne Zeilenumbrüche

Der -w 0-Flag auf Linux ist essenziell – Standard-Base64 fügt alle 76 Zeichen einen Zeilenumbruch ein, und GitHub Secrets akzeptieren zwar Newlines, aber viele Downstream-Tools zerlegen den String daran. Wer OAuth-Tokens für den Google Play Developer Publishing API statisch speichert statt Service-Account-JWTs zu verwenden, sollte spätestens jetzt umstellen: Google hat statische Refresh-Tokens im Juni 2025 für neue Projekte deaktiviert.

Zur Absicherung von Deployments über die reinen Secrets hinaus lohnt sich ein Blick auf Environments in GitHub. Ein production-Environment mit „Required reviewers"-Regel verhindert, dass ein versehentlicher Push in main direkt in den App Store deployed. In den Environment-Secrets legst du die Production-Signaturen ab, während PR-Builds nur die Development-Signaturen sehen. Wer tiefer in sichere Signaturen und Zertifikatsketten einsteigen möchte, findet Kontext dazu in unserem Artikel zu .NET MAUI Security, OAuth 2.0 und Certificate Pinning.

Häufige CI-Fehler und wie man sie debuggt

Nach dutzenden Pipelines sind das die Fehler, die immer wieder auftauchen. Ich liste sie in der Reihenfolge, in der sie mich Zeit gekostet haben:

1. „workload maui-android not found"

Passiert, wenn du eine ältere global.json hast, die auf .NET 8 zeigt. In .NET 10 gibt es die Sub-Workloads nicht mehr – nur noch maui. Update die global.json, oder installiere im CI mit dotnet workload install maui-android, falls du bewusst auf .NET 8 bleibst.

2. „No signing certificate 'iOS Distribution' found"

Fast immer ist der CodesignKey-String falsch. Debug-Trick: nach dem Keychain-Import security find-identity -v -p codesigning $KEYCHAIN_PATH laufen lassen. Der Output zeigt exakt die Namen, die MSBuild sehen wird.

3. „APK signing key does not match"

Google Play akzeptiert für existierende Apps nur einen einzigen Upload-Key. Wenn du das Keystore verlegst und ein neues erzeugst, brauchst du den Play App Signing Key Reset über den Google-Support. Das dauert 48 Stunden. Prävention: den originalen Keystore mit dem Passwort-Manager sichern, plus einen verschlüsselten Cloud-Backup.

4. „HttpRequestException: Bad Gateway" beim NuGet Restore

NuGet.org hat gelegentliche 502er. Fix: dotnet restore --disable-parallel --retry 5 oder einen NuGet-Cache-Step (actions/cache) einbauen. Ich cache immer ~/.nuget/packages gegen den Hash der packages.lock.json.

5. Long-running iOS build hängt bei „Codesign"

Der Keychain ist gelockt. Zwischen zwei Schritten wird die Keychain-Session teils resettet – daher am Anfang jedes Schritts, der signiert, ein security unlock-keychain. Am elegantesten: alle Signing-Schritte in einen zusammengefassten Job packen, damit die Keychain nur einmal entsperrt werden muss.

Häufig gestellte Fragen

Kann ich .NET-MAUI-iOS-Builds ohne macOS-Runner in der CI erzeugen?

Nein, für die Erzeugung eines signierten IPA benötigst du zwingend macOS mit Xcode – Apple lizenziert die Toolchain nur für Mac-Hardware. Alternativen sind gemietete Mac-Runner (Bitrise, MacStadium) oder GitHub-hosted macos-14-Runner. Ohne Mac lässt sich nur der Android-Teil auf Linux bauen.

Wie lange dauert ein typischer .NET-MAUI-Release-Build auf GitHub Actions?

Ein Android-Release-Build mit AAB liegt auf ubuntu-latest bei etwa 6–9 Minuten, ein iOS-Build auf macos-14 bei 12–18 Minuten. NativeAOT-Builds für iOS können auf über 25 Minuten steigen. Mit einem NuGet- und Workload-Cache lassen sich beide um etwa 30 % beschleunigen.

Ist GitHub Actions oder Azure DevOps besser für .NET MAUI?

Für Teams unter zehn Entwicklern, die bereits GitHub nutzen, ist Actions einfacher und günstiger. Azure Pipelines bietet stärkere Approval-Workflows, bessere Enterprise-Governance und selbst-gehostete macOS-Pool-Verwaltung. Beide Systeme können MAUI bauen; der Unterschied liegt in Governance und Ökosystem, nicht in technischer Fähigkeit.

Was ersetzt Visual Studio App Center seit dem Sunset 2025?

Für Build und Distribution: GitHub Actions oder Azure Pipelines plus TestFlight und Google Play Internal Testing. Für Diagnostics: Sentry oder Firebase Crashlytics. Für Push Notifications: Azure Notification Hubs oder Firebase Cloud Messaging direkt. Ein Eins-zu-eins-Ersatz existiert bewusst nicht – das Ökosystem hat sich fragmentiert.

Muss ich Fastlane zusätzlich zu GitHub Actions einsetzen?

Nein, aber Fastlane vereinfacht komplexe iOS-Signing-Setups (match, gym, pilot) erheblich. Für einfache Pipelines reichen die nativen GitHub-Actions-Steps mit xcrun altool. Für Teams mit vielen Apps und Zertifikatsrotationen ist Fastlane die stabilere Wahl.

Sofia Rodriguez
Über den Autor Sofia Rodriguez

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