Titelbild zum Beitrag Von Commit zu AAB/APK: .NET MAUI Android Builds mit GitHub Actions automatisieren

Blogbeitrag

Von Commit zu AAB/APK: .NET MAUI Android Builds mit GitHub Actions automatisieren

In diesem Beitrag zeige ich Schritt für Schritt, wie ihr eine .NET MAUI Android App mit GitHub Actions automatisch bauen, signieren und als AAB- und APK-Datei bereitstellen könnt.

Eine Android-App lokal zu bauen, ist zunächst relativ einfach. Schwieriger wird es allerdings, wenn der Build-Prozess reproduzierbar sein und nicht von einem einzelnen Entwicklerrechner abhängen soll.

Schnell entsteht ansonsten eine Situation, in der eine Person lokal eine .aab-Datei erstellt, jemand anderes eine .apk exportiert und einige Wochen später niemand mehr genau weiß, welcher Keystore, welches Passwort oder welcher Befehl für das finale Release verwendet wurde.

Genau hier können uns GitHub Actions unterstützen.

In diesem Beitrag zeige ich Schritt für Schritt, wie wir eine .NET MAUI Android App innerhalb einer GitHub Action bauen und signieren können. Am Ende erhalten wir sowohl eine .aab- als auch eine .apk-Datei, die direkt über den jeweiligen Workflow-Run heruntergeladen werden können.

Unser Ziel ist also:

  • eine .NET MAUI Android App über GitHub Actions bauen
  • die Anwendung mit einem Android Keystore signieren
  • sowohl eine .aab- als auch eine .apk-Datei erzeugen
  • die erzeugten Dateien als Artifacts des Workflows bereitstellen

Für das Beispiel verwende ich .NET 10, .NET MAUI und einen Windows-basierten GitHub Actions Runner.

Warum sollten wir Android Builds mit GitHub Actions automatisieren?

Ein automatisierter Build bietet uns einige Vorteile.

Jeder Build verwendet dieselben SDK- und Workload-Versionen, der Signing-Prozess läuft immer gleich ab und die erzeugten Dateien werden direkt zusammen mit dem jeweiligen Workflow-Run gespeichert.

Darüber hinaus können wir Versionsnummern automatisch aus unserem Pipeline-Run ableiten.

Vor allem reduzieren wir aber Unterschiede zwischen lokalen Entwicklungsumgebungen. Der Release-Build hängt damit nicht mehr davon ab, welche SDK-Version oder welche Konfiguration gerade auf einem Entwicklerrechner vorhanden ist.

Bei einer .NET MAUI Anwendung eignet sich Android außerdem sehr gut als Einstieg in die Build-Automatisierung. Im Vergleich zum Signing einer iOS-Anwendung ist der benötigte Workflow relativ überschaubar.

Voraussetzungen

Bevor wir mit unserer GitHub Action beginnen, benötigen wir zunächst einige Dinge:

  • eine .NET MAUI App mit net10.0-android
  • ein GitHub Repository mit unserer Anwendung
  • eine über global.json festgelegte .NET SDK-Version
  • einen Android Keystore
  • GitHub Secrets für den Keystore und die benötigten Passwörter

In meinem Beispiel-Repository befindet sich das .NET MAUI Projekt unter folgendem Pfad:

src/MyAwesomeMauiApp/MyAwesomeMauiApp.csproj

Schritt 1: .NET 10 für Android verwenden

Zunächst sollte unser .NET MAUI Projekt Android explizit mit .NET 10 als Target Framework verwenden.

In der .csproj-Datei finden wir beispielsweise:

<TargetFrameworks>net10.0-android</TargetFrameworks>

Sollte eure Anwendung noch net9.0-android verwenden, müsst ihr entweder zunächst auf .NET 10 aktualisieren oder den späteren Workflow entsprechend anpassen.

Wichtig ist vor allem, dass das Target Framework der Anwendung und das Framework innerhalb des Workflows übereinstimmen.

Schritt 2: SDK-Version mit global.json festlegen

Unsere CI-Pipeline sollte nicht davon abhängig sein, welche .NET SDK-Version gerade zufällig auf dem GitHub Runner installiert oder als Standard konfiguriert ist.

Daher erstellen wir im Root-Verzeichnis unseres Repositories eine global.json.

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

Damit erreichen wir zwei Dinge:

  • lokale Builds und CI-Builds verwenden dieselbe SDK-Version
  • ein SDK-Update erfolgt bewusst und nicht automatisch durch ein aktualisiertes Runner-Image

GitHub Actions kann diese Datei später direkt über actions/setup-dotnet verwenden.

Schritt 3: Android Keystore erstellen

Damit wir eine signierte Android-Anwendung erzeugen können, benötigen wir einen Keystore.

Falls ihr bereits einen Keystore für eure Anwendung besitzt, solltet ihr natürlich diesen verwenden.

Für eine neue Anwendung können wir über keytool einen neuen Keystore erzeugen.

keytool -genkeypair \
  -v \
  -storetype PKCS12 \
  -keystore myapp.keystore \
  -alias myapp \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

Die einzelnen Parameter haben dabei folgende Bedeutung:

  • -genkeypair erzeugt ein neues Schlüsselpaar
  • -v aktiviert eine ausführlichere Ausgabe
  • -storetype PKCS12 verwendet PKCS#12 als Keystore-Format
  • -keystore myapp.keystore legt den Namen unseres Keystores fest
  • -alias myapp definiert den Alias des Schlüssels
  • -keyalg RSA verwendet RSA
  • -keysize 2048 erzeugt einen Schlüssel mit 2048 Bit
  • -validity 10000 setzt die Gültigkeit des Zertifikats auf 10.000 Tage

Die 10.000 Tage entsprechen etwas mehr als 27 Jahren.

Das ist bei Android durchaus sinnvoll, da das Signing-Zertifikat langfristig gültig bleiben sollte. Schließlich benötigen wir dasselbe Zertifikat auch für zukünftige Updates unserer Anwendung.

Falls ihr den Keystore beispielsweise innerhalb eines Skripts ohne Benutzereingabe erzeugen möchtet, könnt ihr auch alle notwendigen Informationen direkt angeben:

keytool -genkeypair \
  -v \
  -storetype PKCS12 \
  -keystore myapp.keystore \
  -alias myapp \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -storepass "YOUR_KEYSTORE_PASSWORD" \
  -keypass "YOUR_KEY_PASSWORD" \
  -dname "CN=Name, OU=Mobile, O=Company, L=City, S=State, C=US"

Schritt 4: Keystore nach Base64 konvertieren

GitHub Secrets speichern Text und keine Binärdateien.

Daher können wir unsere .keystore-Datei nicht einfach direkt als Secret hinterlegen. Stattdessen konvertieren wir die Datei zunächst in einen Base64-String.

Unter PowerShell können wir dafür folgende Befehle verwenden:

$base64 =
  [Convert]::ToBase64String([IO.File]::ReadAllBytes(".\myapp.keystore"))

$base64 | Set-Content -Path .\myapp.keystore.base64.txt -NoNewline

Get-Content .\myapp.keystore.base64.txt

Unter Linux bzw. Bash funktioniert es entsprechend mit:

base64 -w 0 myapp.keystore > myapp.keystore.base64.txt
cat myapp.keystore.base64.txt

Damit erhalten wir eine Textrepräsentation unseres Keystores, die wir anschließend als GitHub Secret hinterlegen können.

Schritt 5: GitHub Secrets anlegen

Nun öffnen wir unser GitHub Repository und wechseln unter Settings in den Bereich für Secrets and variables.

Für unseren Android Build benötigen wir vier Secrets:

  • ANDROID_KEYSTORE
  • ANDROID_KEY_ALIAS
  • ANDROID_KEY_PASSWORD
  • ANDROID_KEYSTORE_PASSWORD

ANDROID_KEYSTORE enthält dabei den vollständigen Base64-String, den wir im vorherigen Schritt erzeugt haben.

ANDROID_KEY_ALIAS entspricht dem Alias innerhalb unseres Keystores, in unserem Beispiel also myapp.

Die beiden anderen Secrets enthalten das Passwort für den Key beziehungsweise den gesamten Keystore.

Damit haben wir alle Informationen vorbereitet, die unsere GitHub Action später zum Signieren der Anwendung benötigt.

Schritt 6: GitHub Action erstellen

Jetzt können wir unsere eigentliche GitHub Action erstellen.

Dafür legen wir folgende Datei an:

.github/workflows/android-build.yml

Der vollständige Workflow sieht folgendermaßen aus:

name: Build .NET MAUI Android App (.aab, .apk)

on:
  workflow_dispatch:

jobs:
  build-android:
    runs-on: windows-latest

    env:
      PROJECT_PATH: src/MyAwesomeMauiApp/MyAwesomeMauiApp.csproj

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup .NET from global.json
        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 }}" | Out-File -Encoding ascii -FilePath myapp.keystore.b64
          certutil -decode myapp.keystore.b64 myapp.keystore

      - name: Set version variables
        run: |
          $buildNumber = "${{ github.run_number }}"
          echo "VERSION_DISPLAY=1.0.$buildNumber" >> $env:GITHUB_ENV
          echo "VERSION_CODE=$((10000 + $buildNumber))" >> $env:GITHUB_ENV

      - name: Restore NuGet packages
        run: dotnet restore "$env:PROJECT_PATH"

      - name: Build (Release)
        run: |
          dotnet publish "$env:PROJECT_PATH" -f net10.0-android -c Release `
            -p:ApplicationVersion="$env:VERSION_CODE" `
            -p:ApplicationDisplayVersion="$env:VERSION_DISPLAY" `
            -p:AndroidKeyStore=true `
            -p:AndroidSigningKeyAlias="${{ secrets.ANDROID_KEY_ALIAS }}" `
            -p:AndroidSigningKeyPass="${{ secrets.ANDROID_KEY_PASSWORD }}" `
            -p:AndroidSigningStorePass="${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" `
            -p:AndroidSigningStore=myapp.keystore `
            -p:AndroidPackageFormats="aab;apk"

      - name: Copy signed .aab and .apk to output
        run: |
          $destination = "$(pwd)\output"
          New-Item -ItemType Directory -Path $destination -Force | Out-Null

          $projectDir = Split-Path "$env:PROJECT_PATH" -Parent
          $publishDir = Join-Path $projectDir "bin\Release\net10.0-android\publish"

          $files = Get-ChildItem -Path $publishDir -Recurse -Include *.aab,*.apk -File

          foreach ($file in $files) {
            Copy-Item $file.FullName -Destination $destination -Force
          }

      - name: Upload final AAB and APK only
        uses: actions/upload-artifact@v4
        with:
          name: android-build
          path: output

Schritt 7: Was macht unser Workflow?

Schauen wir uns noch einmal kurz an, was innerhalb unserer GitHub Action passiert.

  1. Repository auschecken
  2. .NET SDK installieren
  3. .NET MAUI Workload installieren
  4. Keystore wiederherstellen
  5. Versionsnummer erzeugen
  6. Anwendung bauen und signieren
  7. Artifacts sammeln und veröffentlichen

Schritt 8: Workflow ausführen

Aktuell verwenden wir in unserem Workflow ausschließlich:

on:
  workflow_dispatch:

Dadurch wird der Build nicht automatisch bei jedem Commit gestartet.

Nachdem wir die Workflow-Datei in unser Repository gepusht haben, können wir in GitHub in den Bereich Actions wechseln und den Workflow dort manuell starten.

Das eignet sich sehr gut, um die Pipeline zunächst zu testen.

Sobald alles funktioniert, können wir den Trigger natürlich erweitern und den Workflow beispielsweise bei einem Push, einem Tag oder bei der Erstellung eines Releases ausführen.

Nach einem erfolgreichen Run sollten wir ein Artifact mit dem Namen android-build sehen.

Dieses enthält unsere finalen, signierten .aab- und .apk-Dateien.

Typische Probleme

Auch wenn unser Workflow insgesamt recht einfach aufgebaut ist, gibt es einige typische Fehlerquellen.

Falscher Projektpfad

Der Wert von PROJECT_PATH muss auf die richtige .csproj-Datei zeigen.

env:
  PROJECT_PATH: src/MyAwesomeMauiApp/MyAwesomeMauiApp.csproj

Stimmt dieser Pfad nicht, werden bereits dotnet restore oder spätestens dotnet publish fehlschlagen.

Falsche Secrets

Keystore-Passwort, Key-Passwort und Alias müssen exakt zu unserem Keystore passen.

Stimmt einer dieser Werte nicht, schlägt das Signing während dotnet publish fehl.

Fehlender MAUI Workload

Auch das MAUI Workload darf innerhalb eines frischen GitHub Runners nicht vergessen werden.

dotnet workload install maui

Andernfalls kann unsere .NET MAUI App nicht korrekt veröffentlicht werden.

Unterschiedliche SDK-Versionen

Ohne eine global.json kann sich das Verhalten unseres Builds ändern, sobald GitHub das verwendete Runner-Image aktualisiert.

Daher empfehle ich, die verwendete .NET SDK-Version immer explizit festzulegen.

Es wird nur eine .aab erzeugt

In manchen Konfigurationen erzeugt dotnet publish im Release-Modus standardmäßig nur ein Android App Bundle.

Wenn ihr sowohl .aab als auch .apk im selben Build benötigt, setzt beim Publish zusätzlich:

-p:AndroidPackageFormats="aab;apk"

AAB oder APK?

Warum erzeugen wir eigentlich beide Formate?

Beide Dateien haben unterschiedliche Anwendungsfälle.

AAB

Das Android App Bundle (.aab) ist das bevorzugte Format für die Veröffentlichung einer Anwendung über den Google Play Store.

Google verwendet das Bundle anschließend, um optimierte APKs für die verschiedenen Geräte der Benutzer zu erzeugen.

APK

Eine Android Package-Datei (.apk) können wir dagegen direkt auf einem Android-Gerät installieren.

Sie eignet sich daher beispielsweise sehr gut für:

  • interne Tests
  • direkte Installation auf Testgeräten
  • schnelle Überprüfung eines Builds
  • Verteilung außerhalb des Google Play Stores

Wenn wir beide Formate innerhalb derselben Pipeline erzeugen, erhalten wir mit einem einzigen Build sowohl das benötigte Format für den Store als auch eine direkt installierbare Version für unsere Tests.

Fazit

Das automatisierte Bauen einer .NET MAUI Android App muss nicht kompliziert sein.

Sobald wir den Umgang mit dem Keystore, die GitHub Secrets, die verwendete .NET SDK-Version und unseren dotnet publish-Befehl einmal sauber definiert haben, erhalten wir einen reproduzierbaren Build-Prozess.

Und genau das ist für mich der eigentliche Vorteil einer solchen Pipeline.

Es geht nicht nur darum, einen manuellen Schritt zu automatisieren. Wir stellen vielmehr sicher, dass jedes Release unserer Android-Anwendung unter denselben Bedingungen und auf dieselbe Art erstellt wird.

Ein vollständiges Beispiel mit einer einfachen .NET MAUI Anwendung und den entsprechenden GitHub Actions findet ihr in meinem Repository

Weiterempfehlen

Diesen Beitrag teilen

Wenn dir der Beitrag gefallen hat: gern weiterreichen. Gute Links dürfen sich ruhig schnell verbreiten.

Vorheriger/Nächster Beitrag

Titelbild zum Beitrag Erstellen eines Seven Segment Display Controls mit .NET MAUI und SkiaSharp Nächster Beitrag Erstellen eines Seven Segment Display Controls mit .NET MAUI und SkiaSharp
Titelbild zum Beitrag Erstellen eines Seven Segment Display Controls mit .NET MAUI und SkiaSharp Erstellen eines Seven Segment Display Controls mit .NET MAUI und SkiaSharp Titelbild zum Beitrag Android Archive Erstellung schlägt fehl Android Archive Erstellung schlägt fehl Titelbild zum Beitrag Xamarin.Android App mit Azure DevOps bauen Xamarin.Android App mit Azure DevOps bauen Titelbild zum Beitrag PDF-Dateien unter Android mit Xamarin.Forms anzeigen PDF-Dateien unter Android mit Xamarin.Forms anzeigen
Lust auf ein kurzes digitales Hallo? Wenn du eine Idee teilen, eine Frage loswerden oder ein Projekt anschieben willst: mein Posteingang ist deutlich zuverlässiger als Brieftauben. [email protected] Mail schicken