Sitemap
Mendix Community

The community-sourced publication for low-code

How to Build a CI/CD Pipeline for Mendix Native Apps Using Azure DevOps (Without Bitrise)

16 min readJul 13, 2026

--

Automating Android and iOS builds for Mendix Native apps using Azure DevOps pipelines

By Mohamed Kamel ElNady —Mendix Solution Architect | Mendix MVP

Press enter or click to view image in full size

Shipping Mendix Native apps to the App Store or Google Play is not as simple as deploying a web application.

Most official guides recommend Bitrise for CI/CD — but many enterprise and government teams already rely on Azure DevOps and cannot introduce another CI platform. In this article, I’ll show how to build a fully automated Azure DevOps pipeline that builds, signs, and ships Mendix Native apps, without Bitrise.

Why We Built This Pipeline

When we started shipping Mendix Native apps, the recommended CI/CD solution was Bitrise. It works well — but in our environment, all pipelines were already standardized on Azure DevOps due to internal DevOps governance and security requirements. Our source code couldn’t leave our infrastructure, and our compliance team had questions we couldn’t easily answer with a third-party cloud build service.

On top of that, the retirement of Microsoft App Center forced many Mendix teams to rethink their mobile build automation entirely. For teams already on Azure DevOps, the natural answer was to build the mobile pipeline directly inside it.

So instead of introducing another platform, we built a complete mobile CI/CD pipeline inside Azure DevOps — one that generates, signs, and publishes both Android and iOS builds on every release. What previously required manual steps and hours of developer time is now fully automated.

This article is the complete, production-tested guide to how we did it.

The Problem With Manual Mobile Builds

A typical Mendix Native release process involves:

  • Generating the native mobile bundle from Studio Pro
  • Preparing Android and iOS build environments
  • Managing signing certificates
  • Producing installable artifacts (.aab / .ipa)
  • Publishing builds to the App Store and Google Play

Without automation, developers end up running manual builds, managing certificates locally, and repeating the same steps for every release. This slows delivery and increases the risk of mistakes. CI/CD pipelines eliminate all of that.

Why Not Use Bitrise?

Bitrise is the default recommendation in most Mendix mobile deployment guides. It provides mobile-focused CI/CD and integrates with the Mendix Native build process. But it may not fit every organization:

  • Enterprise teams standardized on Azure DevOps
  • Security policies restricting third-party CI/CD platforms
  • Build credit limits hitting hard during active sprints
  • Desire to centralize all pipelines in one ecosystem
  • Compliance requirements preventing source code from leaving internal infrastructure

For teams already using Azure DevOps, building the mobile pipeline natively is the cleaner, more secure, and more cost-effective solution.

How Mendix Native Builds Actually Work

Before touching any pipeline code, you need to understand the architecture Mendix uses under the hood.

A Mendix native app is not a traditional compiled app — it is a React Native application. When you publish from Studio Pro, it generates a JavaScript bundle and assets, not a binary. The binary shell is a separate project called the Native Template.

# The Two-Layer Model

┌─────────────────────────────────────────────────────────────┐
│ The Two-Layer Model │
├─────────────────────────┬───────────────────────────────────┤
│ Mendix Studio Pro │ Native Template (GitHub) │
│ │ │
│ ● Your domain model │ ● React Native shell app │
│ ● Pages & microflows │ ● iOS Xcode project │
│ ● Business logic │ ● Android Gradle project │
│ ● JS Bundle output ────┼──► Bundled at build time │
└─────────────────────────┴───────────────────────────────────┘

The three ways to build:

Method How It Works Best For Mendix Native Mobile Builder (GUI) Wizard-based, triggers Bitrise Quick start, small teams Bitrise Cloud CI/CD Automated, Mendix-managed workflows Teams already on Bitrise Azure DevOps CI/CD (this article) You control everything Enterprise, compliance, cost control

Official docs for reference:

Azure DevOps Pipeline for Mendix Native Apps — Architecture Overview

The full pipeline architecture looks like this:

# Full Pipeline Architecture

┌──────────────────────────────────────────────────────────────────┐
│ Developer Workflow │
└──────────────────────┬───────────────────────────────────────────┘
│ 1. Publish Native App from Studio Pro
│ (generates JS bundle + assets)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Native Template Repository (Azure DevOps Git) │
│ │
│ /ios /android package.json app config │
└──────────┬──────────────────────────┬───────────────────────────┘
│ │
│ Trigger Pipeline │ Trigger Pipeline
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ iOS Pipeline │ │ Android Pipeline │
│ Agent: macOS Pool │ │ Agent: Windows Pool │
│ │ │ │
│ 1. Node.js 18 │ │ 1. Node.js 18 │
│ 2. CocoaPods │ │ 2. JDK 17.0.9 │
│ 3. Import Cert │ │ 3. Android SDK │
│ 4. Install Profile │ │ 4. NDK 26.x │
│ 5. Archive (xcbuild)│ │ 5. npm install │
│ 6. Export IPA │ │ 6. Gradle bundleRel.│
│ 7. App Store Upload │ │ 7. jarsigner (sign) │
│ (via Fastlane) │ │ 8. zipalign │
└─────────┬────────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ Apple App Store │ │ Google Play Store │
│ (Production) │ │ (Production) │
└─────────────────────┘ └──────────────────────┘

DevOps Team Contribution

This pipeline was implemented in close collaboration with the DevOps team, who played a key role in setting up and operating the CI/CD environment.

Agent Setup

  • Provisioning and maintaining self-hosted build agents
  • Using Windows agents for Android builds and macOS agents for iOS builds
  • Installing and managing required tooling (Node.js, JDK, Android SDK/NDK, Xcode, CocoaPods)
  • Ensuring consistent and reliable build environments

Secrets and Signing Management

  • Managing secure files such as Android keystores and Apple certificates (.p12)
  • Handling provisioning profiles and signing identities
  • Storing sensitive values using secure pipeline variables
  • Ensuring secure access and credential rotation

Publishing and Integration

  • Configuring service connections for Google Play and Apple App Store
  • Automating application publishing and release tracks
  • Managing release metadata and submission workflows
  • Ensuring compliance with store requirements

Pipeline Design and Operations

  • Designing CI/CD pipeline stages and structure
  • Monitoring pipeline executions and resolving failures
  • Optimizing build performance and reliability
  • Maintaining scalability and reusability

Who This Approach Is For

This solution is best suited for:

  • Enterprise environments requiring full control over CI/CD pipelines
  • Teams already using Azure DevOps
  • Organizations with strict security and compliance requirements
  • Projects requiring automated builds and publishing at scale

Prerequisites

For Both Platforms

  • Azure DevOps organization with pipelines enabled
  • Native Template repository pushed to your Azure DevOps Git repo
  • Node.js 18.x compatible with your React Native version
  • appStoreMetadata/ folder in your repo root with en-US/release_notes.txt

For Android

  • Windows-based self-hosted agent
  • Java Keystore file (.keystore) stored securely on the agent machine
  • Azure DevOps secret pipeline variables: KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD

For iOS

  • macOS self-hosted agent with Xcode installed
  • Apple Distribution Certificate (.p12) uploaded as a Secure File in Azure DevOps
  • Provisioning Profile (.mobileprovision) uploaded as a Secure File
  • Secret pipeline variable: P12_PASSWORD
  • Apple App Store Connect service connection configured in Azure DevOps
  • Fastlane pre-installed on the macOS agent (gem install fastlane)

Azure DevOps Service Connections

Go to Project Settings → Service Connections and create:

  1. Apple App Store connection — name it apple_store_connect
  2. Google Play connection — name it google_play

Step 1 — Preparing the Native Template Repository

When you build your Mendix app for native, Studio Pro generates the React Native source via the Native Builder. Push it to your own Azure DevOps Git repository:

# Setup — push Native Template to Azure DevOps

git clone <native-builder-output-path> mendix-native-app
cd mendix-native-app
git remote add origin https://dev.azure.com/YOUR_ORG/YOUR_PROJECT/_git/mendix-native-app
git push -u origin main

Every time you publish a new version from Studio Pro, push the updated source to this repo. The pipeline handles the rest.

The Android Pipeline — Step by Step

Create azure-pipelines-android.yml in your repository root.

# azure-pipelines-android.yml — job definition

jobs:
- job: MendixAndroidAAB
displayName: 'Mendix Android Build & Publish'
pool:
name: YOUR_WINDOWS_AGENT_POOL
steps:

Step 1 — Install Node.js 18

# azure-pipelines-android.yml — Step 1: Node.js

- task: NodeAndNpmTool@1
displayName: "Install Node.js 18"
inputs:
versionSpec: '18.x'
- task: UseNode@1
displayName: 'Use Node 18'
inputs:
version: '18.x'
- powershell: |
Write-Host "=== Node & npm Versions ==="
node -v
npm -v
displayName: "Verify Node Installation"

Why two tasks? In some self-hosted agent configurations, NodeAndNpmTool sets the version but doesn't activate it in the PATH for subsequent tasks. UseNode@1 ensures the correct version is active throughout the entire job.

Step 2 — Install JDK 17

We pin JDK 17.0.9 for reproducibility. The script checks if it’s already installed before downloading — this makes repeat builds significantly faster:

# azure-pipelines-android.yml — Step 2: JDK 17.0.9

- script: |
@echo off
setlocal EnableDelayedExpansion
set "jdkVersion=17.0.9"
set "jdkRoot=C:\build-tools\Java\jdk-%jdkVersion%"
set "downloadUrl=https://download.oracle.com/java/17/archive/jdk-%jdkVersion%_windows-x64_bin.zip"
if exist "%jdkRoot%" (
echo JDK %jdkVersion% already exists. Skipping download.
) else (
echo Downloading JDK...
mkdir "C:\build-tools\Java"
powershell -command "Invoke-WebRequest -Uri '%downloadUrl%' -OutFile 'C:\build-tools\Java\jdk.zip'; Expand-Archive -Path 'C:\build-tools\Java\jdk.zip' -DestinationPath 'C:\build-tools\Java' -Force"
)
setx JAVA_HOME "%jdkRoot%" /M
setx ORG_GRADLE_JAVA_HOME "%jdkRoot%" /M
"%jdkRoot%\bin\java.exe" -version
endlocal
displayName: "Install & Configure JDK 17.0.9"

⚠️ Important: ORG_GRADLE_JAVA_HOME is the variable Gradle actually reads in newer versions. Setting only JAVA_HOME is not always sufficient — set both.

Step 3 — Install Android SDK

# azure-pipelines-android.yml — Step 3: Android SDK

- script: |
@echo off
set "sdkRoot=C:\build-tools\AndroidSDK"
set "toolsUrl=https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip"
set "cmdlineTools=%sdkRoot%\cmdline-tools\latest\bin\sdkmanager.bat"
if exist "%cmdlineTools%" (
echo SDK already installed. Skipping.
) else (
mkdir "%sdkRoot%"
powershell -command "Invoke-WebRequest -Uri '%toolsUrl%' -OutFile '%sdkRoot%\tools.zip'"
powershell -command "Expand-Archive -Path '%sdkRoot%\tools.zip' -DestinationPath '%sdkRoot%\temp' -Force"
mkdir "%sdkRoot%\cmdline-tools\latest"
xcopy "%sdkRoot%\temp\cmdline-tools\*" "%sdkRoot%\cmdline-tools\latest\" /E /Y
rmdir /S /Q "%sdkRoot%\temp"
)
setx ANDROID_HOME "%sdkRoot%" /M
setx ANDROID_SDK_ROOT "%sdkRoot%" /M
echo === Installing SDK Components ===
call "%cmdlineTools%" --sdk_root=%sdkRoot% "platform-tools" "platforms;android-35" "build-tools;36.0.0"
echo y | call "%cmdlineTools%" --licenses
endlocal
displayName: "Install & Configure Android SDK"

Pinned component versions:

Get Mohamed ElNady’s stories in your inbox

Join Medium for free to get updates from this writer.

Component Version Platform android-35 Build Tools 36.0.0 NDK 26.1.10909125

Step 4 — Install NDK and Accept Licenses

# azure-pipelines-android.yml — Step 4: NDK + Licenses

- script: |
set sdkRoot=C:\build-tools\AndroidSDK
set cmdlineTools=%sdkRoot%\cmdline-tools\latest\bin\sdkmanager.bat
call "%cmdlineTools%" --sdk_root=%sdkRoot% "ndk;26.1.10909125"
echo y | call "%cmdlineTools%" --licenses
displayName: "Install NDK + Accept Licenses"
- powershell: |
$licenses = "C:\build-tools\AndroidSDK\licenses"
if (!(Test-Path $licenses)) { New-Item -ItemType Directory -Force -Path $licenses | Out-Null }
@(
"d56f5187479451eabf01fb78af6dfcb131a6481e",
"24333f8a63b6825ea9c5514f83c2829b004d1fee"
) | ForEach-Object { $_ | Out-File "$licenses\android-sdk-license" -Encoding ascii -Append }
"601085b94cd77f0b54ff86406957099ebe79c4d6" | Out-File "$licenses\android-ndk-license" -Encoding ascii
Write-Host "Licenses written successfully."
displayName: "Pre-accept Android SDK/NDK licenses"

💡 Why hardcode license hashes? In non-interactive CI environments, the y | pipe to sdkmanager --licenses sometimes fails silently. Writing the known license hashes directly to the license files is the most reliable approach.

Step 5 — Re-Export Environment Variables

Because setx writes to the system registry but not the current process environment, we must explicitly set variables for all remaining pipeline tasks:

# azure-pipelines-android.yml — Step 5: Export env vars for pipeline

- powershell: |
$jdkRoot = "C:\build-tools\Java\jdk-17.0.9"
$sdkRoot = "C:\build-tools\AndroidSDK"
Write-Host "##vso[task.setvariable variable=JAVA_HOME]$jdkRoot"
Write-Host "##vso[task.setvariable variable=ORG_GRADLE_JAVA_HOME]$jdkRoot"
Write-Host "##vso[task.setvariable variable=ANDROID_HOME]$sdkRoot"
Write-Host "##vso[task.setvariable variable=ANDROID_SDK_ROOT]$sdkRoot"
Write-Host "##vso[task.setvariable variable=PATH]$jdkRoot\bin;$sdkRoot\platform-tools;$env:PATH"
& "$jdkRoot\bin\java.exe" -version
displayName: "Export Environment Vars for Pipeline"

Step 6 — Install npm Dependencies

# azure-pipelines-android.yml — Step 6: npm install

- script: |
echo === Installing npm dependencies ===
npm install
displayName: "Install npm packages"

Step 7 — Build the Android App Bundle (.aab)

# azure-pipelines-android.yml — Step 7: Gradle build

- powershell: |
cd "$(Build.SourcesDirectory)\android"
.\gradlew.bat bundleRelease --stacktrace --info
displayName: "Build Android .aab"
workingDirectory: $(Build.SourcesDirectory)\android

This produces an unsigned .aab file at:

android/app/build/outputs/bundle/appstoreRelease/app-appstore-release.aab

Step 8 — Sign, Verify & Zipalign the AAB

# azure-pipelines-android.yml — Step 8: Sign + align AAB

- powershell: |
$jdkBin = "C:\build-tools\Java\jdk-17.0.9\bin"
$buildTools = "C:\build-tools\AndroidSDK\build-tools\35.0.0"
$aabPath = "$(Build.SourcesDirectory)\android\app\build\outputs\bundle\appstoreRelease\app-appstore-release.aab"
$signedAab = "$(Build.SourcesDirectory)\android\app\build\outputs\bundle\appstoreRelease\app-signed.aab"
$alignedAab = "$(Build.SourcesDirectory)\android\app\build\outputs\bundle\appstoreRelease\app-signed-aligned.aab"
$keystorePath = "C:\secure-keys\YourApp.keystore"
$alias = "$(KEY_ALIAS)"
$storePass = "$(KEYSTORE_PASSWORD)"
$keyPass = "$(KEY_PASSWORD)"
# Sign
& "$jdkBin\jarsigner.exe" -verbose `
-sigalg SHA256withRSA -digestalg SHA-256 `
-keystore $keystorePath -storepass $storePass -keypass $keyPass `
-signedjar $signedAab $aabPath $alias
# Verify
& "$jdkBin\jarsigner.exe" -verify -verbose -certs $signedAab
# Align
& "$buildTools\zipalign.exe" -v 4 $signedAab $alignedAab
Write-Host "Final signed AAB: $alignedAab"
displayName: "Sign, Verify & Align AAB"

🔐 Security note: $(KEY_ALIAS), $(KEYSTORE_PASSWORD), and $(KEY_PASSWORD) are Azure DevOps secret variables. Never hardcode credentials in YAML files.

Step 9 — Publish to Google Play

# azure-pipelines-android.yml — Step 9: Publish to Google Play

- task: PublishBuildArtifacts@1
displayName: "Publish Android Artifact"
inputs:
pathToPublish: '$(Build.SourcesDirectory)\android\app\build\outputs\bundle\appstoreRelease'
artifactName: 'android-release'
publishLocation: 'Container'
- task: ms-vsclient.google-play.google-play-release.GooglePlayRelease@4
displayName: 'Release to Google Play Production'
inputs:
serviceConnection: 'google_play'
applicationId: com.yourcompany.appname
bundleFile: '$(System.DefaultWorkingDirectory)\android\app\build\outputs\bundle\appstoreRelease\app-signed-aligned.aab'
track: production
changeLogFile: 'appStoreMetadata/en-US/release_notes.txt'
changesNotSentForReview: true
rescueChangesNotSentForReview: true

The iOS Pipeline — Step by Step

Create azure-pipelines-ios.yml in your repository root.

# azure-pipelines-ios.yml — job definition

jobs:
- job: Build_Mendix_iOS
displayName: "Build Mendix iOS IPA"
pool:
name: YOUR_MACOS_AGENT_POOL
demands: xcode

Step 1 — Node.js on macOS

# azure-pipelines-ios.yml — Step 1: Node.js + npm install

steps:
- checkout: self
- task: NodeTool@0
displayName: "Install Node.js"
inputs:
versionSpec: "18.x"
- script: |
npm install
displayName: "Install npm dependencies"

Step 2 — CocoaPods + Disable Pod Signing

This is the most critical and commonly broken step for iOS CI builds. All Pods must have code signing completely disabled — only your main app target should be signed:

# azure-pipelines-ios.yml — Step 2: CocoaPods + disable Pod signing

- script: |
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
cd $(Build.SourcesDirectory)/ios
pod install --repo-update
PODS_PROJECT="Pods/Pods.xcodeproj/project.pbxproj"
echo "Disabling signing for ALL Pods targets..."
sed -i '' 's/CODE_SIGN_STYLE = Automatic;/CODE_SIGN_STYLE = Manual;/g' "$PODS_PROJECT"
sed -i '' 's/CODE_SIGNING_ALLOWED = YES;/CODE_SIGNING_ALLOWED = NO;/g' "$PODS_PROJECT"
sed -i '' 's/CODE_SIGNING_REQUIRED = YES;/CODE_SIGNING_REQUIRED = NO;/g' "$PODS_PROJECT"
echo "Pod signing disabled successfully."
displayName: "CocoaPods install + disable Pods signing"

⚠️ Why this matters: If Pods try to use automatic signing, Xcode will fail looking for certificates across dozens of third-party frameworks. The sed command must run after pod install — CocoaPods regenerates the Pods project on every install.

Step 3 — Import Apple Distribution Certificate

# azure-pipelines-ios.yml — Step 3: Import .p12 certificate

- script: |
CERT_PATH="$(Agent.TempDirectory)/YourAppDistribution.p12"
security create-keychain -p "" build.keychain
security set-keychain-settings build.keychain
security unlock-keychain -p "" build.keychain
security import "$CERT_PATH" \
-k build.keychain \
-P "$(P12_PASSWORD)" \
-T /usr/bin/codesign \
-T /usr/bin/xcodebuild
security list-keychains -d user -s build.keychain login.keychain
security default-keychain -s build.keychain
echo "Available signing identities:"
security find-identity -v -p codesigning
displayName: "Import Apple Distribution Certificate"

How to get your .p12 file:

  1. Open Keychain Access on your Mac
  2. Export the Apple Distribution certificate (must include private key)
  3. Save as .p12 with a strong password
  4. Upload to Azure DevOps under Pipelines → Library → Secure Files

Step 4 — Install Provisioning Profile

# azure-pipelines-ios.yml — Step 4: Install provisioning profile

- task: InstallAppleProvisioningProfile@1
displayName: "Install Provisioning Profile"
inputs:
provisioningProfileLocation: 'secureFiles'
provProfileSecureFile: 'YourAppDistribution.mobileprovision'
- script: |
echo "Installed provisioning profiles:"
for f in "$HOME/Library/MobileDevice/Provisioning Profiles/"*.mobileprovision; do
echo "----"
security cms -D -i "$f" | grep -A1 "<key>Name</key>"
done
displayName: "Debug provisioning profiles"

Step 5 — Create exportOptions.plist

# azure-pipelines-ios.yml — Step 5: Create exportOptions.plist

- script: |
printf '%s\n' \
'<?xml version="1.0" encoding="UTF-8"?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
'<plist version="1.0">' \
'<dict>' \
' <key>method</key>' \
' <string>app-store</string>' \
' <key>teamID</key>' \
' <string>$(APPLE_TEAM_ID)</string>' \
' <key>signingStyle</key>' \
' <string>manual</string>' \
' <key>provisioningProfiles</key>' \
' <dict>' \
' <key>com.yourcompany.appname</key>' \
' <string>YourAppDistribution2026</string>' \
' </dict>' \
' <key>compileBitcode</key>' \
' <false/>' \
'</dict>' \
'</plist>' > $(Build.SourcesDirectory)/ios/exportOptions.plist
displayName: "Create exportOptions.plist"

Key Value Notes method app-store For App Store distribution teamID $(APPLE_TEAM_ID) Store as pipeline secret variable signingStyle manual Required for CI environments compileBitcode false Deprecated in newer Xcode

Step 6 — Archive the App

# azure-pipelines-ios.yml — Step 6: xcodebuild archive

- script: |
cd $(Build.SourcesDirectory)/ios
xcodebuild \
-workspace NativeTemplate.xcworkspace \
-scheme nativeTemplate \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath "$(Build.ArtifactStagingDirectory)/YourApp.xcarchive" \
archive \
DEVELOPMENT_TEAM="$(APPLE_TEAM_ID)" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="iPhone Distribution" \
PROVISIONING_PROFILE_SPECIFIER="YourAppDistribution2026" \
PRODUCT_BUNDLE_IDENTIFIER="com.yourcompany.appname"
displayName: "Archive iOS App"

⚠️ Most common failure point: CODE_SIGN_IDENTITY="iPhone Distribution" must match exactly what appears in security find-identity output from Step 3. A single character mismatch fails the entire archive.

Step 7 — Export IPA

# azure-pipelines-ios.yml — Step 7: Export IPA

- script: |
xcodebuild -exportArchive \
-archivePath "$(Build.ArtifactStagingDirectory)/YourApp.xcarchive" \
-exportPath "$(Build.ArtifactStagingDirectory)/ipa" \
-exportOptionsPlist $(Build.SourcesDirectory)/ios/exportOptions.plist
displayName: "Export IPA"

Step 8 — Publish to App Store via Fastlane

# azure-pipelines-ios.yml — Step 8: Publish to App Store

- script: |
echo "##vso[task.prependpath]/opt/homebrew/bin"
displayName: "Expose Fastlane to pipeline PATH"
- script: |
which fastlane && fastlane --version
displayName: "Verify Fastlane"
- task: ms-vsclient.app-store.app-store-release.AppStoreRelease@1
displayName: 'Publish to App Store Production'
inputs:
serviceEndpoint: 'apple_store_connect'
releaseTrack: Production
appIdentifier: com.yourcompany.appname
ipaPath: '$(Build.ArtifactStagingDirectory)/ipa/*.ipa'
uploadMetadata: true
metadataPath: 'appStoreMetadata'
shouldSubmitForReview: true
shouldAutoRelease: true
teamId: $(APPLE_TEAM_ID)
teamName: 'Your Organization Name'
installFastlane: false
fastlaneArguments: '--submission_information "{\"export_compliance_uses_encryption\": false}"'

💡 installFastlane: false — Pre-install Fastlane on your macOS agent. Reinstalling on every build adds ~3 minutes unnecessarily.

💡 export_compliance_uses_encryption: false — Removes the manual export compliance question from every App Store submission if your app only uses HTTPS.

Pipeline Trigger Configuration

# Pipeline trigger configuration

trigger:
branches:
include:
- main
paths:
exclude:
- '*.md'
- 'docs/**'
# For manual-only releases:
# trigger: none

We run on manual trigger or a dedicated release/* branch — not on every commit to main. Mobile releases need a deliberate action, not an accidental push.

Variable Groups — The Right Way to Handle Secrets

Create a Variable Group in Pipelines → Library → Variable Groups, name it mobile-signing-secrets:

Variable Name Type Description APPLE_TEAM_ID Secret Your 10-character Apple Team ID P12_PASSWORD Secret Password for .p12 certificate KEYSTORE_PASSWORD Secret Android keystore store password KEY_ALIAS Secret Android key alias KEY_PASSWORD Secret Android key password

Link it in each pipeline file:

# Link variable group in pipeline

variables:
- group: mobile-signing-secrets

Repository Structure

# Expected repository structure

mendix-native-app/
├── android/
│ ├── app/
│ │ ├── build.gradle
│ │ └── src/
│ ├── build.gradle
│ └── gradlew.bat
├── ios/
│ ├── NativeTemplate.xcworkspace/
│ ├── NativeTemplate/
│ └── Podfile
├── appStoreMetadata/
│ └── en-US/
│ └── release_notes.txt
├── package.json
├── azure-pipelines-android.yml
└── azure-pipelines-ios.yml

Lessons Learned and Common Pitfalls

Android

1. Gradle fails with “JAVA_HOME not set” setx persists to the registry but not the current session. Always follow with ##vso[task.setvariable] to expose it to the running pipeline process.

2. NDK not found during bundleRelease Your android/app/build.gradle often specifies an exact NDK version with ndkVersion. Make sure the version you install via sdkmanager matches exactly what is declared there.

3. License errors mid-build Pre-writing license hash files is more reliable than piping y to sdkmanager --licenses in a non-interactive environment.

4. AAB vs APK Google Play requires .aab for new apps. Use bundleRelease, not assembleRelease.

iOS

1. “No signing certificate found” Always print security find-identity -v -p codesigning after import. Verify the identity name matches exactly what you pass to CODE_SIGN_IDENTITY. This single check saves hours.

2. Pods signing error across 50+ targets The sed disable-signing command must run after pod install. Never before.

3. “Profile doesn’t match bundle identifier” Your exportOptions.plist bundle ID key must exactly match your app's bundle identifier. Even a trailing space causes a silent failure.

4. Fastlane not found The macOS CI PATH often excludes Homebrew. The ##vso[task.prependpath] step adding /opt/homebrew/bin is not optional.

5. Two-factor authentication issues Use an App Store Connect API key instead of Apple ID + password for your service connection. It is more stable and does not require 2FA handling.

General

6. Always validate the native build locally first Before connecting the pipeline, confirm the project builds successfully on a local machine. This isolates Mendix configuration issues before you’re debugging them inside a CI environment with a 20-minute feedback loop.

7. Use pipeline caching for dependencies Installing Node packages and React Native modules adds several minutes to every build. Caching the node_modules folder between runs using Azure DevOps cache tasks can noticeably reduce total pipeline time, especially on frequent releases.

Build Time Benchmarks

Results

After implementing this pipeline, our team achieved several measurable improvements:

  • ✅ Fully automated mobile builds — zero manual steps per release
  • ✅ Consistent build environments — pinned SDK, NDK, and JDK versions
  • ✅ Faster release cycles — what previously took hours of manual work now takes under 30 minutes end-to-end
  • ✅ Source code never leaves our infrastructure — full compliance
  • ✅ Centralized CI/CD — one platform for everything, not two

Developers push code. The pipeline takes care of the rest.

When This Approach Might Not Be Ideal

While this setup provides full control and flexibility, it may not be suitable for every team:

  • Small teams without dedicated DevOps support
  • Quick prototypes or short-term projects
  • Teams looking for minimal setup and faster onboarding

In such cases, simpler CI/CD solutions like Bitrise may offer a faster alternative.

Final Thoughts

Although Bitrise is widely used for mobile CI/CD in the Mendix, it is not the only option — and for many enterprise teams, it is not the right one.

For organizations already invested in Azure DevOps, building a dedicated pipeline for Mendix Native apps gives you tighter DevOps integration, improved security and governance, and a fully automated mobile release process that lives alongside every other pipeline in your organization.

The key insight is simple: a Mendix native app is a React Native app. Once you understand that, the CI/CD strategy becomes standard mobile DevOps. The Mendix-specific piece is only getting the correct Native Template source into your repository — everything after that is vanilla Azure DevOps.

The pipeline steps in this article are production-tested across real enterprise and government deployments in the Middle East. They handle the edge cases that aren’t in any official documentation: Pod signing conflicts, NDK license issues, Fastlane PATH problems, and Gradle environment variable scoping.

If this saved you build credits, hours of debugging, or an uncomfortable compliance conversation — hit the clap button and share it with your team. Got questions or a step that isn’t working for you? Drop it in the comments. Happy to help.

Pipeline Files — Ready to Use

To make this easier to adopt, I’ve shared the exact pipeline files we use in production.

Both complete pipeline files are available on GitHub. Download them, replace the placeholder values with your own configuration, and you’re ready to go.

azure-pipelines-android.yml — Full Android pipeline: Node.js, JDK 17, Android SDK, NDK, Gradle build, signing, zipalign, Google Play publish.

azure-pipelines-ios.yml — Full iOS pipeline: Node.js, CocoaPods, certificate import, provisioning profile, xcodebuild archive, IPA export, App Store publish via Fastlane.

Before using, replace these placeholders in each file:

Placeholder — — — — — — — ->Replace With

YOUR_WINDOWS_AGENT_POOLYour Windows agent pool name
YOUR_MACOS_AGENT_POOLYour macOS agent pool name
YOUR_APP_DISTRIBUTIONYour .p12 and .mobileprovision file names
YOUR_APP_ARCHIVEA name for your .xcarchive output
com.yourcompany.appnameYour app bundle / application ID
Your Organization NameYour Apple Developer team name
C:\secure-keys\YourApp.keystorePath to your keystore on the agent

Secret pipeline variables to configure:

Variable — — — — — —>Used In
APPLE_TEAM_IDiOS — Apple Developer Team ID
P12_PASSWORDiOS — .p12 certificate password
KEY_ALIASAndroid — keystore key alias
KEYSTORE_PASSWORDAndroid — keystore store password
KEY_PASSWORDAndroid — keystore key password

Acknowledgements

This pipeline was built in collaboration with our organization’s DevOps team, who handled the agent setup, service connections, secure file management, and infrastructure configuration. The Mendix side and the DevOps side both had to work together to make this happen — big thank you to the whole team.

Special thanks to Remon Louis for the help in reviewing and publishing this article. 🙌

Mohamed ElNady is a Mendix Solution Architect and Tech Lead, and the first Mendix MVP in Saudi Arabia. He has worked with multiple clients across the Middle East, leading enterprise application architecture, API integrations, and development implementations across various industries and government sectors.

Tags: Mendix Azure DevOps CI/CD Mobile Development React Native

--

--

Mohamed ElNady
Mohamed ElNady

Written by Mohamed ElNady

Technical Lead & Solution Architect specializing in Mendix. First Mendix MVP in Saudi Arabia. Building scalable enterprise and government low-code solutions.

Mendix Community
Mendix Community

Published in Mendix Community

The community-sourced publication for low-code