Recently, I’ve worked on automating some internal processes for building and releasing Flutter applications. Part of this effort included utilizing Fastlane with a Continuous Integration/Delivery platform to build and deploy the app. This blog post will outline the process I followed to run the build on CircleCI after I had configured Fastlane to build and deploy the application from my local machine.
I chose to use CircleCI as a proof of concept platform because it integrates easily with Github, supports MacOS build images, provides a library to support Flutter, and because we were already using it for other projects. However, Fastlane integrates with various CI/CD platforms, and the general concepts for configuring the build covered here should apply across the board.
This blog is part 3 of a three-part series exploring automating Flutter CI/CD on CircleCI. Part 1 covers setting up Fastlane to build and deploy applications, part 2 outlines automating screenshot capture and test runs, and this post discusses configuring CircleCI to automate these processes.
CircleCI has extensive documentation, and their getting started guide is a good starting place. However, for the sake of coherency, I will summarize the basic terminology that I reference throughout this guide below.
A job defines an executor, which may be a virtual machine or a container, along with a list of steps to perform to complete a task. Reusable steps or configurations can be encapsulated in a component called an orb, which can be imported and referenced. CircleCI maintains some common orbs, but many additional orbs are available through the community.
Workflows orchestrate the execution of jobs. In particular, a workflow can define dependencies between jobs, schedule when jobs execute, and define dependencies between the jobs needed to fulfill a task.
Environment variables can be set on a single project and can also be defined once in a context. Then, they can be referenced across multiple projects. This can be used to manage secrets, following the CircleCI security recommendations.
To create my initial configuration, I followed the steps in the CircleCI “Getting Started Guide” to generate a new configuration file for my project, but I didn’t commit it immediately. The CircleCI workflow is triggered each time commits are pushed to the remote repository. I wanted to keep an eye on both my storage usage and build minutes to avoid unnecessary usage of my available credits.
CircleCI workflows support a special job type called approval. This type of job places the workflow in an
On Hold status until someone manually approves it to continue. While the workflow is
On Hold, it will not consume any build minutes.
I added an approval job named
build-approval. Then, I updated the
say-hello job to depend on
build-approval and committed my basic configuration file.
version: 2.1 jobs: say-hello: docker: - image: cimg/base:stable steps: - checkout - run: name: "Say hello" command: "echo Hello, World!" workflows: say-hello-workflow: jobs: - build-approval: type: approval - say-hello: requires: - build-approval
CircleCI picked up the configuration, validated the YAML, and set the workflow status to
On Hold. This verified the basic integration between Github and CircleCI without needing to approve the job.
I didn’t want to inadvertently leave multiple
On Hold workflows running, so I went to the project settings and set
Auto-cancel redundant workflows to true. This setting will cancel prior workflows on the branch when a new one is triggered. By design, it does not work on the default build branch, but it will work on all other project branches.
Now that I had the basic workflow setup in place, I was ready to add project-specific configuration. Our Flutter app runs on both Android and iOS devices, and I had already created two local Fastlane configuration files with a deployment lane.
The deployment lane runs tests, captures app screenshots, builds the application for release, then deploys the app to the App Store. These lanes can execute simultaneously, so I configured separate jobs for each app.
For iOS, I had locally configured Fastlane to run and upload to the app store, but the build was not optimized for running on CI. I needed to make a few changes first.
upload_to_testflight command, I set
skip_waiting_for_build_processing: true. This change reduced the number of build minutes used by no longer waiting for the build processing to complete.
While I was experimenting with the configuration on CI, I did not want to inadvertently upload partial or invalid builds to TestFlight, so I temporarily commented out the call to
upload_to_testflight. I also did not want to impact the app store, so I set the
verify_only: true parameter on the
upload_to_app_store action. Once everything else was working, uploads to both were re-enabled.
Finally, I set a Fastlane action, which sets up the keychain and helps with CircleCI setup_circle_ci by adding a new
before_all lane in my iOS fastfile.
before_all do setup_circle_ci end
With these Fastlane tweaks, I was ready to move on to the CircleCI-specific configuration.
To create the iOS job, the steps are fairly simple. I need a macOS executor with Xcode. Then, I need to install flutter and run the Fastlane deployment lane.
CircleCI has a macOS guide that provides in-depth explanations of the available options for macOS executors. I decided to start out with the simplest and latest configuration, macOS with 14.2.0. Then, I needed to install Flutter on the machine. CircleCI maintains a circleci/flutter orb to install the SDK and project dependencies.
Some of the commands, such as
install_sdk_and_pub use caching, which will use up storage space on the CircleCI plan. This is stated in the documentation, but I missed that initially and it surprised me when my storage credit usage suddenly went up by over 1 GB. I learned that it was important to pay attention to the cache settings when using orbs.
The integration tests and screenshot tests in my application also handle starting and stopping the simulators, so I did not need to configure them. The CircleCI/macOS orb can be used for controlling simulators if external configuration is needed.
I also wanted to make sure that things generally worked the way I expected by keeping the build logs and checking the snapshots were generated correctly. To do this, I need to persist them to the workspace. The
store_artifacts command from CircleCI can be used to specify a path and a destination to keep artifacts from the job.
To save the screenshots, I just had to add the path where they are stored. For the build output, the
setup_circle_ci action allows a directory to be specified for fastlane output by setting an environment variable
FL_OUTPUT_DIR which will define the path for the output. This can be set directly on the job since it is specific to this workflow.
At this point, I have a file that should be able to run the app tests through the Fastlane configuration, but I know the deployment will not work because I haven’t set the credentials yet. I created a temporary lane in Fastlane named
all-tests that ran every step that did not require credentials and plugged it into my config.
# .circleci/config.yml version: 2.1 orbs: flutter: circleci/[email protected] jobs: ios-deployment: macos: xcode: 14.2.0 environment: FL_OUTPUT_DIR: output steps: - checkout - flutter/install_sdk_and_pub: flutter_version: 3.3.10 - flutter/install_ios_gem - run: name: Fastlane ios deployment command: bundle exec fastlane all-tests working_directory: ios - store_artifacts: path: output - store_artifacts: path: ios/fastlane/screenshots/en-US workflows: deployment: jobs: - build-approval: type: approval - ios-deployment: requires: - build-approval context: - ios-fastlane
This let me check that the tests ran, including the snapshot generation, and served as a stable base to configure the build and deploy steps.
With the basic test run in place, I needed to set credentials on CircleCI to sign the code and upload to the app store.
Code signing is configured through Match in Fastlane, and the certificates are in a git repository. To allow CircleCI to access the certificate repository, I needed to set up the ssh key for an authorized user. I followed the iOS codesigning guide to complete this step.
The app signing process also requires the password for Match. The fastlane continuous integration documentation indicates this should be set as the
MATCH_PASSWORD environment variable.
I wanted to use this password across multiple projects, so I set it in a context named
ios-fastlane. In CircleCI I navigated to Organization Settings -> Contexts -> Create Context. I named the context, then added the environment variable and value.
Now, I could sign the app, but I also needed to connect to the app store to upload it. The app store authorization uses the App Store Connect API as well as a 2FA (Two-Factor Authentication) enabled account, so I needed to set both types of access.
My Fastlane file uses the
app_store_connect_api_key action for App Store Connect, with the login information specified by the
key_id, and the
To keep the
key secret, it needed to be set as an environment variable. I decided to also set the
issuer_id the same way in the context, so that they can be easily reused across projects, although this wasn’t strictly necessary.
These variable names are required by the action to be:
The appfile in the project contains the
apple_id for uploading to the app store. This must be a 2FA-enabled account, and it needs to be added as an environment variable named
These were all the variables that needed to be set for our application, but a few others may need to be set depending on the specific project configuration. Those are outlined in the Fastlane continuous integration documentation.
With all of this setup, I went back to the job configuration and switched the Fastlane command to
bundle exec fastlane deployment. After the build succeeded, I restored the code to deploy to TestFlight and switched
verify_only back to
false in the
I waited to build with a full release until I was also done with the Android configuration.
The Android configuration is similar to the iOS configuration. Before beginning, I updated the fastfile to prevent inadvertent uploads by setting
verify_only:true in the
setup_circle_ci setting only applies to iOS, so it does not need to be set in the Android fastfile.
For building Android apps, I needed to have the ability to start and run emulators, run the integration tests, and collect screenshots. The Android machine utilizes nested virtualization, so they are able to support x86 emulators with reasonable performance. This image is built on Linux and comes pre-installed with the Android platform and sdk manager.
Next, I had to install Flutter, which I was able to do with the CircleCI/Flutter orb that I used for iOS. Note that since this is a different job, the dependencies will all be cached separately from the iOS job.
When I configured iOS, I let the integration tests handle starting and stopping the emulators. I tried this with Android as well, but the first time the application screenshots were collected, they included a notification that the system was unresponsive. Rather than adjusting the emulator startup logic in the tests to optimize it for running on CircleCI, I used the CircleCI/Android orb to create and start the emulators prior to the test run.
android/start-emulator command is designed to launch
gradle tests by default, so I set
post-emulator-launch-assemble-command: ‘’ to override this behavior for Flutter. I also used a generic device with lower resource requirements.
The way that I implemented this worked for our application because I am only running one or two emulators at the most, but it required some experimentation to find the right balance for performance and run time. To troubleshoot this, I made sure to persist the screenshots the same way I had for iOS. When I had everything configured, the android configuration (without the iOS code) was:
# .circleci/config.yml version: 2.1 orbs: android: circleci/[email protected] flutter: circleci/[email protected] jobs: android-deployment: executor: name: android/android-machine resource-class: large tag: 2022.12.1 steps: - checkout - flutter/install_sdk_and_pub: flutter_version: 3.3.10 - flutter/install_android_gradle - flutter/install_android_gem - android/create-avd: avd-name: Phone_5.1 install: true system-image: system-images;android-31;default;x86_64 additional-args: -d 50 # This is a 5.1in WVGA device - android/start-emulator: avd-name: Phone_5.1 post-emulator-launch-assemble-command: '' restore-gradle-cache-post-emulator-launch: false - run: name: Fastlane - android deployment command: bundle exec fastlane deployment working_directory: android - android/kill-emulators - store_artifacts: path: android/fastlane/metadata/android/en-US/images/phoneScreenshots workflows: deployment: jobs: - build-approval: type: approval filters: branches: only: main - android-deployment: requires: - build-approval context: - android-fastlane
To build the app for Android deployment, I needed to set up the credentials for app signing and deployment. Unlike the iOS configuration, the Android build tools don’t integrate directly with the environment variables. However, they can still be used to set sensitive information, and then it can be output to a file during the build, as long as it is not printed on the console.
I began by configuring app signing. The keystore and app reference to the keystore that were created during the app signing setup had to be added to CircleCI.
First, I had to convert both files to base64, per the CircleCI documentation. I did this by running
base64 my_keystore.jks and
base64 key.properties from a terminal. Then, I created a new context in CircleCI named
android-fastlane and added new environment variables
To deploy the app, I needed to configure the JSON file that was generated for Supply. I added this in a variable named
SUPPLY_JSON_KEY_DATA. This didn’t need to be encoded to base64.
Now that I had these variables, I would need to be able to get the values when the build ran. To do so, I can pipe them to the output files that are required by the Android build process and defined in the Appfile used by Supply. Because they are secrets, these files must not be persisted, and their contents must not be visible from the console.
I added the following steps to the job, prior to running the fastfile deployment:
- run: echo "$PLAY_STORE_KEYSTORE" | base64 --decode > android/app/key.jks - run: echo "$PLAY_STORE_KEYSTORE_INFO" | base64 --decode > android/key.properties - run: echo "$SUPPLY_JSON_KEY_DATA" > android/fastlane/play-store-credentials.json
With all of this setup, I went back to the job configuration and switched the Fastlane command to
bundle exec fastlane deployment. After the build succeeded, I switched
verify_only back to
false in the
Now I was done with both the Android configuration and the iOS configuration, I ran a full release. I validated that the app was updated in both stores and that screenshots were uploaded as well. I wasn’t quite done with the configuration, though. I needed to make one more update.
When I merge back to main, the CircleCI configuration will be included as other developers branch and merge. This workflow is specific to deployment and should not run on every branch of code.
We wanted to release directly from our main branch, so I updated the
build-approval job. I renamed it to
deployment-approval and set it to only run on the main branch.
- deployment-approval: type: approval filters: branches: only: main
Since the other jobs both require the
deployment-approval step, they are also not triggered from other branches. With this change in place, I was ready to take my code back to the main branch safely.
While working on the CircleCI configuration, I learned a lot about app signing, the app release process and store guidelines, and emulator configuration for both Android and iOS.
The configuration file in this blog post is a good starting point for running the full deployment, but it can and should be optimized for different processes and branch runs.
In particular, running integration tests and capturing screenshots is a complex topic that should be optimized to meet specific organizational needs. I look forward to spending more time tweaking the configuration and exploring CircleCI features to improve the build time and create more robust output.
Automating the release and deployment of our Flutter application was very helpful to us as a team. Prior to this, our builds were not running on CI, so just getting a build running with tests added value even before the release and deployment were added.
Failures from merges became more visible and having screenshots as a build artifact made layout issues easier to identify and test across a variety of devices. When the deployment steps were added, our release process became more robust. The secrets and keys for releasing did not need to be shared for someone new to complete a deployment.
Overall, it just saved us time. For our small application, running the entire deployment process for both app stores, including screenshot generation, takes under 15 minutes. A developer just needs to approve the release, then monitor the build.
For any team doing Flutter development, I recommend taking the time to configure an automated deployment process. The consistency and reliability it adds is worth the initial investment of time and effort.