Implementing a Flutter CI/CD Pipeline in Jenkins: Part 4(Jenkinsfile — Android and iOS/Fastlane)

This fourth and final part of the series, I’ll explain the Android and iOS bits of the Jenkinsfile and what is necessary to distribute to AppCenter.

In the Jenkinsfile, to build an Android Release APK, the section below is all that’s necessary.

stage ('Flutter Build APK') {
      steps {
          sh "flutter build apk --split-per-abi"
      }
}

However, in order for this to be successful, you must confirm that you have the Android SDK (above version 28) frameworks installed on the machine Jenkins is running on. Additionally, you must include the .jks key file necessary for signing the apk in the base directory of the Flutter project and confirm that you are able to build a release APK locally. Steps for this are found in Flutter’s documentation here.

After building, in order for those built files to be of use, the built files must be distributed somewhere so testers can access them. Luckily, AppCenter has a jenkins plugin that you can install that makes this super simple. In order to upload to AppCenter, you need to have setup your AppCenter account, have an API Token, an App in AppCenter for Android, and a Distribution Group for that App as well.

Assuming you’ve completed Step 2 of this series, the AppCenter plugin should already be installed. If not, install it now. Once the AppCenter plugin is installed and Jenkins has rebooted, this next stage will work. Simply fill in your API Token, Owner Name(AppCenter account name, not email), App Name in AppCenter, and Distribution Group. The path to the app section of code should remain the same across all projects, though you may want to distribute multiple apk’s if you’re using devices with different processor requirements.

stage('Distribute Android APK') {
      steps {
          appCenter apiToken: 'APITOKENHERE',
                  ownerName: 'Phtiv08',
                  appName: 'Flutter-Beer',
                  pathToApp: 'build/app/outputs/apk/release/app-arm64-v8a-release.apk',
                  distributionGroups: 'Flutter-Beer-Distribution'
      }
}

Finally, I’ll go over the iOS stages of this Jenkinsfile. The first stage for iOS should require no special handling. It should work without any fancy legwork required. Also, this step doesn’t sign the build, as signing will happen in the archive step handled in Fastlane.


stage('Flutter Build iOS') { steps { sh "flutter build ios --release --no-codesign" } }

The next stage unfortunately is a bit more involved. Because of the complexity of keychain and signing processes for iOS, I leverage the brbuild_ios repo on Bitbucket to create a custom/temporary keychain. This requires a ‘mobileprovision’ in the ios/ios-provisioning-profile-vault folder of your flutter project. It also requires two other files. The first of these is a .p12 file which is created by exporting a saved .cer file from your local machine’s keychain access. The second is a .pass file containing the password to this .p12 file. Both of these files should be in the ios/ios-p12-vault folder of your flutter project. Additionally, you must open Xcode and set the Runner target to use this provisioning profile on your local machine.

After you’ve made your Distribution Certificate, Provisioning Profile, and generated your p12, and pass files and put them where they need to go, you can move on to your fastfile to execute iOS build and distribution commands. Below is the full fastfile used in my pipeline.

fastlane_version "2.102.0"
default_platform(:ios)
 
import("brbuild_ios/fastlane/Fastfile")
 
platform :ios do
 
  build_number = ENV["BUILD_NUMBER"]
  output_name = "FlutterBeer_AdHoc_#{build_number}"
  ipa_name = "./.build/#{output_name}"
  scheme = "Runner"
 
  def kill_simulators
    Action.sh("killall -9 Simulator 2>/dev/null || echo No simulators running")
  end
 
  def setup
    build_setup(certificate_names: ["Flutter_Cert.p12"],
                provisioning_profile_names: ["Flutter_Beer_Ad_Hoc.mobileprovision"],
                should_log: true)
  end
 
  def cleanup
    build_cleanup
    clear_derived_data(derived_data_path: "./dd")
  end
 
  desc "The buildAdHocCore lane builds the FlutterBeer archive in the ad-hoc configuration"
  lane :buildAdHocCore do
    gym(scheme: "#{scheme}", configuration: "Release", output_name: "#{output_name}", clean: true, export_method: "ad-hoc",
        output_directory: "./.build", archive_path: ipa_name, derived_data_path: "./dd")
  end
 
  desc "The uploadToAppCenter lane uploads a pre-built IPA to AppCenter"
  lane :uploadToAppCenter do
      appcenter_upload(
      api_token: "APITOKENHERE",
      owner_name: "Phtiv08",
      owner_type: "user",
      app_name: "Flutter-Beer-iOS",
      file: ".build/#{output_name}.ipa",
      destinations: 'Flutter-Beer-iOS-Distribution',
      destination_type: 'group',
      notify_testers: true
    )
  end
 
  desc "The buildAdHoc lane builds the FlutterBeer archive in the ad-hoc configuration"
  lane :buildAdHoc do
    begin
      setup
      buildAdHocCore
      uploadToAppCenter
    rescue => exception
      cleanup
      raise exception
    else
      cleanup
    end
  end
end

I’d like to go over a couple sections of this fastfile to explain them and their functions. First, the setup step calls a function from the brbuild_ios repo that sets up the temporary keystore with the supplied certificates and provisioning profiles.


def setup build_setup(certificate_names: ["Flutter_Cert.p12"], provisioning_profile_names: ["Flutter_Beer_Ad_Hoc.mobileprovision"], should_log: true) end

The second block I want to go over is the buildAdHocCore lane. This lane builds the iOS Archive and then the IPA to distribute. The ‘scheme’ variable passed in should always be ‘Runner’ for Flutter projects. This is because ‘Runner’ is the name/xcode project auto-generated in the ‘flutter build ios’ command. If you want to do other configurations and export methods, these can be defined in this lane.

desc "The buildAdHocCore lane builds the FlutterBeer archive in the ad-hoc configuration"
  lane :buildAdHocCore do
    gym(scheme: "#{scheme}", configuration: "Release", output_name: "#{output_name}", clean: true, export_method: "ad-hoc",
        output_directory: "./.build", archive_path: ipa_name, derived_data_path: "./dd")
end

Before you attempt to run your fastfile, assuming you’re going to distribute via AppCenter, you should navigate locally to your flutter/ios directory and run the following command and commit the resulting file. The AppCenter plugin can be viewed in more detail here.

fastlane add_plugin appcenter

Next, we run the Fastlane commands defined in the above fastfile. The ‘dir’ block in this step tells Jenkins where to run the fastlane commands.

stage('Make iOS IPA') {
    steps {
        dir('ios'){
                sh "bundle install"
                sh "bundle exec fastlane buildAdHoc --verbose"
        }
    }
}

Because distribution of the iOS build is handled in fastlane, it’s unnecessary to have a distribution stage for iOS in the Jenkinsfile. The last stage of the Jenkinsfile should be to clean your Flutter project. This will ensure that leftover artifacts from failed builds don’t corrupt future new builds.

stage('Cleanup') {
    steps {
        sh "flutter clean"
    }
}

One more parting thought:

If you are getting errors with code ‘65’, that likely means something is wrong with your signing setup. The first thing I would do is confirm that you can build locally with the mobileprovision and certificate that you include in your repo for brbuild_ios to use. Next, I would check your Jenkins machine and ensure that there aren’t any duplicate certificates or provisioning profiles in Keychain Access.

That concludes this series. I hope this helps you create a CI/CD pipeline for your Flutter project!

Published by Steven Foskett in Android, iOS, Technology, App Development, Flutter