With .NET MAUI (Multi-platform App UI), developers can build native mobile and desktop apps using a single codebase for Android, iOS, macOS, and Windows. While the framework simplifies app development, building and publishing apps to the App Store and Play Store can still be a time-consuming manual process.
This is where continuous integration and delivery (CI/CD) pipelines come into play. CI/CD pipelines automate building, testing, and publishing your apps, allowing you to focus on writing code rather than managing builds. GitHub Actions, GitHub’s own automation tool, is a powerful solution to automate these tasks.
In this comprehensive guide, I’ll walk you through setting up a CI/CD pipeline using GitHub Actions to automatically build your .NET MAUI app and publish it to both the Apple App Store and Google Play Store.
What is GitHub Actions?
GitHub Actions is a CI/CD tool that integrates directly with your GitHub repositories. It allows you to automate tasks like building, testing, and deploying applications based on events such as commits, pull requests, or scheduled workflows.
Prerequisites
Before getting started, make sure you have the following:
- GitHub account: Your project should be hosted in a GitHub repository
- Apple Developer account: You'll need this to publish to the Apple App Store
- Google Play Developer account: Necessary for publishing to the Play Store
- Certificates: Ensure Xcode (for iOS), and any necessary signing certificates (e.g., Android keystore, iOS provisioning profiles) are accessible
- App Setup: You have to set up your apps in the App Store and Play Store already
Complete iOS App Store Deployment Setup
Step 1: Configure macOS Build Environment
The iOS deployment workflow requires a macOS runner since Xcode is only available on macOS. This step sets up all the necessary tools including Xcode, .NET SDK, and the MAUI workload for cross-platform development.
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Xcode version
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Install MAUI workload
run: dotnet workload install maui
Key Configuration Points:
- macos-latest: The workflow runs on a macOS machine, which is required for iOS builds
- Checkout repository: It checks out your code from the repository so that the following steps can access it
- Setup Xcode: This step installs Xcode (version 16.0), which is essential for building iOS apps
- Install .NET SDK: This step installs the .NET 8.0 SDK
- Install MAUI workload: This ensures the MAUI workload is available for building the project
Step 2: Import iOS Certificates and Provisioning Profiles
iOS apps require proper code-signing certificates and provisioning profiles for deployment. This section handles the secure import of your Apple Developer certificates and automatically downloads the correct provisioning profiles for your app.
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-filepath: 'AppleSigningCertificate.p12'
p12-password: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
- name: Download Apple Provisioning Profiles
uses: Apple-Actions/download-provisioning-profiles@v1
with:
bundle-id: 'com.yourcompany.yourapp'
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
Certificate Management Process:
iOS apps require code-signing to be deployed, and this section handles importing the necessary certificates and provisioning profiles:
Import Code-Signing Certificates: It uses a .p12
certificate for code-signing, which is required by Apple. The p12-password
is securely fetched from GitHub secrets.
- Learn how to get .p12 files from your Apple Keychain: iOS Creating a Distribution Certificate and p12 File
- Pack your .p12 file in your project folder and set a GitHub Action Secret with the password you chose
Download Apple Provisioning Profiles: It fetches the required provisioning profile based on your app’s bundle identifier, using credentials stored in GitHub secrets.
- The Bundle-Id is your app identifier
- Generate an API Key following Apple’s documentation: Creating API Keys for App Store Connect API
- Choose Team Key and download it. Copy the ID and put it in GitHub Actions Secret (
APPSTORE_KEY_ID
) - Copy the content of your key and put it in a Secret (
APPSTORE_PRIVATE_KEY
) - Get your Issuer ID for the Secret (
APPSTORE_ISSUER_ID
): FastLane Discussion
Step 3: Build iOS App and Upload to TestFlight
This final step compiles your MAUI project for iOS and automatically uploads the resulting IPA file to TestFlight for testing and eventual App Store submission.
- name: Build iOS App
run: |
dotnet publish YourApp/YourApp.csproj \
-f net8.0-ios \
-c Release \
-p:ArchiveOnBuild=true \
-p:EnableAssemblyILStripping=false
- name: Upload app to TestFlight
uses: Apple-Actions/upload-testflight-build@v1
with:
app-path: 'YourApp/bin/Release/net8.0-ios/publish/YourApp.ipa'
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
Build and Deployment Process:
This section builds your MAUI project for iOS:
- Build: The
dotnet publish
command compiles the app for the iOS platform using the .NET 8.0 framework in Release configuration. The additional properties ensure the app builds successfully - Upload to TestFlight: This step uploads the
.ipa
file (iOS app archive) to TestFlight using Apple’s API credentials securely stored in GitHub secrets
- Build: The
Complete iOS Workflow Configuration
Here's the complete iOS workflow that combines all the steps above into a single, ready-to-use GitHub Actions job:
build-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Xcode version
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Install MAUI workload
run: dotnet workload install maui
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-filepath: 'AppleSigningCertificate.p12'
p12-password: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
- name: Download Apple Provisioning Profiles
uses: Apple-Actions/download-provisioning-profiles@v1
with:
bundle-id: 'com.yourcompany.yourapp'
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
- name: Build iOS App
run: |
dotnet publish YourApp/YourApp.csproj \
-f net8.0-ios \
-c Release \
-p:ArchiveOnBuild=true \
-p:EnableAssemblyILStripping=false
- name: Upload app to TestFlight
uses: Apple-Actions/upload-testflight-build@v1
with:
app-path: 'YourApp/bin/Release/net8.0-ios/publish/YourApp.ipa'
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
Complete Android Play Store Deployment Setup
Step 1: Configure Windows Build Environment
Android MAUI apps can be built on Windows runners, which are often faster and more cost-effective than macOS runners. This step prepares the Windows environment with all necessary tools for Android development.
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Install MAUI workload
run: dotnet workload install maui
Environment Configuration:
The Android workflow sets up the environment required for building your MAUI Android app:
- windows-latest: The workflow runs on a Windows machine, as Windows is commonly used for Android development
- Checkout repository: It checks out your code from the GitHub repository so the subsequent steps can access it
- Setup .NET: This step installs the .NET 8.0 SDK
- Install MAUI workload: Installs the MAUI workload necessary for Android app development
Step 2: Build and Sign Your Android App
This step compiles your MAUI project for Android and signs it with your keystore for Play Store deployment. Proper signing is essential for app store acceptance and user security.
- name: Build and Sign Android App
run: |
dotnet publish YourApp/YourApp.csproj \
-f net8.0-android \
-c Release \
-p:AndroidKeyStore=true \
-p:AndroidSigningKeyStore=your-app.keystore \
-p:AndroidSigningKeyAlias=your-alias \
-p:AndroidSigningKeyPass="${{ secrets.KEYSTORE_PASSWORD }}" \
-p:AndroidSigningStorePass="${{ secrets.KEYSTORE_PASSWORD_ALIAS }}"
Android Build and Signing Configuration:
Build command: The dotnet publish
command builds the MAUI Android project for the net8.0-android
framework in Release mode.
Signing parameters: The command uses key properties to enable Android signing:
AndroidKeyStore=true
: Enables the use of the Android keystore for signing- Keystore path and alias: The app is signed with the specified keystore and alias
- Passwords: The keystore and alias passwords are securely fetched from GitHub secrets
⚠️ Security Note: Storing the keystore file directly in your repository is not the most secure approach. For better security practices, consider encoding the keystore as base64 and storing it in GitHub Secrets, then decoding it during the build process.
Learn more about Android keystore management: YouTube Tutorial
Step 3: Upload to Google Play Console
The final step automatically uploads your signed Android App Bundle to the Google Play Console, making it available for internal testing or release preparation.
- name: Upload to Google Play Console
uses: r0adkll/upload-google-play@v1.1.3
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_SERVICE_ACCOUNT }}
packageName: com.yourcompany.yourapp
releaseFiles: YourApp/bin/Release/net8.0-android/publish/com.yourcompany.yourapp-Signed.aab
track: internal
Google Play Upload Configuration:
This part automates the upload of the signed Android app to the Google Play Console:
- Google Play upload action: Uses the
r0adkll/upload-google-play
action to upload the Android App Bundle (.aab
) to the Play Console - Service account: The Play Store service account credentials are securely passed as a GitHub secret
- Package name: Specifies the app’s unique package name, which identifies the app on the Play Store
- Release file: Points to the signed Android App Bundle that was built in the previous step
- Track: The app is uploaded to the internal track for internal testing or deployment
- Google Play upload action: Uses the
Setting up Google Play Service Account: Follow the official documentation to configure access via service account.
Complete Android Workflow Configuration
Here's the complete Android workflow that combines all steps into a production-ready GitHub Actions job:
build-android:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Install MAUI workload
run: dotnet workload install maui
- name: Build and Sign Android App
run: |
dotnet publish YourApp/YourApp.csproj \
-f net8.0-android \
-c Release \
-p:AndroidKeyStore=true \
-p:AndroidSigningKeyStore=your-app.keystore \
-p:AndroidSigningKeyAlias=your-alias \
-p:AndroidSigningKeyPass="${{ secrets.KEYSTORE_PASSWORD }}" \
-p:AndroidSigningStorePass="${{ secrets.KEYSTORE_PASSWORD_ALIAS }}"
- name: Upload to Google Play Console
uses: r0adkll/upload-google-play@v1.1.3
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_SERVICE_ACCOUNT }}
packageName: com.yourcompany.yourapp
releaseFiles: YourApp/bin/Release/net8.0-android/publish/com.yourcompany.yourapp-Signed.aab
track: internal
Production-Ready Complete Workflow
This comprehensive workflow file combines both iOS and Android deployment pipelines into a single, production-ready GitHub Actions configuration that you can use immediately in your MAUI project:
name: MAUI CI/CD Pipeline
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build-android:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Install MAUI workload
run: dotnet workload install maui
- name: Build and Sign Android App
run: |
dotnet publish YourApp/YourApp.csproj \
-f net8.0-android \
-c Release \
-p:AndroidKeyStore=true \
-p:AndroidSigningKeyStore=your-app.keystore \
-p:AndroidSigningKeyAlias=your-alias \
-p:AndroidSigningKeyPass="${{ secrets.KEYSTORE_PASSWORD }}" \
-p:AndroidSigningStorePass="${{ secrets.KEYSTORE_PASSWORD_ALIAS }}"
- name: List build output
run: ls -R ./**/*-Signed.aab
- name: Upload to Google Play Console
uses: r0adkll/upload-google-play@v1.1.3
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_SERVICE_ACCOUNT }}
packageName: com.yourcompany.yourapp
releaseFiles: YourApp/bin/Release/net8.0-android/publish/com.yourcompany.yourapp-Signed.aab
track: internal
build-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Xcode version
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
- name: Install MAUI workload
run: dotnet workload install maui
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-filepath: 'AppleSigningCertificate.p12'
p12-password: ${{ secrets.APPLE_SIGNING_CERTIFICATE_PASSWORD }}
- name: Download Apple Provisioning Profiles
uses: Apple-Actions/download-provisioning-profiles@v1
with:
bundle-id: 'com.yourcompany.yourapp'
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
- name: Build iOS App
run: |
dotnet publish YourApp/YourApp.csproj \
-f net8.0-ios \
-c Release \
-p:ArchiveOnBuild=true \
-p:EnableAssemblyILStripping=false
- name: List build output
run: ls -R YourApp/bin/Release/net8.0-ios/publish/
- name: Upload app to TestFlight
uses: Apple-Actions/upload-testflight-build@v1
with:
app-path: 'YourApp/bin/Release/net8.0-ios/publish/YourApp.ipa'
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
Required GitHub Secrets Checklist
Before running your GitHub Actions workflow, ensure you have configured all the required secrets in your repository settings:
iOS Deployment Secrets
- ✅
APPLE_SIGNING_CERTIFICATE_PASSWORD
- Password for your .p12 certificate file - ✅
APPSTORE_ISSUER_ID
- Your App Store Connect API issuer ID - ✅
APPSTORE_KEY_ID
- Your App Store Connect API key ID - ✅
APPSTORE_PRIVATE_KEY
- Content of your App Store Connect API private key (.p8 file)
Android Deployment Secrets
- ✅
KEYSTORE_PASSWORD
- Password for your Android keystore file - ✅
KEYSTORE_PASSWORD_ALIAS
- Password for your keystore alias - ✅
PLAYSTORE_SERVICE_ACCOUNT
- JSON content of your Google Play service account
To add secrets to your GitHub repository:
- Go to your repository → Settings → Secrets and Variables → Actions
- Click "New repository secret"
- Add the secret name and value
- Save the secret
Make sure all secrets are properly configured before triggering your first deployment!
Key Benefits of This Automated Approach
- Automated Deployments: No more manual builds and uploads
- Consistent Environment: Every build uses the same configuration
- Time Savings: Focus on development instead of deployment tasks
- Error Reduction: Automated processes reduce human error
- Version Control: All deployment configurations are version controlled
Common Challenges and Solutions
- Certificate Management: Ensure all certificates are valid and properly configured
- Secrets Configuration: Double-check that all GitHub Secrets are correctly set
- Path Issues: Verify that file paths in your workflow match your project structure
- Platform Differences: Remember that iOS requires macOS runners while Android can use Windows or Linux
Conclusion
Implementing CI/CD for .NET MAUI applications with GitHub Actions significantly improves your development workflow. This automated approach saves time, reduces errors, and ensures consistent deployments across both major mobile platforms.
The initial setup requires careful attention to certificate management and secret configuration, but once established, you'll have a robust deployment pipeline that scales with your development needs.
Start with this basic configuration and gradually enhance it with additional features like automated testing, conditional deployments, and notification systems as your project grows.