Multi-stage builds for Ionic Apps with Azure Pipeline Templates
Tags: tutorial, azure, devops, ionic
Published: 2021-01-26 05:31:05

---
title: Multi-stage builds for Ionic Apps with Azure Pipeline Templates
published: true
date: 2021-01-26 05:30:00 UTC
tags: #tutorial #azure #devops #ionic
canonical_url: https://sahansera.dev/multi-stage-builds-with-azure-pipelines-ionic/
cover_image: https://sahansera.dev/static/88129d5d657f47b1c0e6d768f85db212/dcbba/multi-stage-builds-with-azure-pipelines-ionic.webp
---

In this article, we will create an Azure Pipeline for an Ionic app using the new Azure pipelines YAML templates. We will also cover signing a release build of the published artifacts (APK signing). I have summarised the goals of this blog post in the diagram below.

![multi-stage-builds-with-azure-pipelines-ionic-0.png](https://sahansera.dev/static/bae2960ee9ab6d889a814e6099c8aa99/5a190/multi-stage-builds-with-azure-pipelines-ionic-0.png "multi-stage-builds-with-azure-pipelines-ionic-0.png")

_A picture is worth a thousand words. Any points for my diagramming skills? 💯😅_

Our main goals are to;

1. Scaffold an Ionic app
2. Generate the Android app
3. Set up Azure pipelines
4. Sign the release APKs

> 💡 You can follow along with my completed project repository in [here](https://github.com/sahan91/AzurePipelinesTemplateForIonic).

### Scaffold an Ionic App

Let’s get the initial setup out of the way! Ionic docs already have got a great [getting started guide](https://ionicframework.com/docs). In this step, we are not going to do anything fancy in Ionic’s side of things. We will just scaffold a basic app and use Capacitor to generate an Android app. If you have already got an existing Ionic app, you can skip ahead to the “Create the Azure Pipeline” section.

**1. Creating a blank Ionic app**

```sh
ionic start sampleapp blank --capacitor --type=ionic-angular
```

As you can see, there’s nothing fancy here. Just a blank Ionic+Angular project with Capacitor integration.

**2. Set Project ID (optional)** - You don’t need to change the appId for this tutorial, but I like to set it anyway.

```json
{
...
  "appId": "com.sahan.sampleapp",
...
}
```

Open up [capacitor.config.json](https://github.com/sahan91/AzurePipelinesTemplateForIonic/blob/main/sampleapp/capacitor.config.json) file and change the `appId` property to whatever suits you.

**3. Add Capacitor - Android Project**

If you haven’t set up your environment for Android development, please follow this [guide](https://ionicframework.com/docs/developing/android) to do so.

```sh
ionic build && ionic cap add android
```

Note the `ionic build` command here. We first need to issue the `ionic build` command before adding the Android project for the first time. With the initial `ionic cap add android` command it will copy the required web assets into the native project.

> 💡 It’s now [recommended to keep your native projects](https://capacitorjs.com/docs/cordova#native-project-management) in source control.

If you make any changes to your Ionic project, you can use the `ionic cap copy` command to copy any changed web assets or `ionic cap sync` if you are using/updated any native dependencies.

After these steps, your project structure should look like the following.

![multi-stage-builds-with-azure-pipelines-ionic-1.png](https://sahansera.dev/static/c8b7b9b1d611ef3e3684302eaee26af8/bb630/multi-stage-builds-with-azure-pipelines-ionic-1.png "multi-stage-builds-with-azure-pipelines-ionic-1.png")

### Create the Azure Pipeline

For this tutorial, I will create a single `azure-pipelines.yml` file and build-publish two Android apps, one of Debug configuration and another for Release.

**1. Creating the initial structure of the `azure-pipelines.yml`**

Let’s first create a folder called `infrastructure` at the root of our solution structure. We will use this folder to keep all our Azure Pipeline line YAML files. We will then add two additional YAML files called `ionic-android-debug-build.yml` and `ionic-android-release-build.yml`. I will explain why we do this in a second. Your file structure should look like this.

![multi-stage-builds-with-azure-pipelines-ionic-2.png](https://sahansera.dev/static/eed8069d53f12410b7077fc11f68bebe/d5c60/multi-stage-builds-with-azure-pipelines-ionic-2.png "multi-stage-builds-with-azure-pipelines-ionic-2.png")

For our initial step, we will create the barebones structure of our azure-pipelines.yml file

```yaml
trigger:
- main

variables:
  vmImageName: 'windows-latest'
  projectName: 'SampleApp'

stages:
  - stage: Build
    displayName: Build Ionic - Android projects
    jobs:
      # Debug build
      - job: Build_Ionic_Android_Debug
        variables:
          - name: buildConfiguration
            value: Debug
        displayName: Build Debug
        pool:
          vmImage: $(vmImageName)
        steps:
          - template: ionic-android-debug-build.yml

      # Release build
      - job: Build_Ionic_Android_Release
        variables:
          - name: buildConfiguration
            value: Release
        displayName: Build Release
        pool:
          vmImage: $(vmImageName)
        steps:
          - template: ionic-android-release-build.yml
```

Here’s a rough explanation of the structure of the above file.

1. `trigger`: We trigger our pipeline whenever a change happens to the `main` branch
2. `variables`: We have defined two global variables which can be reused in our stages
3. 

`stages`: This is where we define our stages. Initially, we have added `Build` stage. Later we will add another stage called `Deploy`

  - `jobs`: We have two jobs called `Debug` and `Release` .
  - `jobs:steps`: Note how we give our template path with a `template` attribute. This way, we don’t need to keep everything in one YAML file.

> 💡 Tip: If you are using the `script` task you will sometimes find that any multiline commands won’t run. Only the first one would get picked up. You can append your commands with `&&` or use a PowerShell task as shown in this tutorial.

**2. Create ionic-android-debug-build.yml**

```yaml
steps:    
  - script: npm install -g @ionic/cli
    displayName: 'Install Ionic CLI'

  - task: Npm@1
    inputs:
      workingDir: '$(Build.SourcesDirectory)/$(projectName)'
      command: install
    displayName: 'NPM Install'

  - powershell: |
      ionic cap build android --no-open
      npx cap sync
      cd android
      ./gradlew assemble$(buildConfiguration)
    workingDirectory: $(Build.SourcesDirectory)/$(projectName)
    displayName: 'Build Android Project'

  - task: CopyFiles@2
    inputs:
      SourceFolder: '$(Build.SourcesDirectory)/$(projectName)/android/app/build/outputs/apk/$(buildConfiguration)'
      contents: "**/app-$(buildConfiguration).apk"
      targetFolder: "$(Build.ArtifactStagingDirectory)/$(projectName)"
    displayName: "Copy unsigned APK to staging directory"

  - task: PublishBuildArtifacts@1
    inputs:
      PathtoPublish: "$(Build.ArtifactStagingDirectory)/$(projectName)"
      ArtifactName: "$(projectName)"
      publishLocation: "Container"
    displayName: "Publish artifacts"
```

We use the `gradelw` utility that comes with the android project to build it. This is a wrapper around Gradle which invokes it with any parameters we have given (in our case `assembleDebug` or `assembleRelease`). If Gradle is not found in your system (or in a build agent) it will also download it from a distribution server.

The `ionic-android-release-build.yml` is pretty much the same configuration except for some extra flags to do the release build. We will go through this file in the Signing step.

Now, let go ahead and commit the changes. You might want to [create a new Azure Pipeline](https://docs.microsoft.com/en-us/azure/devops/pipelines/create-first-pipeline?view=azure-devops&tabs=java%2Ctfs-2018-2%2Cbrowser#create-your-first-java-pipeline) if you haven’t already. Once the pipeline is run, you will be able to see all green (hopefully). 😀

![multi-stage-builds-with-azure-pipelines-ionic-3.png](https://sahansera.dev/static/951693eb5b9c5757645e06620dc4d5be/e185b/multi-stage-builds-with-azure-pipelines-ionic-3.png "multi-stage-builds-with-azure-pipelines-ionic-3.png")

If you have a look at the build artifacts, you will find the debug and release build APKs all in one place now. Next, we will have a look at signing the APK files.

![multi-stage-builds-with-azure-pipelines-ionic-4.png](https://sahansera.dev/static/a1024958d33dc7f70a8901a235b4ba74/1e043/multi-stage-builds-with-azure-pipelines-ionic-4.png "multi-stage-builds-with-azure-pipelines-ionic-4.png")

### Signing the APK

If you are publishing your app to the Play Store, we need to consider signing our APKs. Ionic’s [official documentation](https://ionicframework.com/docs/deployment/play-store) already covers this step, so we will look at how we can automate this step.

**1. Generating the .keystore file**

If you are doing this for the first time, you need to create the private `.keystore` file used for signing the APK. You can use the `keytool` file that comes with the Android SDK:

```sh
keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
```

Remember to take note of the following when you are generating this file.

- Keystore Password
- Keystore Alias
- Key Password

We will be putting these into pipeline variables as our next step.

**2. Creating the pipeline variables**

Once you have generated the keystore file, you can navigate to the Library section of your project in Azure DevOps.

![multi-stage-builds-with-azure-pipelines-ionic-5.png](https://sahansera.dev/static/2518476b152b1902061e06a2efd9f3ee/5a190/multi-stage-builds-with-azure-pipelines-ionic-5.png "multi-stage-builds-with-azure-pipelines-ionic-5.png")

Make sure to put in the `keystorePassword`, `keyAlias` and `keyPassword` variables which we created in the above step.

After that, you need to upload your keystore file to Secure files section. We will later access this file in the Android signing task.

![multi-stage-builds-with-azure-pipelines-ionic-6.png](https://sahansera.dev/static/849c39384bf3de44ddb6290ff23e34e5/d7398/multi-stage-builds-with-azure-pipelines-ionic-6.png "multi-stage-builds-with-azure-pipelines-ionic-6.png")

**3. Add the signing task**

We will be using the [AndroidSigning@3](https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/build/android-signing?view=azure-devops) task to sign our release build. It’s pretty straight forward task to use and the parameters we need to send are as follows.

```yaml
- task: AndroidSigning@3
    inputs:
      apkFiles: '$(Build.SourcesDirectory)/$(projectName)/android/app/build/outputs/apk/$(buildConfiguration)/*.apk'
      apksign: true
      apksignerKeystoreFile: '${{ parameters.keystoreFileName }}'
      apksignerKeystorePassword: '${{ parameters.keystorePassword }}'
      apksignerKeystoreAlias: '${{ parameters.keyAlias }}'
      apksignerKeyPassword: '${{ parameters.keyPassword }}'
      apksignerArguments: --out $(Build.SourcesDirectory)/$(projectName)/android/app/build/outputs/apk/$(buildConfiguration)/$(projectName)-$(buildConfiguration).apk --verbose
      zipalign: true
    displayName: 'Sign the APK'
```

Notice how we are linking the variables we created in the Library section. Now, our final [ionic-android-release-build.yml](https://github.com/sahan91/AzurePipelinesTemplateForIonic/blob/main/infrastructure/ionic-android-release-build.yml) file would look like the following.

```yaml
parameters:
  - name: keystoreFileName
    displayName: "The keystore file name for signing the apk"
    type: string
  - name: keystorePassword
    displayName: "Password for the keystore"
    type: string
  - name: keyAlias
    displayName: "Key alias"
    type: string
  - name: keyPassword
    displayName: "Key password"

steps:    
  - script: npm install -g @ionic/cli
    displayName: 'Install Ionic CLI'

  - task: Npm@1
    inputs:
      workingDir: '$(Build.SourcesDirectory)/$(projectName)'
      command: install
    displayName: 'NPM Install'

  - powershell: |
      ionic cap build android --prod --no-open
      npx cap sync
      cd android
      ./gradlew assemble$(buildConfiguration)
    workingDirectory: $(Build.SourcesDirectory)/$(projectName)
    displayName: 'Build Android Project'

  - task: AndroidSigning@3
    inputs:
      apkFiles: '$(Build.SourcesDirectory)/$(projectName)/android/app/build/outputs/apk/$(buildConfiguration)/*.apk'
      apksign: true
      apksignerKeystoreFile: '${{ parameters.keystoreFileName }}'
      apksignerKeystorePassword: '${{ parameters.keystorePassword }}'
      apksignerKeystoreAlias: '${{ parameters.keyAlias }}'
      apksignerKeyPassword: '${{ parameters.keyPassword }}'
      apksignerArguments: --out $(Build.SourcesDirectory)/$(projectName)/android/app/build/outputs/apk/$(buildConfiguration)/$(projectName)-$(buildConfiguration).apk --verbose
      zipalign: true
    displayName: 'Sign the APK'

  - task: CopyFiles@2
    inputs:
      SourceFolder: '$(Build.SourcesDirectory)/$(projectName)/android/app/build/outputs/apk/$(buildConfiguration)'
      contents: "**/$(projectName)-$(buildConfiguration).apk"
      targetFolder: "$(Build.ArtifactStagingDirectory)/$(projectName)"
    displayName: "Copy unsigned APK to staging directory"

  - task: PublishBuildArtifacts@1
    inputs:
      PathtoPublish: "$(Build.ArtifactStagingDirectory)/$(projectName)"
      ArtifactName: "$(projectName)"
      publishLocation: "Container"
    displayName: "Publish artifacts"
```

> 💡 Tip: I found that I can’t make the file name coming from the variable group. Which means you need to have it hardcoded in the azure-pipelines.yml file

First, we need to tell the azure-pipeline.yml which variable group to use. Here’s a small excerpt of that.

```yaml
# other tasks removed for brevity
- job: Build_Ionic_Android_Release
  variables:
    - group: SampleApp-Release
    - name: buildConfiguration
      value: Release
```

> 💡 Note that the notation for [defining a variable](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch) can take two variants. If you are not using a group you could simply write it as `buildConfiguration: Release`. If you are using the `group` attribute, you need to use the notation I have shown in the above snippet. I had to learn it the hard way 😂

You would have also noticed that we are using `parameters` here. These will be injected in the azure-pipeline.yml file.

```yaml
# other tasks removed for brevity
steps:
  - template: ionic-android-release-build.yml
    parameters:
      keystoreFileName: 'sampleapp-release-key.keystore'
      keystorePassword: $(keystorePassword)
      keyAlias: $(keyAlias)
      keyPassword: $(keyPassword)
```

Once you push your changes to the repo, Azure DevOps would ask you to permit your pipeline to access the secure file which we added in the above step. Click on View and permit it. Alternatively, you could also use Azure KeyVault for storing secrets.

![multi-stage-builds-with-azure-pipelines-ionic-7.png](https://sahansera.dev/static/0aebaf65b3e7841c3073aec94cb06e75/5a190/multi-stage-builds-with-azure-pipelines-ionic-7.png "multi-stage-builds-with-azure-pipelines-ionic-7.png")

The completed YAML files can be found in my repo over [here](https://github.com/sahan91/AzurePipelinesTemplateForIonic/tree/main/infrastructure). Once the pipeline is completed, you can have a look at the artifacts. Voilà! We have the signed APKs

![multi-stage-builds-with-azure-pipelines-ionic-8.png](https://sahansera.dev/static/4788c14fe715d74c8dfc8ed000153e10/43142/multi-stage-builds-with-azure-pipelines-ionic-8.png "multi-stage-builds-with-azure-pipelines-ionic-8.png")

### Conclusion

In summary, we created a blank Ionic app, set up Azure DevOps pipeline YAML files and published the signed artifacts. You can use the same structure to add an iOS project as well. However, the build steps could be more involved and cumbersome to get right initially. In my next article, we will look at how we can integrate with Microsft App Center for distribution.

If you have any feedback or questions, let me know in the comments below. Until next time 👋

### References

1. [https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops)
2. [https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch)
3. [https://ionicframework.com/docs/developing/android](https://ionicframework.com/docs/developing/android)