Build Once Release Everywhere – APK


Continuous Delivery and Continuous Integration is key to any successful software, automating our build and release pipelines is a strategic matter. However, mobile apps are a special case, they are compiled applications, and there is no “Configuration” support out of the box like we have in web products web.config and appsettings.json.

The lack of configuration support on mobile apps often leads developers to add important information in static classes, doing this forces you to build your app every time you want to release/promote it to a different environment.

Build Once Release it Every Where

So what exactly do we want? We want our build pipeline to kick off as soon as new features are merged into our master branch, and create our APK/IPA, plus we want to be able to distribute that generated app to multiple environments, without having to rebuild it!

Like you can see on the GIF below, we have the same app, in the same version but with different label names, and all pointing to different environments!


So how exactly do we achieve the Nirvana of mobile releases, and what do we need to get there?

What do we need?

  • Mobile apps based on config files
  • Azure DevOps Build
  • Azure DevOps Release
  • apkTools / Fast Lane
  • App Center

Mobile apps based on config files

What exactly is this? Well, my good friend Andrew Hoefling has already debated about this in his blog here. However, I had to tweak it a little bit, because I need to load this file at the startup time of our application, so instead of packing the config file from the assembly, I grabbed it from the asset folder of our Android head project.

Azure DevOps Build

Azure DevOps will allow us to have a flexible Build and Release pipeline, and this will be our main CI/CD, let’s start by examining the .yml file that I’m using for this build pipeline. You can also check out the project in DevOps here.

The first part of the build definition is boilerplate, just setting the .yml variables and restoring NuGet packages.

On the two tasks below, we bump the app version to be the build number, and after that, we finally build the android APK and set the output directory for it.

– task: android-manifest-version@1
    sourcePath: ‘$(Build.SourcesDirectory)/CauserException/CauserException.Android/Properties/AndroidManifest.xml’
    versionCodeOption: ‘buildid’
    versionCode: ‘$(Build.BuildId)’
    printFile: true
– task: XamarinAndroid@1
    projectFile: ‘**/*CauserException.Android*.csproj’
    outputDirectory: ‘$(outputDirectory)’
    configuration: ‘$(buildConfiguration)’
The next step is to download the .keystore file that we will use to sign out APK, if you don’t know what that is, check this link out.
– task: DownloadSecureFile@1
    secureFile: ‘com.sample.causerexception.keystore’
– task: AndroidSigning@3
    apkFiles: ‘**/*.apk’
    apksign: true
    apksignerKeystoreFile: ‘com.sample.causerexception.keystore’
    apksignerKeystorePassword: ‘123456’
    apksignerKeystoreAlias: ‘com.sample.causerexception’
    apksignerArguments: –out $(outputDirectory)/com.sample.causerexception.apk
    apksignerKeyPassword: ‘123456’
    zipalign: true

And finally, we will copy the APK, the Keystore and our shell script (we will cover this next) and upload it all to the build artifact directory, where our release pipeline will pick it up and distribute it to App Center.

# Copy Keystore
– task: CopyFiles@2
  displayName: ‘Copy Keystore to: $(Build.ArtifactStagingDirectory)’
    SourceFolder: ‘$(Build.SourcesDirectory)/Scripts/KeyStore/’
    Contents: ‘com.sample.causerexception.keystore’
    TargetFolder: ‘$(build.artifactstagingdirectory)’
    OverWrite: true
#Copy Scripts
– task: CopyFiles@2
  displayName: ‘Copy Scripts to: $(Build.ArtifactStagingDirectory)’
    SourceFolder: ‘$(Build.SourcesDirectory)/Scripts/’
    Contents: ‘’
    TargetFolder: ‘$(build.artifactstagingdirectory)/Scripts’
    OverWrite: true
# Copy APK
– task: CopyFiles@2
  displayName: ‘Copy APK to: $(Build.ArtifactStagingDirectory)’
    SourceFolder: ‘$(outputDirectory)’
    Contents: ‘**/*.apk’
    TargetFolder: ‘$(build.artifactstagingdirectory)/Release/’
    OverWrite: true
– task: PublishBuildArtifacts@1
    PathtoPublish: ‘$(Build.ArtifactStagingDirectory)’
    ArtifactName: ‘drop’
    publishLocation: ‘Container’

Azure DevOps Release

This is where the magic happens. Now we are only covering Android APKs, next blog pots we will include iOS specifics.

We will use the Azure DevOps amazing flexible Release pipelines, as soon as the Build finishes, the release is triggered for the Development and Test Environments, then, someone has to approve the deployment to production, once the Testers approve the bug fixes or features of the app.

Screen Shot 2019-11-03 at 2.44.57 PM

Now, checking out a bit further on what the release tasks are, you can see that we only execute a shell script which you can find here, against the APK that the build generated, and we use the App Center Deploy task.

Screen Shot 2019-11-03 at 3.08.55 PM

APK Tools / Fast Lane

This is the crucial step of the release pipeline when the unzip the APK with apk tools and modify it to point to the correct environments. Overall, what does the script do?

  1. Use homebrew to install apktools and xmlStarlet
  2. Decompile the app using apktool.
  3. Leave the appsettings.json that points for the Environment we want.
  4. Use xmlStarlet to rename values on AndroidManifest.xml
  5. Use xmlStarlet to rename values on strings.xml
  6. Repackage the folder into an APK
  7. Sing the APK with the .keystore file
  8. Align the APK

You can check out the script I’m using right here

App Center

Last but not least, we will use App Center to get our app in the hands of our users and testers! You can check out how to set this up here.

And please feel free to download the latest builds of the sample app,


Being able to one app installer in the same device for multiple environments helps testers out a fair bit, they do not need to keep uninstalling and installing the app for every all the time. Also, having the app name match the Environment they point helps them have a visual idea of which app they are working.

In addition to these great things, you make sure that the same binaries and version of the app that was built and released to your testers is the one that gets approved to production and eventually promoted to the stores!


On the script where we modify the APK, there is a step where we change the value of a property on the manifest file called authorities. However, there can be more properties that need to be modified, and I’m still not entirely familiar with xmlstarlet to bulk edit all of the occurrences of the package name. I will adjust the script when in the future to fix this 🙂

And of course, I could not miss the opportunity to thank my good friends that helped me to build this puppy, @dylanberry, @ChaseFlorell and Scott MacDougall!

throw new CauserException();


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s