Deploy a Flutter app with Fastlane and Travis CI

Thomas Gallinari
8 min readJul 8, 2020
Why does it have to be so hard?

Hello and welcome to this new article of the Journey to Flutter series!

I will explain here how to use Fastlane to deploy a Flutter application in a snap, and how we can automatize the deployment with Travis CI so we don’t event need to think about it anymore!

Fastlane

Fastlane is a powerful tool that comes with a lot — A LOT! — of options to script your builds and deployments. And this is very welcome because most of the time deployment is a pain in the neck (not surprising that there are so many memes about it)!

Actually the topic is pretty well covered in both Flutter and Fastlane documentations, so I will not dive into every step in this article. Instead I will focus on what matters most and how to wrap it all together.

Here is a good place to start: https://flutter.dev/docs/deployment/cd. It describes pretty well everything we need to do in order to get a Flutter app deployed with Fastlane, on both Android and iOS stores. And when the topic needs to be explained in more detail, there are links to the specific topic.

In follow-up to my previous article related to Flavors, I needed to declare two lanes in order to deploy either the staging version or the production version:

Android

  • The function get_version_code is used for the prod version only, to get the version code of the build. It is used to create a changelog file that Fastlane will use to fill the new release notes in Google Play Store.
  • upload_to_play_store takes the appropriate parameters depending on the flavor we want to deploy. Pay close attention to the path to the AAB file. Note that I chose to deploy the staging version to the internal track, so only our internal QA team can download it from the Google Play Store, but it could work with any other track.

iOS

  • Here we need to build the .ipa because with flutter build we only get an .app file. I pass the appropriate Scheme in parameter of build_app depending on the flavor.
  • I distribute the staging version via TestFlight. I set skip_waiting_for_build_processing to true because it will be executed from Travis CI, and I do not want to block the job while iTunes Connect processes the build (it is quite long).
  • The changelog is easier to provide for iOS as it is a parameter of upload_to_app_store.
  • You may wonder what sync_code_signing is? Fear not, I will explain that in the very next chapter.

Signing

I hate signing

This is to me the most terrifying step. But to be honest, signing is far less painful now than a few years ago when we had to deal with the whole process manually. Now most of the configuration and files are generated. Let’s take a look.

Android

Signing the Android app is pretty straightforward, as described here: https://flutter.dev/docs/deployment/android#signing-the-app. Actually Android is not the main concern of this chapter.

iOS

On iOS the pain point is mostly about the certificates and provisioning profiles. Luckily, Fastlane comes with Match, a handy tool to manage everything for us! Basically it allows to generate, store and download these files from a remote repository.

The official documentation (https://docs.fastlane.tools/actions/match/) is pretty good, but I found this article particularly useful to sum it up: https://andreaslydemann.com/set-up-cd-for-ios-apps-using-travis-ci-and-fastlane/ (chapter 3 is about Match).

Now, remember this line in the Fastfile?

It tells Match to download the signing files before building the .ipa. The readonly parameter is here to prevent Match to generate the files for me if it does not exist (which I do not want for security reasons). In that regard, I did not give the App Manager role to the iOS account I use for Fastlane.

Thus I first generated these files manually with my own account:

Important! This step should ask you a password to encrypt the generated provisioning profiles. Remember this password, or put it in the MATCH_PASSWORD environment variable as you will need it later.

In the Matchfile we can use the Fastlane-specific account:

Note: I used the HTTPS URL to the repository, because I want to access it via Travis CI with Basic Authentication (more on that later).

Then comes the Xcode configuration. In my case I only need to sign the Release-Staging and Release-Prod versions, so I let the Automatic Signing box checked for the other versions:

The last thing we need to do is to tell Apple that our app is not using encryption, by adding this to our Info.plist:

<key>ITSAppUsesNonExemptEncryption</key>
<false/>

Otherwise, the IPA will not be available to the testers until we manually provide the compliance information in App Store Connect.

Wrap it up

Now we have everything configured, we can try it out:

  • On Android we need to deploy manually the very first .aab so the app metadata is initialized in the Google Play Console.
  • On iOS we need to pass --no-codesign as we sign when building the .ipa with Fastlane.
  • You may receive an email from Apple saying that your app is ready for push notifications, but your provisional profile is not. This is because by default the Flutter app implements a function to receive notifications, even if you do not need it. This is not blocking though. See https://github.com/flutter/flutter/issues/9984.
  • A few files are generated by Fastlane, that we do not want to push to your repository. We need to add them to the .gitignore: https://docs.fastlane.tools/best-practices/source-control/

Travis CI

Again, there is an official documentation about setting up Travis CI for Flutter: https://flutter.dev/docs/deployment/cd#cloud-build-and-deploy-setup.

However, there is no guide for creating a complete .travis.yml and how to build and deploy the application on both platforms from what we have configured before. And when it comes to signing and Continuous Deployment, it can turn into a living hell. When I set it up at Intent Technologies, I went through several issues before I get it right, so I will explain a few things that may be important to know.

As a starting point, here is my .travis.yml:

  • There are two jobs, one per platform.
  • In both cases we have to install Flutter and Fastlane.
  • Then we run the appropriate deployment script depending on the current branch or tag: in my case I want to deploy the staging version when I push on the staging branch, and I want to deploy the prod version when I create a tag (which I will do on master).

The deployment scripts look a lot like what we have tried a few paragraphs earlier (“Wrap it up”), but for the staging version we force the build_number (we cannot deploy two builds with the same number):

deploy/android-staging.sh:

deploy/ios-staging.sh:

We also need to declare a few keys as environment variables on Travis CI:

  • storeFile, storePassword, keyAlias, keyPassword: these are the keys for Flutter to sign the Android application. We store these keys in a local file, but we do not want this file in the remote repository, so we can tell Gradle to find the keys from these environment variables:
  • FASTLANE_PASSWORD: the password of the Apple account used by Fastlane to connect to App Store Connect.
  • FASTLANE_SESSION: if the Apple account uses 2FA, in addition to the previous password we need to pre-generate a session ID:(fastlane spaceauth -u account@whatever.com on our computer) and copy the result to this variable (between quotes).
  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: still in the case of 2FA, this one is a password to upload the IPA to App Store Connect. We need to generate an application password from the Apple account settings (https://support.apple.com/en-us/HT204397) and copy it to this variable.
  • MATCH_GIT_BASIC_AUTHORIZATION: it contains the Github username:token for Match to access the certificates repository. We can generate the token from Github. Also we have to encode the whole value to Base64.
  • MATCH_PASSWORD: this is the password we have been asked when we launched fastlane match for the first time. This password will be used to decrypt the provisioning profiles.

Warning! It seems that the application specific passwords expire after 8 hours, which is obviously not good for Continuous Integration. I am not aware of a workaround at the time, so I would suggest to turn off 2FA on your Fastlane Apple account: https://support.apple.com/en-us/HT204915.

Note: I did not manage to store the content of thejson_key_file in a Travis CI variable. I minified the JSON to remove the blank characters but it seems that the line breaks in the private key caused a parse error too, so I eventually added the file to our Git repository (which is private). Now I suspect I should have used quotes, let me know if you are more lucky.

We are almost there, just a last bit of configuration left!

By default the Travis CI keychain on iOS is locked. In order to setup the keychain for Match, we need a new line on top of the ios/fastlane/Fastfile. It will also force Matchreadonly (even if we already have, see “Signing — iOS”).

We just make sure that everything is still running locally, and it should now work on Travis CI as well. Not really as straightforward as we could expect, but now we can forget about it and enjoy our automatic builds! 💪

--

--