|||

Flutter CI/CD with Azure Devops & Firebase - iOS - Part 1

This is a guide on how to build and deploy flutter hello world apps both iOS and Android from a private git repo. The code will be built in Azure devops pipeline and will be deployed to firebase for distribution on test devices.

logo

Pre-reqs

  • A project exists in your AzureDevops account. For this post we’ll use a name flutter-helloworld-counterapp
  • The code is checked into this project’s private git repo.
  • An active Apple developer account that can be used to create siging keys etc.
  • A Google account for firebase distribution and firebase-cli installed locally.

Getting started

Our initial goal is to setup a pipeline that will trigger when code is pushed to our git repo. For this we’ll start by adding a new Yaml file to our repo that will contain the code for the pipeline. This file will contain two jobs one for each platform ios and android. Since AzureDevops does not have native support for building Flutter apps, we’ll use a plugin from the marketplace.

Build Phase - iOS

Lets create our first job in the azureops.yaml file to build our iOS app without any code signing at this stage using the marketplace plugin. The azureops.yaml should like below

trigger:
  - master

jobs:
  - job: ios
    pool:
      vmImage: "macos-latest"
    variables:
      - name: configuration
        value: "Release"
      - name: sdk
        value: "iphoneos"
      - name: iosReleaseDir
        value: $(Build.SourcesDirectory)/output/$(sdk)/$(configuration)
      - name: ipaFile
        value: $(iosReleaseDir)/hello_world_counter_app.ipa

    steps:
      - task: FlutterInstall@0
        displayName: "Install Flutter"
        inputs:
          mode: "auto"
          channel: "stable"
          version: "custom"
          customVersion: "2.2.3"

      - task: FlutterBuild@0
        displayName: "Flutter Build Apps - iOS"
        inputs:
          target: "ios"
          buildName: "$(Build.BuildNumber)"
          entryPoint: "lib/main.dart"
          iosCodesign: false

Now to test this file. We’ll have to create a new pipeline in AzureDevops and point it to the azureops.yaml inside this git repo. This can be done by clicking Create pipeline under pipelines and flow the sequence below: Connect > Azure Repos Git > Select > flutter-helloworld-counterapp > Configure > Existing Azure Pipelines YAML file > branch master > Path > /azureops.yaml > Run If the build is successful, next we’ll look at signing the app using a provisioning profile that has our test device.

Code Signing - iOS - Prerequisites

To sign the built ipa, we need to login to Apple developer portal and create a provisiong profile and a p12 certificate. To start with, we need a certificate signing request file (CSR). If you dont have one already, follow the instructions here to create one. Follow the steps below to create the provisioning profile:

  • We need to create a p12 cert file. For that we need to register an iOS distribution certificate in keychain. This can be done by logging into Xcode with the dev portal credentials and the click Manage certificates > Click the + button the left > Apple Distribution xcode
  • And the keychain should looks like this for My certificates” keychain
  • Next, right click on the private key and select export certificate. Password is optional. This should give us our p12 certificate. Password is optional.
  • Next, log into the apple portak and create an identifier for this app. Identifiers > AppIDs > App > Add in a description > Add Bundle Id (explicit) > com.example.helloWorldCounterApp > Register. Note, this can also be found in the project.pbxproj under PRODUCT_BUNDLE_IDENTIFIER field
  • Add a test device under the Devices tab. You can find UUID of the test device from XCode or iTunes. See Here
  • Finally create the provisioning profile under the profiles tab. Choose Distribution > AdHoc > Select the App ID we created earlier > Select the certificate we created earlier from xcode > select test devices > enter name for the new profile > Generate > Download

Code Signing - iOS

Pipelines > Library > Secure Files > + Secure File > Browse and upload both p12 and mobileprovision files. You can also create a variable group called secrets and store the p12 password as a secure variable if you chose a password whilst exporting the p12 from the keychain. Next add, the following yaml to azureops file to sign the build

Note: AzureDevops requires you to manually approve the use of the secure files first time they are referenced by the pipeline. This can be done via the GUI.

- task: InstallAppleCertificate@2
  displayName: "Install Apple p12 cert"
  inputs:
    certSecureFile: "app.p12"
    # certPwd: "$(certpassword)"
    keychain: "temp"

- task: InstallAppleProvisioningProfile@1
  displayName: "Install Apple Mobile Provisioning Profile"
  inputs:
    provisioningProfileLocation: "secureFiles"
    provProfileSecureFile: "app.mobileprovision"

- task: Xcode@5
  displayName: "Code Sign ipa for Distribution"
  inputs:
    actions: "build"
    scheme: "Runner"
    sdk: "$(sdk)"
    configuration: "$(configuration)"
    xcWorkspacePath: "ios/Runner.xcworkspace"
    xcodeVersion: "default"
    packageApp: true
    signingOption: "manual"
    signingIdentity: "$(APPLE_CERTIFICATE_SIGNING_IDENTITY)"
    provisioningProfileUuid: "$(APPLE_PROV_PROFILE_UUID)"

Upload signed build to firebase for distribution

To upload the signed ipa to firebase, we need to setup an ios project in firebase and then obtain an upload token that will use in our pipeline.

Firebase app distribution

iOS Bundle ID app nickname
com.example.helloWorldCounterApp hellocounter ios app

Download the GoogleService-Info.plist file as indicated on the console. Now open Xcode > Open a project or a file > Browse the Runner.xcworkspace file under the ios folder > Right click on the top Runner tree root item and select Add files to Runner.

Info-plist file

Run the app locally once again to make sure nothing is broken by issuing flutter run. The app should run as before. Next, lets obtain a firebase ci token by issuing the command firebase login:ci. We will copy the generated token value and store that in azure devops as a secret variable under pipeline > library. Also lets create a group called secrets that will store all future secret variables. We will call this token firebasetoken and save it under the secrets group. While we are at it, lets create a group called general’ that will store all our non-secrets variables. Make sure to reference these variable groups in the pipeline yaml like below.

jobs:
  - job: ios
    pool:
      vmImage: "macos-latest"
    variables:
      - group: secrets
      - group: general
      - name: configuration
        value: "Release"

A few more things we need to setup before we can publish the app to firebase.

  1. Set up a beta testers group with emails of testers

    testers

  2. ios app distribution id - this can be seen under project settings of our helloworld-counterapps > under Your apps section > ios apps > App ID. Copy this ID and save that as a variable named iosFirebaseDistAppId under the general group.

With all this setup, lets add the last task in our pipeline for distribution.

- task: Bash@3
  displayName: "Upload to firebase app distribution"
  inputs:
    targetType: "inline"
    script: |
      npm i -g firebase-tools
      ls -la $(iosReleaseDir)        
      firebase appdistribution:distribute "$(ipaFile)" \
      --app "$(iosFirebaseDistAppId)" \
      --release-notes "From Azure Devops" \
      --token "$(firebasetoken)" \
      --groups "beta-testers"

Push the code and if all the steps succeed, the testers in the beta testers group will be notified with the link to download the app, after they’ve accepted the invite. There might be additional steps to install the Firebase profile on the device, but the setup will guide you through the process. Below is the complete yaml file for reference.

trigger:
  - master

jobs:
  - job: ios
    pool:
      vmImage: "macos-latest"
    variables:
      - group: secrets
      - group: general
      - name: configuration
        value: "Release"
      - name: sdk
        value: "iphoneos"
      - name: iosReleaseDir
        value: $(Build.SourcesDirectory)/output/$(sdk)/$(configuration)
      - name: ipaFile
        value: $(iosReleaseDir)/hello_world_counter_app.ipa

    steps:
      - task: FlutterInstall@0
        displayName: "Install Flutter"
        inputs:
          mode: "auto"
          channel: "stable"
          version: "custom"
          customVersion: "2.2.3"

      - task: FlutterBuild@0
        displayName: "Flutter Build Apps - iOS"
        inputs:
          target: "ios"
          buildName: "$(Build.BuildNumber)"
          entryPoint: "lib/main.dart"
          iosCodesign: false

      - task: InstallAppleCertificate@2
        displayName: "Install Apple p12 cert"
        inputs:
          certSecureFile: "app.p12"
          keychain: "temp"

      - task: InstallAppleProvisioningProfile@1
        displayName: "Install Apple Mobile Provisioning Profile"
        inputs:
          provisioningProfileLocation: "secureFiles"
          provProfileSecureFile: "app.mobileprovision"

      - task: Xcode@5
        displayName: "Code Sign ipa for Distribution"
        inputs:
          actions: "build"
          scheme: "Runner"
          sdk: "$(sdk)"
          configuration: "$(configuration)"
          xcWorkspacePath: "ios/Runner.xcworkspace"
          xcodeVersion: "default"
          packageApp: true
          signingOption: "manual"
          signingIdentity: "$(APPLE_CERTIFICATE_SIGNING_IDENTITY)"
          provisioningProfileUuid: "$(APPLE_PROV_PROFILE_UUID)"

      - task: Bash@3
        displayName: "Upload to firebase app distribution"
        inputs:
          targetType: "inline"
          script: |
            npm i -g firebase-tools
            ls -la $(iosReleaseDir)        
            firebase appdistribution:distribute "$(ipaFile)" \
            --app "$(iosFirebaseDistAppId)" \
            --release-notes "From Azure Devops" \
            --token "$(firebasetoken)" \
            --groups "beta-testers"
Up next Flutter CI/CD with Azure Devops & Firebase - Android - Part 2 WCAG - Notes
Latest posts Refactor react code to use state store instead of multiple useState hooks Notes on Python Threat Modelling - Using Microsoft STRIDE Model WCAG - Notes Flutter CI/CD with Azure Devops & Firebase - iOS - Part 1 Flutter CI/CD with Azure Devops & Firebase - Android - Part 2 How to samples with AWS CDK A hashicorp packer project to provision an AWS AMI with node, pm2 & mongodb Some notes on Zeebe (A scalable process orchestrator) Docker-Compose in AWS ECS with EFS volume mounts Domain Driven Design Core Principles Apple Push Notifications With Amazon SNS AWS VPC Notes Building and Deploying apps using VSTS and HockeyApp - Part 3 : Windows Phone Building and Deploying apps using VSTS and HockeyApp - Part 2 : Android Building and Deploying apps using VSTS and HockeyApp - Part 1 : iOS How I diagnosed High CPU usage using Windbg WCF service NETBIOS name resolution woes The troublesome Git-Svn Marriage GTD (Getting things done) — A simplified view Javascript Refresher Sharing common connection strings between projects A simple image carousel prototype using Asp.net webforms and SignalR Simple logging with NLog Application logger SVN Externals — Share common assembly code between solutions Simple async in .net 2.0 & Winforms Clean sources Plus Console 2 — A tabbed console window