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.
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:
- Base source code is in the
master
, or latest convention,main
branch. - 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 namedfeatures/something
orfixes/something
. - 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 leastcode_quality
andtest
jobs 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
master
. - 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. - 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 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 byString.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
master
branch in Git, we usetags
. 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
master
branch as production-grade code. - Development is started by branching the
development
branch frommaster
. From this branch, all early development should be based by creatingfeatures/something
and keepinggit rebase
continuously againstdevelopment
. - 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 currentdevelopment
. 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
release
build, the code is merged intomaster
, where a new build for Google Play, and also the App Store will be done and submitted for review. By that time, the existingrelease
branch is deleted and everything starts over again with thedevelopment
branch. - In case of critical bugs, the team must create a
hotfix/something
branch from the latestmaster
code, do the fixes as quickly as possible. After testing is done, merge it tomaster
, and to thedevelopment
and/orrelease
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!
————––
𝐈𝐧𝐯𝐞𝐬𝐭𝐢𝐝𝐞𝐚 𝐓𝐞𝐜𝐡 - 𝐁𝐫𝐢𝐧𝐠 𝐢𝐝𝐞𝐚𝐬 𝐭𝐨 𝐋𝐢𝐟𝐞
📞 Hotline: (+84) 886 996 436
📧 Email: hello@investidea.tech
🌐 Website: investidea.tech
#Investidea #InvestideaTech #BringIdeasToLife