Flutter, fastlane, macOS, CI/CD: deploy to TestFlight

In this article I describe how to setup build and deployment pipeline for Flutter macOS desktop app using fastlane and gitlab-runner.

March 16, 2023 3 min read

#flutter
#fastlane
#macos
#ci/cd
#desktop
#testflight

Introduction

So, as I stated in previous article, one day I realized I have to build own CI/CD pipeline for Flutter app. With iOS version of the app I had no problems, but with macOS version I faced some difficulties. So, here is my path.

Code Signing Management

I use match to manage code signing certificates and provisioning profiles. I have two lanes for this: certificates and generate_new_certificates. The first one is used to get certificates and provisioning profiles from git repository. The second one is used to generate new certificates and provisioning profiles and push them to git repository. Setup is pretty straightforward, just look into docs.

Build

Well, in general it's as easy as flutter build macos --release. But I never managed to make it work with Code Signing. Whatever settings I tried, code signing always failed. So, I decided to use xcodebuild directly. Not literally, but with build_mac_app action. It's a wrapper around xcodebuild and it works like a charm. I use it like this:

  build_mac_app(
    configuration: "Release",
    skip_package_pkg: false,
    output_directory: "builds",
    xcodebuild_formatter: 'xcbeautify',
    silent: true,
    export_method: "app-store",
    export_options: {
      provisioningProfiles: {
        "com.app.flutter" => "match AppStore com.app.flutter macos"
      }
    }
  )

But before that flutter needs to generate some files, so I need to run following commands:

  Dir.chdir "../.." do
    sh("flutter packages get")
    sh("flutter clean")
    sh("flutter build macos --release")
  end

Unfortunately, flutter build macos action doesn't support --config-only flag just yet, so double build is required. First time your app is built with flutter build macos and second time with build_mac_app action. As long as #118649 will be released, I will be able to use --config-only flag and save build time.

Deployment to TestFlight

Well, it's easy. First, increment build number:

  increment_build_number({
    build_number: latest_testflight_build_number(
      version: '2.5.0',
      api_key_path: 'fastlane/XXXXXX.json',
      platform: 'osx'
    ) + 1
  })

Then, upload to TestFlight:

  upload_to_testflight(
    uses_non_exempt_encryption: false,
    api_key_path: 'fastlane/XXXXXX.json',
    skip_waiting_for_build_processing: true,
    app_version: '2.5.0',
  )

And all that orchestrated by gitlab CI:

testflight-macos:
  stage: deploy
  needs: [ ]
  script:
    - cd $CI_PROJECT_DIR/apps/frontend/macos
    - bundle exec fastlane beta
  only:
    - main

As easy as that.

Complete Fastlane file

default_platform(:mac)

platform :mac do
  desc "Get certificates"
  lane :certificates do
    sync_code_signing(
      type: "development",
      api_key_path: 'fastlane/XXXXXX.json',
      app_identifier: ['com.app.flutter'],
      force_for_new_devices: true,
      readonly: true
    )
  end

  desc "Push a new beta build to TestFlight"
  lane :beta do
    ensure_git_status_clean
    match(
      type: "appstore",
      readonly: is_ci,
      platform: "macos",
      verbose: false,
      additional_cert_types: ["mac_installer_distribution"]
    )

    Dir.chdir "../.." do
      sh("flutter packages get")
      sh("flutter clean")
      sh("flutter build macos --release")
    end

    increment_build_number({
      build_number: latest_testflight_build_number(
        version: '2.5.0',
        api_key_path: 'fastlane/XXXXXX.json',
        platform: 'osx'
      ) + 1
    })

    build_mac_app(
      configuration: "Release",
      skip_package_pkg: false,
      output_directory: "builds",
      xcodebuild_formatter: 'xcbeautify',
      silent: true,
      export_method: "app-store",
      export_options: {
        provisioningProfiles: {
          "com.app.flutter" => "match AppStore com.app.flutter macos"
        }
      }
    )

    upload_to_testflight(
      uses_non_exempt_encryption: false,
      api_key_path: 'fastlane/XXXXXX.json',
      skip_waiting_for_build_processing: true,
      app_version: '2.5.0',
    )
  end
end

Comments