Getting Started with CI/CD for Flutter

Getting Started with CI/CD for Flutter

The idea behind a typical CI/CD pipeline is very simple. Whenever new code is pushed to the centralized version control management system, we want to ensure that the code won't break any existing features or introduce any new bugs. Also it should be re-producible, which mean from those pieces of code, we are able to to make a stable, installable build, especially for mobile platforms where everything can only be verified by installing the binaries into destination operating system, in our case, iOS and Android devices.

At InvestIdea, we use a very simple pipeline to ensure the CI/CD workflow for mobile platforms works just-enough for us, while maintaining the flexibility of the whole pipeline, this allows us to help other teams, especially QA and our end users, in an early phase, the customers, to be able to try and test the new versions of applications as soon as possible.

Photo by Artur Shamsutdinov / Unsplash

How Does It Work

As introduced in our previous post, Docker in Practices, we extensively use Docker and GitLab Runners in our centralized Kubernetes cluster to run all our CI/CD workloads. The mobile platform does not go outside. Based on the requirements of specific projects, we use one of the following Git workflows:

Simple Workflow

The Simple one relies on one primary principal:

Maintain only the master branch with the latest, stable, production-ready code.

By using just the master branch, the Git workflow becomes very easy to implement:

  1. Base source code is in the master, or latest convention, main branch.
  2. Whenever we need to fix a bug, or develop a new feature, our engineers should create a branch from the latest master, a new branch named features/something or fixes/something.
  3. During the development of the issues, the engineer should keep his local branch up-to-date with the current master as much as possible, by using git pull --rebase. This will help to avoid conflict in a large team.
  4. Whenever the development is done, the engineer creates merge-request in GitLab to merge the branch into master. The CI/CD workflow is configured to automatically run against the merge request with at least code_quality and test jobs to ensure it won't break anything.
  5. Code review and further analysis are done before the code is merged. During the process, the engineer may continue pushing new commits to the branch, and updating the merge request as well, while rebasing to respect the latest master.
  6. After the code is carefully reviewed, it is merged and another pipeline is triggered against master which will build the APK for Android, or the IPA for iOS, the output artifacts are copied to our centralized storage hosting, where our QAs can easily grab them and install them on the test devices.
  7. Note that, by using the Kubernetes executor of GitLab, we currently have no elegant way to build iOS IPA files, so we just use a promising third party service named CodeMagic, it offers a good free package that can be used with small projects to build IPA files, and upload them to App Store Connect, then the build will be available as a TestFlight build later, so our QAs with proper TestFlight can be able to access and install them.
  8. Our source utilizes flutter build-time arguments to be able to pass any arbitary variables to the source code itself. By using --dart-define=VAR_NAME=VAR_VALUE we can instruct the code to get the value by String.fromEnvironment("VAR_NAME") and separate the builds by environments, like staging or production, or eventually build them in parallel.
  9. For production builds, instead of relying on the master branch in Git, we use tags. Whenever a new tag is pushed, the same pipeline is triggered but with the above variable, the output artifacts will be the production ones. It means the API endpoints and other configurations are production-specific. This is done without any changes to the source code itself, as the default configuration is to point to the staging environment. This approach can be extended to multiple environments at the same time.
  10. As we use just a single branch, quickly resolving issues and short iterations of software development lifecycle is a must. Magically, it fits perfectly with our Agile-based workflow and Scrum framework where our team runs in 2-weeks sprints during the whole project.

Multiple Branches Workflow

This workflow is suitable for a complex team with complex environments and release schedules. It is based on A successful Git branching model article.

  1. We maintain the master branch as production-grade code.
  2. Development is started by branching the development branch from master. From this branch, all early development should be based by creating features/something and keeping git rebase continuously against development.
  3. After a few iterations, like Sprints, team can move to another phase where we decide to prepare for a new release. It can be done only if we agree that all features are completed in terms of business logic. Then we create a release branch from current development. The only acceptable commits in this branch, are the ones which fix the existing features and to complete them.
  4. Whenever QAs team is done with the release build, the code is merged into master, where a new build for Google Play, and also the App Store will be done and submitted for review. By that time, the existing release branch is deleted and everything starts over again with the development branch.
  5. In case of critical bugs, the team must create a hotfix/something branch from the latest master code, do the fixes as quickly as possible. After testing is done, merge it to master, and to the development and/or release branch.

GitLab Examples

A typical GitLab pipeline is defined by the following stages:

stages:
  - sync-dependencies
  - test
  - build
  - deploy

Where sync-dependencies just do flutter pub get and cache the flutter dependencies in our centralized cache. It will fasten the subsequence stages later.

test stage includes code-quality and test jobs, where they run:

flutter test --machine --coverage > tests.output
sonar-scanner -Dsonar.projectKey=$PROJECT_NAME -Dsonar.projectVersion=$CI_COMMIT_SHORT_SHA -Dsonar.sourceEncoding=UTF-8 -Dsonar.host.url=$PLATFORM_SONAR_HOST -Dsonar.login=$PLATFORM_SONAR_TOKEN -Dsonar.sources=lib -Dsonar.tests=test

metrics lib -r codeclimate  > gl-code-quality-report.json

As you can see, the commands send the code quality and metrics to our SonarQube instance using credentials in environment variables.

The build stage is quite obvious:

flutter build apk --split-per-abi --no-tree-shake-icons
flutter build appbundle --no-tree-shake-icons --dart-define="VAR_NAME=VAR_VALUE"
flutter build ipa --no-tree-shake-icons --export-options-plist=fastlane/ExportOptions.plist

After that, the deploy stage is responsible for copying artifacts to our centralized storage for teams to grab from.

In order to run the flutter command with sonar-scanner, a sample Dockerfile can be used:

FROM cirrusci/flutter:2.10.0

WORKDIR /opt/sonar

RUN wget -q https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.6.2.2472-linux.zip

RUN unzip sonar-scanner-cli-4.6.2.2472-linux.zip && rm sonar-scanner-cli-4.6.2.2472-linux.zip

ENV PATH=$PATH:/opt/sonar/sonar-scanner-4.6.2.2472-linux/bin

This image is built and used in all jobs inside the GitLab CI/CD workflow.

Conclusion

Above are at least two successful types of Git workflow that we have applied in our company. Because each customer, each client, each project, each product, and even each team has different requirements and expertise that will drive the success of a Git workflow, we recommend software engineers try and adapt to their capabilities as much as possible. Our success cases may not fit with you, but they are good examples of how we iterate during recent years with Flutter as well as mobile development in general. If you find our approach interesting, join us today!