The Complete Guide to Implementing CI/CD in Flutter with GitLab

Amitsingh
10 min readMay 27, 2023

Introduction

Imagine you’re a Flutter developer, juggling code, coffee, and occasional moments of frustration. But fear not! In this article, we’re about to embark on an exciting adventure through the world of CI/CD (Continuous Integration and Continuous Deployment) using GitLab. Get ready to automate your builds, squash bugs, and level up your Flutter development game like a boss. So, grab your favorite snack and let’s dive into the magical realm of making your app development journey smoother than a hot knife through butter!

Prerequisites

Before you begin, you’ll need the following:

  • A GitLab account
  • A Flutter Project with a Git repository
  • A basic understanding of GitLab CI/CD

Note : If you would like to watch in video formate:

https://youtube.com/playlist?list=PLOoOwFsn2IJOea4oB3Zt-jtnwOKjg45sE&si=xk0yurTdXe-3nsFu

Runner

What is a runner?

A runner is a process or server that executes the various stages and tasks defined in your pipeline configuration. Runners are responsible for running your code, building your application, running tests, and deploying your application to different environments.

Understanding through an analogy

Imagine you’re in a big race, running as fast as you can, but you’re starting to get tired. Suddenly, a friendly robot shows up and starts running alongside you!

This robot is called a “runner.” It’s like your personal cheerleader and helper. It can run really fast and never gets tired, so it helps you keep going even when you feel like giving up.

Not only that, but the runner robot is also really smart. It can check your code to ensure everything is working correctly, so you don’t have to worry about making mistakes. And when you’re ready to deploy your code, the runner robot helps you do that too, so you can get your awesome project out into the world for everyone to see.

How to create a new runner?

  1. Install GitLab Runner on your system according to your OS:

2. Register your runner by running the following command in your terminal:

sudo gitlab-runner register

3. And Now in your terminal steps of question would be ask, first question would be:

Enter the GitLab instance URL : then paste this url:

https://gitlab.com/

And press enter.

4) Next Question would be:

Enter the registration token, then copy your registration token from :

left sidebar SETTING>CI/CD

GItLab/CI-CD

Paste registration token and press enter.

5) Next question would be Enter a description for the runner:

Then you can give your runner name for eg: flutter_runner or ios, flutter_web This name would be shown with your runner in gitlab, and press enter.

6) Next question would be, Enter tags for the runner (comma-separated):

tags are very important if you have a multi runner in your project which is used to uniquely identify your runner.

You can give multiple tags to your runner,for eg: flutter, flutter_ios,flutter_web and then press enter.

7) Next question would be , Enter optional maintenance note for the runner:

You can give notes according to your convenience. And press enter.

8) Now you will see :

Enter an executor: kubernetes, docker, parallels, shell, virtualbox, docker+machine, custom, docker-ssh, ssh, docker-ssh+machine, instance:

You can type shell and press enter.

Now you will see your runner is available on gitlab.

9) To start runner hit command :

sudo gitlab-runner run

Setting up GitLab CI/CD for Flutter

Follow these steps to set up GitLab CI/CD for your Flutter project:

  1. Create a .gitlab-ci.yml file in the root of your project repository. This file will define the stages and jobs of your CI/CD pipeline.
  2. Define the stages of your pipeline. A typical CI/CD pipeline consists of the following stages: lints,tests,build and deploy.
image: ghcr.io/cirruslabs/flutter
stages:
- lints
- tests
- build
- deploy

3) Define a test job. We will use our local runner which we created.

unit tests:
stage: tests
script:
- flutter test --coverage ./lib
- lcov -r coverage/lcov.info '*/__test*__/*' -o coverage/lcov_cleaned.info
- genhtml coverage/lcov_cleaned.info --output=coverage
tags:
- flutter
artifacts:
paths:
- coverage

flutter test — coverage ./lib runs the unit tests using Flutter’s built-in testing framework and generates a code coverage report. The — coverage ** flag tells Flutter to generate code coverage information while running the tests, and ** ./lib specifies the directory where the tests are located.

dart lcov -r coverage/lcov.info ‘/__test__/*’ -o coverage/lcov_cleaned.info uses the lcov tool to filter the code coverage report and remove any files or directories that match the pattern /__test__/*. This is useful because it removes coverage information for test files themselves.

genhtml coverage/lcov_cleaned.info — output=coverage generate** an HTML report from the filtered coverage data and saves it in the coverage directory.

NOTE:

Here you can get an error not being able to find lcov if you didn’t install lcov in your system where you created runner. So install from brew.

brew install lcov

4) Now we will define our next stage that is build:

android:
stage: build
script:
- flutter build apk --release --build-number ${CI_JOB_ID:0:8}
- flutter build appbundle --release --build-number ${CI_JOB_ID:0:8}
- sudo bundle install
- cd android && bundle exec fastlane move_files
artifacts:
paths:
- build/artifacts/
tags:
- flutter
  • The script section contains the commands that will build the app, which will result in an APK and App Bundle file, which are files used to install the app on an Android device. The artifacts section specifies where the APK and App Bundle files should be stored after the build is complete.
  • cd android && bundle exec fastlane move_files This command changes the directory to the android folder and executes the fastlane script called move_files.
  • We will define our move_files lane once we complete the .gitlab-ci.yml file .

5) Now we will define a deploy job that deploys your app to a target environment. For example, you can deploy to the Google Play Store or the Apple App Store:

playstore:
stage: deploy
dependencies:
- android
script:
- cd android && bundle exec fastlane playstore_internal_release
tags:
- flutter

The “dependencies” field specifies that this stage depends on the “build” stage, which should have already completed successfully.

The “tags” field specifies that this pipeline should be run on specific runner that have been tagged with “flutter”. This ensures that the pipeline is run on runner that have the Flutter SDK installed and configured properly, so that the build and deployment process can proceed smoothly.

In this script -cd android && bundle exec fastlane playstore_internal_release we change the current directory to the android folder and execute our script playstore_internal_release lane.

In Next steps we will define our fastlane file where we will define our 2 lane:

  • move_files
  • playstore_internal_release

Now let’s take a overview of our code in .gitlab-ci.yml file:

image: ghcr.io/cirruslabs/flutter
stages:
- lints
- tests
- build
- deploy
lints:
stage: lints
before_script:
- bash reconfigure.sh
script:
- flutter analyze
tags:
- flutter
unit tests:
stage: tests
script:
- flutter test --coverage ./lib
- lcov -r coverage/lcov.info '*/__test*__/*' -o coverage/lcov_cleaned.info
- genhtml coverage/lcov_cleaned.info --output=coverage
tags:
- flutter
artifacts:
paths:
- coverage
android:
stage: build
script:
- flutter build apk --release --build-number ${CI_JOB_ID:0:8}
- flutter build appbundle --release --build-number ${CI_JOB_ID:0:8}
- sudo bundle install
- cd android && bundle exec fastlane move_files
artifacts:
paths:
- build/artifacts/
tags:
- flutter

playstore:
stage: deploy
dependencies:

- android
script:
- cd android && bundle exec fastlane playstore_internal_release
tags:
- flutter

FASTLANE

With Fastlane, you can define tasks and actions that automate the various stages of your app development pipeline. For example, you can define tasks to build your app, run automated tests, generate screenshots, upload your app to beta testing platforms, and submit your app to app stores for review and release.

For more information: https://docs.fastlane.tools/

As above we say that we have to define 2 lanes in our fastlane file, one for move files at one place and for deployment in internal release at play store.

Now let’s define our first lane: move_lane

If you don’t have a fastlane installed in your system where you create your runner then install it first.

sudo gem install fastlane
  • It will ask for a password then give your system password.

Once you installed fastlane in your system we are good to go to edit our fastfile to make deployment on playstore/appstore.

default_platform(:android)

platform :android do

desc "move all the files to separate folder to upload them as artifacts"

lane :move_files do

version = File.read(File.join(File.dirname(__FILE__), '..', '..', 'pubspec.yaml')).match(/version: (.+)/)[1]
buildCode = version.split('+')[0]

buildNumber = ENV['CI_JOB_ID']

if buildNumber == nil
buildNumber = version.split('+')[1]
end

job = ENV['CI_JOB_ID']
if job == nil
job = '-1000'
end


buildNumber = buildNumber.slice(0, 8)

file_prefix ="Academicmaster-#{git_branch}-#{buildCode}+#{buildNumber}".split("\n").first
sh("cd .. && cd .. && mkdir -p build/artifacts")
sh("mv ../../build/app/outputs/flutter-apk/app-release.apk ../../build/artifacts/#{file_prefix}.apk")
sh("mv ../../build/app/outputs/flutter-apk/app-release.apk.sha1 ../../build/artifacts/#{file_prefix}.apk.sha1")
sh("mv ../../build/app/outputs/apk/release/output-metadata.json ../../build/artifacts/output-metadata.json")
sh("mv ../../build/app/outputs/bundle/release/app-release.aab ../../build/artifacts/#{file_prefix}.aab")

dataToWrite = buildCode.split("\n").first + ',' + buildNumber.split("\n").first + ',' + job + ',' + sh("cd .. && cd .. && pwd | tr -d '\n'") + '/build/artifacts/' + file_prefix.split("\n").first + ".aab"
sh("echo #{dataWrite} > ../../build/artifacts/metadata.dat")
end

default_platform(:android)

  • In our first line we set the default platform android for our script .
platform :android do

desc "move all the files to separate folder to upload them as artifacts"

lane :move_files do

Here we are saying that inside move_files lane code would be executed only for android platform where move_files is our lane name and des is description.

 version = File.read(File.join(File.dirname(__FILE__), '..', '..', 'pubspec.yaml')).match(/version: (.+)/)[1]
buildCode = version.split('+')[0]

Here we read the version number of the app from the “pubspec.yaml” file and assign it to the “version” variable. The regular expression is used to extract the version number from the file.

And after that we extract the build code from the version number by splitting it at the ‘+’ character and taking the first part.

file_prefix ="Academicmaster-#{git_branch}-#{buildCode}+#{buildNumber}".split("\n").first

Here we create a file prefix for the artifacts by concatenating the build code, build number, and the current Git branch.

sh("cd .. && cd .. && mkdir -p build/artifacts")
sh("mv ../../build/app/outputs/flutter-apk/app-release.apk ../../build/artifacts/#{file_prefix}.apk")
sh("mv ../../build/app/outputs/flutter-apk/app-release.apk.sha1 ../../build/artifacts/#{file_prefix}.apk.sha1")
sh("mv ../../build/app/outputs/apk/release/output-metadata.json ../../build/artifacts/output-metadata.json")
sh("mv ../../build/app/outputs/bundle/release/app-release.aab ../../build/artifacts/#{file_prefix}.aab")

Now we execute a shell command to create a directory “build/artifacts” if it does not exist.

And after that we move the generated Android APK and AAB files, as well as the output metadata file, to the “build/artifacts” directory with the specified file prefix.

dataToWrite = buildCode.split("\n").first + ',' + buildNumber.split("\n").first + ',' + job + ',' + sh("cd .. && cd .. && pwd | tr -d '\n'") + '/build/artifacts/' + file_prefix.split("\n").first + ".aab"
sh("echo #{dataToWrite} > ../../build/artifacts/metadata.date")

Here dataToWrite string is written to a file called metadata.dat in the build/artifacts directory using sh(“echo #{dataToWrite} > ../../build/artifacts/metadata.dat”). The echo command writes the string to standard output, and the > operator redirects the output to the specified file, creating it if necessary.

Now we will define our next lane that is playstore_internal_release:

lane :playstore_internal_release do

comps = File.read("../../build/artifacts/metadata.dat").split("\n").first.split(",")
version = comps[0]
build = comps[1]
job = comps[2]
aabPath = comps[3]

supply(
track: 'internal',
version_name: build + " (" + version + ")",
version_code: build,
aab: aabPath,
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
  • The comps variable is assigned the value of an array that is generated by splitting the first line of the file metadata.datinto comma-separated values using the split(“,”) method. The file is read using File.read(“../../build/artifacts/metadata.dat”) metadata.dat is a file where we store all our required files by using move_files lane.
  • And after that we extract version number , build number , job id and aabPath which represents the absolute path to the AAB file generated in the previous lane.
  • The supply method is used to upload the AAB file to the Google Play Store. It takes several arguments: version_name,version_code and aab etc

Create AppFile in android/fastlane folder and paste this code :

json_key_file(“secrets/service-account-user.json”)# Path to the json secret file — package_name(“com.academic.master”) # e.g. com.example.app

Once your pipeline executed successfully your app will be deploy on play store internal release you can check out there:

How can you run lane individuality?

To run a Fastlane lane, you can use the fastlane command followed by the name of the lane you want to execute. Here’s an example: Go to android directory and then :

fastlane move_files

What is the stage?

Stages are defined in the CI/CD configuration file, and each stage is executed in the order defined in the file. The output of one stage typically serves as the input to the next stage. For example: The output of the build stage can be used as input for the test stage, and deploy stage like suppose If you want to Upload your application on play store and app store then firstly you will execute build stage and run all the required scripts and once your that stage is executed successfully you can save your apk by using artifacts field and you can make deploy stage dependent on build stage once this stage is completed then deploy stage will execute.

For more information about stages: https://docs.gitlab.com/ee/ci/yaml/#image

What is the script?

The “script” field can include any valid command-line commands or scripts that can be executed by the pipeline runner. This might include commands to install dependencies, compile code, run tests, deploy the application, or perform other tasks required for the pipeline stage. The “script” field is an essential part of the pipeline configuration since it defines the specific actions to be taken during each stage of the pipeline.

For more information about script : https://docs.gitlab.com/ee/ci/yaml/#script

What are the artifacts?

The “artifacts” field typically refers to files that are produced during a particular stage of the pipeline and that should be saved for future use. These files might include build artifacts, test reports, code coverage reports, or other files that are useful for debugging or troubleshooting the application.

For more information : https://docs.gitlab.com/ee/ci/yaml/#script

CONCLUSION:

With GitLab CI/CD, you can automate your Flutter app’s testing and deployment processes. By setting up a pipeline that runs your tests and deploys your app automatically, you can ensure that your code is always in a stable state and ready for production.

LinkeDin: https://www.linkedin.com/in/amit-singh-023055193/

StackOverflow: https://stackoverflow.com/users/13051247/amit-singh

Github: https://github.com/amitsingh6391

--

--