Using Azure DevOps to create a CI/CD pipeline for an Android application built with React Native
Published Jan 07 2020 05:13 AM 28.4K Views
Microsoft

In the previous posts I have written on this blog I have talked about many specific tasks related to React Native for Windows: how to build a native module, how to setup a CI/CD pipeline on Azure DevOps, etc. However, one of the advantages of React Native is the ability to target multiple platforms leveraging the same codebase and, at the same time, achieve the same quality of a native application. This is made possible by the different render targets, which translates the various JSX controls into native controls of the platform where the application is running. And thanks to native modules, we can also build plugins which, under the hood, use Android, iOS or Windows APIs to perform a task.

 

As such, after having successfully built a CI/CD pipeline on Azure DevOps for automatically build and deploy my React Native project for Windows, I decided to move to the next step and build such a pipeline also for the other platforms. The goal is to have a Windows, Android and iOS application automatically deployed every time I commit some code to the repository which hosts my React Native project.

 

Let's start to see how we can add Android support to our pipeline!

 

Welcome Gradle

If you have ever tried to deploy your React Native application on Android, you should already have met Gradle. It's a very popular build automation system, widely used for Java projects. You can think of it like MSBuild, but for platforms different than C# and .NET. Exactly like MSBuild, Gradle allows to setup, trough properties and configuration files, complex build systems, which require additional tasks other than just compiling the source code. This is the reason why React Native uses Gradle for the Android platform: it's able to perform all the tasks which are required to bundle and package together the JavaScript files, other than just compiling the Java code into an APK package. When you run your React Native project for Android using the following command:

 

react-native run-android

the React Native CLI will run Gradle to build the APK and then it will launch the Metro packager, so that you can have a live development and debugging experience. If you use the React Native CLI, instead, to generate a release package, Gradle will generate a bundle first and then it will create a standalone APK package. It isn't a very different experience from what we did in the previous posts for the Windows project. In that case, it was MSBuild to take care of everything for us and to generate a MSIX package with all the required files.

 

The Android build process is controlled by a file called build.gradle, which defines all the tasks that must be performed during the compilation. Gradle support a scripting language, so you can define variables, read properties, add conditions and logic, etc.

 

In case of a React Native project, Gradle leverages two different build.gradle files:

  • A build.gradle file inside the android folder, which is the top-level build file where you can add configuration options common to all sub-projects/modules.
  • A build.gradle file inside the android/app folder, which instead contains the various tasks and properties which are used to generate the APK.

Understanding the basics of Gradle is very important, since we'll need to make a few changes to the default build.gradle in order to make the project suitable for a CI/CD pipeline. Let' see them!

 

Create a signing certificate

Exactly like we did in our Windows application, also Android applications must be signed with a valid certificate in order to be installed. As such, the first step is to create a private key that will be used for this task. We can do it using a tool which comes with the Java SDK called keytool. If you have the JDK installed on your machine, you will find it in the C:\Program Files\Java\jdkx.x.x_x\bin folder, where x.x.x._x is the JDK version. If you have Visual Studio with the Xamarin tools installed, you can also open Visual Studio and choose Tools → Android → Android Adb Command Prompt. This option will open a command prompt with the JDK path included in the global paths, so you will be able to invoke the keytool command from any location.

 

Regardless of the option you choose, this is the command you'll need to type:

 

keytool -genkeypair -v -keystore my-upload-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

Two parameters can be customized here:

  • keystore defines the name of the file that will be generated. The previous command will generate a file called my-upload-key.keystore
  • alias defines the alias that will be assigned to the key. The previous command will generate an alias called my-key-alias. Feel free to replace it with the one you prefer, as long as you note it somewhere because you will need it to perform the signing.

After pressing Enter, you will be asked for the following information:

  • The password to use to protect the certificate store
  • First and last name
  • Organizational Unit
  • Organization
  • City or locality
  • State or province
  • Country code

These information will be used to generate the subject of the certificate. For example, in my case the generated subject was:

 

CN=Matteo Pagani, OU=AppConsult, O=Microsoft, L=Como, ST=CO, C=IT

Once you have confirmed, you will be asked for the password to protect the key, which can be a different one or the same you have previously used to protect the certificate store. At the end of the process, the tool will create a file in the same folder called my-upload-key.keystore with the private key. Make sure to store it in a safe place. We're going to need it later to sign the APK as part of our pipeline.

 

Setup the signing

By default, React Native signs the generated APK as part of the build process. We can see this task in the second build.gradle file, the one inside the android/app folder. Open it with a text editor of your choice and look for a section called android. You will find the following entry:

 

signingConfigs {
    debug {
        storeFile file('debug.keystore')
        storePassword 'android'
        keyAlias 'androiddebugkey'
        keyPassword 'android'
    }
}
buildTypes {
    debug {
        signingConfig signingConfigs.debug
    }
    release {
        // Caution! In production, you need to generate your own keystore file.
        // see https://facebook.github.io/react-native/docs/signed-apk-android.
        signingConfig signingConfigs.debug
        minifyEnabled enableProguardInReleaseBuilds
        proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
    }
}

The signingConfigs section includes a signing configuration called debug, which defines the certificate to use, the credentials, etc. In the buildTypes section, instead, we can find different settings that are applied based on the configuration we're using for the build: debug or release. You can notice that, by default, the same signing configuration we have just seen (the one called signingConfigs.debug) is used both for debug and release. If it's fine to keep the debug one, when you move to a CI/CD pipeline, instead, you will need to remove the signing operation for the release configuration.

 

The reason should be pretty clear: to leverage this approach we would need to upload to our repository the private key and to include, in clear, both the store password and the key password. If this approach is fine for the debug configuration since React Native uses a generic test certificate, it isn't a very safe approach when it comes to use our private certificate. As such, before starting to build our pipeline, let's remove the following line from the buildTypes.release section:

 

signingConfig signingConfigs.debug

This is how the final result should look like:

 

signingConfigs {
    debug {
        storeFile file('debug.keystore')
        storePassword 'android'
        keyAlias 'androiddebugkey'
        keyPassword 'android'
    }
}
buildTypes {
    debug {
        signingConfig signingConfigs.debug
    }
    release {
        minifyEnabled enableProguardInReleaseBuilds
        proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
    }
}

With this configuration, we're going to generate a release APK which is unsigned. We're going to sign it with a dedicated Azure DevOps task, which will allow us to keep our private key and the passwords safe.

 

Setting the build number

Exactly like we did for the Windows application, it's best to generate a new APK with a higher version number than the previous version. However, in our scenario, things are slightly more complicated than what we did for Windows. In that case, it was enough to use an Azure DevOps task to inject inside the manifest of the Windows application the correct version number. In our scenario, instead, it won't work, since the application version is set during the Gradle build. Even if we would add a task in the CI pipeline to set the version number in the manifest of the Android application, it would be overwritten by the one defined in the build.gradle file during the build process.

 

As such, the best option is to inject the version number directly in the build.gradle file. If you're worried that this means that you'll need to manually manipulate the Gradle file with a PowerShell script or something similar, you can relax! We have mentioned before that Gradle is very powerful and that the build files can contain properties. These properties can be retrieved from many sources: from the system (like an environment variable), from another file or... from a command line parameter. That's what we need! Azure DevOps is going to invoke the Gradle executable passing, as parameter, the version number. Inside the gradle.build file we'll include the logic to read the value from the parameter and to set it as version number in the manifest.

 

Let's start by opening the build.gradle file contained in the android folder. You will find a section that defines some basic information which are stored in the manifest, like the minimum and maximum supported SDK. This section is called ext and it's included in a section called buildscript:

 

ext {
    buildToolsVersion = "28.0.3"
    minSdkVersion = 16
    compileSdkVersion = 28
    targetSdkVersion = 28
    supportLibVersion = "28.0.0"
}

We're going to add two new properties, to store the version name and the version code, which are the two values used by Android to represent a version number. Version code is the real unique identifier and it's an integer, which must be incremented every time (for example, 56). Version name, instead, is a string and represents the version number you would like to display to your users (for example, 15.2.3). But first, inside the buildscript section, let's add two methods which are going to retrieve the value of the command line parameters:

 

def getMyVersionCode = { ->
    def code = project.hasProperty('versionCode') ? versionCode.toInteger() : -1
    println "VersionCode is set to $code"
    return code
}

def getMyVersionName = { ->
    def name = project.hasProperty('versionName') ? versionName : "1.0"
    println "VersionName is set to $name"
    return name
}

Both method behaves in the same way. With the property.hasProperty() function we can retrieve the value of a parameter passed to the Gradle task. The first one will look for a command line parameter called versionCode, while the second one for a parameter called versionName. If they aren't found, we're going to set a default value. Then we print the value (it will be helpful for logging purposes) and we return it to the caller.

 

Now we can add two properties in the ext section, which value will be retrieved using these two functions:

 

ext {
    buildToolsVersion = "28.0.3"
    minSdkVersion = 16
    compileSdkVersion = 28
    targetSdkVersion = 28
    supportLibVersion = "28.0.0"
    versionName = getMyVersionName()
    versionCode = getMyVersionCode()
}

This is how your entire buildscript section should look like:

 

buildscript {

    def getMyVersionCode = { ->
        def code = project.hasProperty('versionCode') ? versionCode.toInteger() : -1
        println "VersionCode is set to $code"
        return code
    }

    def getMyVersionName = { ->
        def name = project.hasProperty('versionName') ? versionName : "1.0"
        println "VersionName is set to $name"
        return name
    }

    ext {
        buildToolsVersion = "28.0.3"
        minSdkVersion = 16
        compileSdkVersion = 28
        targetSdkVersion = 28
        supportLibVersion = "28.0.0"
        versionName = getMyVersionName()
        versionCode = getMyVersionCode()
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:3.4.1")

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

Now we need to open the other build.gradle file, the one inside the android/app folder of your project. You will find a section called defaultConfig inside the android block:

 

defaultConfig {
    applicationId "com.navigationsample"
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
    versionCode 1
    versionName "1.0"
}

We will need to replace the fixed value assigned to the versionCode and versionName properties with the ones coming from the other build.gradle file. We can see, in the other properties, an example of how to achieve this goal. Since the other file is referenced by the current one, we can access to its properties simply by using the rootProject.ext.propertyName syntax. This is the final result:

 

defaultConfig {
    applicationId "com.navigationsample"
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
    versionCode rootProject.ext.versionCode
    versionName rootProject.ext.versionName
}

That's it. Now we just need to remember to pass to the Gradle executable the versionCode and the versionName parameters, so that they can be injected in the default configuration. We will do it in a moment, when we will start to build our pipeline.

 

Create the CI pipeline on Azure DevOps

We're going to use again my sample project as a starting point. First, go to the Pipelines section and create a new pipeline. You're going to connect it to the same repository you have used for the Windows application but, this time, choose Android as a starting template (you will need to click on Show more at the bottom to see it). The default YAML is pretty simple:

 

# Android
# Build your Android project with Gradle.
# Add steps that test, sign, and distribute the APK, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/android

trigger:
- master

pool:
  vmImage: 'macos-latest'

steps:
- task: Gradle@2
  inputs:
    workingDirectory: ''
    gradleWrapperFile: 'gradlew'
    gradleOptions: '-Xmx3072m'
    publishJUnitResults: false
    testResultsFiles: '**/TEST-*.xml'
    tasks: 'assembleDebug'

It contains just a Gradle task. As you can imagine, we will need to do a few tweaks to make it good enough for our application =) Let's start!

 

Choose the hosted agent

Out of the box, the template uses a hosted agent based on MacOS. That's perfectly fine but, if you have any reason to do it, feel free to switch to a Windows or a Linux one. Android compilation is supported by all the operating systems, so you can use the one you prefer.

 

Set the build number

I want to leverage for the Android version the same build number I'm using for the Windows version, which we're going to inject inside the manifest using the Gradle parameters we have previously defined. As such, exactly like I did for the Windows pipeline, I'm going to change the default Azure DevOps build number by setting the name property:

 

name: $(date:yyyy).$(Month)$(rev:.r)

This configuration will generate a build number like:

 

2020.1.15

 

Install the dependencies

Also this step isn't very different from the one we did to compile the Windows application. Before compiling the Android version, we need to make sure all the dependencies used by our React Native project are installed. As such, we're going to leverage Yarn once again with the following script:

 

- script: yarn install

 

Setup the Gradle build

The Gradle task will take care of running the build, by using the configuration defined in the various build.gradle files. However, compared to the default task included in the template, we need to add a few options:

 

- task: Gradle@2
  inputs:
    gradleWrapperFile: 'android/gradlew'
    workingDirectory: 'android/'
    options: '-PversionName=$(Build.BuildNumber) -PversionCode=$(Build.BuildId)'
    tasks: 'assembleRelease'
    publishJUnitResults: false
    javaHomeOption: 'JDKVersion'
    gradleOptions: '-Xmx3072m'

Here are the changes I've made:

 

  1. We need to specify where gradlew (the wrapper which launches the Gradle executable) is located through the gradleWrapperFile property. Typically it's stored in the root of the project. However, in our scenario, we're working with a React Native project, so the gradlew wrapper is stored inside the android folder and not in the root. For the same reason, we need to set also the workingDirectory property to android.

  2. We need to pass, as parameters, the versionName and the versionCode, so that they can be injected inside the manifest as per the logic we have previously added in the build.gradle file. We set them in the options section of the task configuration. Command line parameters must be prefixed by a capital P, so we set them as -PversionName and -pVersionCode. As value, we use two variables provided by Azure DevOps: we're using $(Build.BuildNumber) as version name (so something like 2020.1.15), while $(Build.BuildId) as version code (which is perfect for our scenario, since it's an incremental integer).

  3. The build.gradle file can define one or more tasks, that we can execute when we run the build. In case of a React Native application, we have two options:

    • assembleRelease is used to generate an APK, which can be used for sideloading or distribution through App Center.
    • bundleRelease is used to generate an AAP, which is the format required by the Google Play Store.

    In my case I opted for distribution through sideloading (so I need only the APK), but you can also decide to generate both packages simply by setting the tasks property like this:

     

    tasks: 'assembleRelease publishRelease'

 

Sign the package

As per the changes we have previously made in the build.gradle file, the generated APK is unsigned. As such, before deploying it, we need to sign it with the certificate we have previously created. We can do it using a dedicated task available in Azure DevOps, which is a much safer approach than doing it as part of the Gradle build: since the task leverages Secure Files and variables, we can protect our private key and our credentials. As first step, we need to upload the my-upload-key.keystore file we have previously generated as a Secure File on Azure DevOps. Secure Files is a feature which allows to upload files that can be only used in a specific pipeline or, eventually, deleted. No one, even the administrator of the Azure DevOps account, will be able to download it. It's a perfect fit for our private certificate: even if we're going to share the project with other developers, they won't be able to download the certificate and perform potentially malicious tasks (like using your certificate to sign other applications).

 

To upload the file, move to the Library section under Pipelines. Select the Secure Files tab and press the + button:

 

library.png

 

Drag the my-upload-key.keystore file you have previously generated and upload it. That's it. Now we'll be able to reference this file in the signing task. We need to safely save also the store password, the key alias and the key password. We're going to create a variable for each of them, so that we don't have to add them in clear in the task. In the pipeline editor in Azure DevOps click on Variables and press + to add a new one. Name it as you prefer. For example, let's start with the store password, so call it KeyStorePassword. As value, type the password you have specified during the key creation, then enable the option Keep this value secret. This will make sure that no one will be able to read its value.

 

StorePassword.png

 

Now repeat the process for the other two variables. At the end, you should see a list like this:

 

Variables.png

 

Now we are ready to add the task to our pipeline:

 

- task: AndroidSigning@3
  inputs:
    apkFiles: '**/*.apk'
    apksignerKeystoreFile: 'my-upload-key.keystore'
    apksignerKeystorePassword: '$(KeyStorePassword)'
    apksignerKeystoreAlias: '$(KeyAlias)'
    apksignerKeyPassword: '$(KeyPassword)'
    zipalign: false
  1. Using the apkFiles property, we specify we want to sign all the APK files included in the output folder.
  2. As apksignerKeystoreFile we need to add a reference to the private key we have previously uploaded as secure file. We need just to specify it's base name, like if it's a local file.
  3. As apksignerKeyStorePassword, apksignerKeystoreAlias and apksignerKeyPassword we need to specify the three variables we have previously created. We use the standard syntax supported by Azure DevOps, which is $(variableName).

Publish the build artifacts

We're almost done. We just need to publish the signed APK package as build artifact, so that our CI pipeline will be able to pick it for deployment. This is the task to add:

 

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: 'android/app/build/outputs/apk/release'
    ArtifactName: 'drop'
    publishLocation: 'Container'

When you generate an APK for a React Native application, the file is created inside the android/app/build/outputs/apk/release folder. As such, we copy only the content of this folder. In case you have generated also an AAP for the Google Play Store, the file will be inside the android/app/build/outputs/bundle/release folder.

 

Executing the pipeline

After all the changes you have done, this is how the final pipeline should look like:

 

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

name: $(date:yyyy).$(Month)$(rev:.r)

steps:

- script: yarn install

- task: Gradle@2
  inputs:
    gradleWrapperFile: 'android/gradlew'
    workingDirectory: 'android/'
    options: '-PversionName=$(Build.BuildNumber) -PversionCode=$(Build.BuildId)'
    tasks: 'assembleRelease'
    publishJUnitResults: false
    javaHomeOption: 'JDKVersion'
    gradleOptions: '-Xmx3072m'

- task: AndroidSigning@3
  inputs:
    apkFiles: '**/*.apk'
    apksignerKeystoreFile: 'my-upload-key.keystore'
    apksignerKeystorePassword: '$(KeyStorePassword)'
    apksignerKeystoreAlias: '$(KeyAlias)'
    apksignerKeyPassword: '$(KeyPassword)'
    zipalign: false

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: 'android/app/build/outputs/apk/release'
    ArtifactName: 'drop'
    publishLocation: 'Container'

After saving it, Azure DevOps will automatically trigger it. Checking the logs will be helpful to understand if everything is running fine. For example, the version code and version number we have passed as parameters should be displayed as part of the Gradle task, since we have printed them:

 

GradleLogging.png

 

At the end of the process, your artifact will contain the generated APK:

 

GenericAPK.png

 

Improving the generated file name

By default, the generated APK will be called app-release-unsigned.apk. There isn't any specific downside in using this name, but it may be hard to track which exact app and version is included inside the package. It would be better to give it a more meaningful name, maybe including also the version number. We can do it by modifying the build.gradle file included in the android/app folder. Towards the end of the file you will find a section called applicationVariants.all, which iterates all the files which are generated by the build process to configure them. This is how it looks like:

 

applicationVariants.all { variant ->
    variant.outputs.each { output ->
        // For each separate APK per architecture, set a unique version code as described here:
        // https://developer.android.com/studio/build/configure-apk-splits.html
        def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
        def abi = output.getFilter(OutputFile.ABI)
        if (abi != null) {  // null for the universal-debug, universal-release variants
            output.versionCodeOverride =
                    versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
        }
    }
}

The output object represents a single file that is produced by the build, so we can use it to change the file name thanks to the outputFileName property. This is how the section should look like after our change:

 

applicationVariants.all { variant ->
    variant.outputs.each { output ->
        // For each separate APK per architecture, set a unique version code as described here:
        // https://developer.android.com/studio/build/configure-apk-splits.html
        def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
        def abi = output.getFilter(OutputFile.ABI)
        if (abi != null) {  // null for the universal-debug, universal-release variants
            output.versionCodeOverride =
                    versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
        }
        output.outputFileName = output.outputFileName
                        .replace("app-", "NavigationSample-")
                        .replace("unsigned", "")
                        .replace(".apk", "${variant.versionName}.apk")

    }
}

We're taking the outputFileName property and:

  • We replace the generic app- string with the name of our application (in my case, it's NavigationSample-).
  • We remove the unsigned string
  • We add, as suffix, the version number, which can be referenced with the ${variant.versionName} property.

Now, if we save the build.gradle file and we push it to our repository, Azure DevOps will trigger a new execution of the pipeline. At the end of the process, we will find in the artifacts an APK with a more meaningful name, like NavigationSample-2020.1.18.apk.

 

RealNameAPK.png

 

Deploy the application

Now that you have generated a signed package, you can create a release pipeline to do the deployment. You have many options, based on your scenario. If you have generated an AAP and you want to publish it on the Google Play Store, Azure DevOps offers a task for that. Another great option is to deploy your application to a group of testers, that will help you to validate the update before publishing it on the Store. In this case you can leverage App Center, the Microsoft platform to automate the build, deployment and testing of mobile and desktop apps. Also for this scenario Azure DevOps offers a dedicated task, you'll just need a free account on App Center.

 

Wrapping up

In this post we have learned how to create a CI pipeline for our React Native project to generate an Android application. We have created it alongside the Windows one we have already created in another post. Now, every time we're going to commit some new code to our repository, Azure DevOps will automatically build and deploy both the Windows and Android versions of our application. Once they have been validated, we can automatically deploy them to our users, regardless if we're using sideloading or the Store.

 

If you're building React Native applications for Android and iOS, there's also another option you can explore: App Center. Other than using it to perform the deployment, App Center supports also directly enabling CI/CD. You just need to connect it to your repository, specify that your project is based on React Native and setup a few parameters (like providing the private certificate and the passwords). App Center will take care of everything and it will automatically create a pipeline for you. In my case, I preferred to use Azure DevOps because I wanted more flexibility and to consolidate all the pipelines for my project (Windows, Android and in the future iOS) in a single place. However, App Center is a great choice if you're looking for a simple and reliable solution to automatically build and deploy your React Native projects.

 

Happy coding!

8 Comments

Thanks for Sharing this great blogpost with the Community :cool:

Microsoft

Hey! Excellent Article, Can you please share the YAML file if possible ? @Matteo Pagani 

Microsoft

Hello @tanmay1992 the project is on GitHub, including the two pipelines for Windows and Android. This is the direct link for the Android YAML: NavigationSample/azure-pipelines-android.yml at master · qmatteoq/NavigationSample (github.com)

 

Best

Copper Contributor

Thank you. Do you have guideline for using Azure DevOps to create a CI/CD pipeline for a IOS application built with React Native?

Microsoft

Hello @markkub20 unfortunately I don't have any handy guide on this topic, since I don't work much on iOS and macOS. However, there are many guides on the web, like Building a React Native pipeline for Android and iOS - DEV. I would take a look at them!

Copper Contributor

Hi, I follow the guidelines and run a pipeline for android but got an error. I stored the Keystore file in the secure file but the pipeline didn't get it. 

> Keystore file '/Users/runner/work/1/s/android/app/***' not found for signing config 'release'.

Any clue?

 

Microsoft

Hello @Roshan_Maddumage  can you please share the YAML with the configuration of your Android signing task?

 

Thanks!

Copper Contributor

@Matteo Pagani 

trigger:
- developer

pool:
  vmImage: 'macos-latest'

name: $(date:yyyy).$(Month)$(rev:.r)

steps:

- script: yarn install

- task: Gradle@2
  displayName: 'Build Android'
  inputs:
    gradleWrapperFile: 'android/gradlew'
    workingDirectory: 'android'
    options: '-PversionName=$(Build.BuildNumber) -PversionCode=$(Build.BuildId)'
    tasks: 'assembleDevRelease'
    publishJUnitResults: false
    gradleOptions: '-Xmx3072m'

- task: AndroidSigning@3
  displayName: 'Android Signing'
  inputs:
    apkFiles: '**/*.apk'
    apksignerKeystoreFile: 'mykeystore.keystore'
    apksignerKeystorePassword: '$(MYAPP_RELEASE_STORE_PASSWORD)'
    apksignerKeystoreAlias: 'mykeyalias'
    apksignerKeyPassword: '$(MYAPP_RELEASE_KEY_PASSWORD)'
    zipalign: false

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: 'android/app/build/outputs/bundle/devrelease'
    ArtifactName: 'drop'
    publishLocation: 'Container'

This is the YAML file that I have used. 

Version history
Last update:
‎Jan 07 2020 05:13 AM
Updated by: