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.
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:
The Simple one relies on one primary principal:
Maintain only the
masterbranch with the latest, stable, production-ready code.
By using just the
master branch, the Git workflow becomes very easy to implement:
- Base source code is in the
master, or latest convention,
- 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
- 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.
- 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
testjobs to ensure it won't break anything.
- 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
- After the code is carefully reviewed, it is merged and another pipeline is triggered against
masterwhich 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.
- 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.
- Our source utilizes
flutter build-time argumentsto be able to pass any arbitary variables to the source code itself. By using
--dart-define=VAR_NAME=VAR_VALUEwe 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.
- For production builds, instead of relying on the
masterbranch 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.
- 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.
- We maintain the
masterbranch as production-grade code.
- Development is started by branching the
master. From this branch, all early development should be based by creating
git rebasecontinuously against
- 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
releasebranch from current
development. The only acceptable commits in this branch, are the ones which fix the existing features and to complete them.
- Whenever QAs team is done with the
releasebuild, 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
releasebranch is deleted and everything starts over again with the
- In case of critical bugs, the team must create a
hotfix/somethingbranch from the latest
mastercode, do the fixes as quickly as possible. After testing is done, merge it to
master, and to the
A typical GitLab pipeline is defined by the following stages:
stages: - sync-dependencies - test - build - deploy
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
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.
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-184.108.40.2062-linux.zip RUN unzip sonar-scanner-cli-220.127.116.112-linux.zip && rm sonar-scanner-cli-18.104.22.1682-linux.zip ENV PATH=$PATH:/opt/sonar/sonar-scanner-22.214.171.1242-linux/bin
This image is built and used in all jobs inside the GitLab CI/CD workflow.
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!