how to quickly set up GitLab CI

Hello! I’m Alexander Omelyanenko, Flutter developer at AGIMA. Recently I needed to quickly set up CI/CD on a Flutter project. The few tutorials I found online on this topic either had examples that didn’t work, or were confusing and just plain poor quality. But I still got some idea. Plus I asked questions to my colleagues. Making mistakes along the way, I finally set up CI/CD on my project. But then clear instructions would be very useful to me. So I decided to write it myself without delay. Today I’m sharing it with you and I hope this instruction will make life easier for those who are setting up CI/CD on a Flutter project right now.

Briefly about CI/CD

The article is intended for the Middle+ level, so I’ll talk briefly about CI/CD, simply because it’s the way it is. CI (Continuous Integration) and CD (Continuous Delivery) is a development methodology with which you can release new versions of applications more often and more efficiently, automate routine processes and save time on testing and building .apk, .abb and .ipa files .

Why we chose GitLab CI/CD

We simply compared GitLab CI/CD with other popular platforms for Flutter projects – GitHub and CodeMagic. And GitLab won in terms of three indicators 🙂

Terminology

You will come across these terms throughout the instructions:

  • Runner – a process that executes tasks in a pipeline. GitLab CI/CD can use both built-in and external Runners.

  • Pipeline – a chain of tasks that are performed in a certain order. In the context of GitLab CI/CD, this is a set of steps that are performed after each MR in a repository.

  • Stage — a stage in a pipeline that consists of several tasks. For example, phases may include build, test, package, and deploy.

  • Job – a task that is performed at a certain stage. For example, a task might involve building code, running tests, or publishing a package.

  • Artifact – an artifact that is created as a result of completing a task. For example, this could be compiled code (assembly), test results, or documentation.

Environment

We are almost ready to move on to setting up CI/CD. But first, let’s look at the environment. We need to understand where the repository will be stored, where Runner will be installed, and whether it makes sense to use Docker.

What do we need for this?

Runner, which launches Pipelines, the GitLab repository where the code will be located, and, of course, the project itself.

  • Runner can be installed on a server or on your local machine. Important for those who work in a team and chose the second option: Pipelines will run on your machine every time someone runs them.

  • The repository can be installed on the server, stored locally on your machine, or left as is on the GitLab server itself.

  • Isolated environments for Pipelines CI/CD can be created using Docker. This can be useful if you need to run jobs in the same environment on different servers. But if you are not familiar with Docker and don’t have time to learn it, everything will work without it.

Introductory information about the project from this article: We used a remote server on MacOS, installed Runner on it, did not install a repository and did not use Docker. Therefore, keep in mind that below there will be instructions for setting up CI/CD specifically for such an environment.

Runner installation

Now we are ready. First, let’s install Runner. To do this, go to GitLab→Settings→CI/CD.

Let’s move on to Runners.

All the Runners of our project will be here. Click New project runner.

On the page that opens, select the platform – in our case it is MacOS.

Adding tags. We added two – ci and cd. You can create more at your discretion. Tags can be useful for grouping and controlling access to tasks. We used tags simply for convenience.

You can leave the remaining fields blank. At the bottom of the page click Create runner. After this, the Runner is created, but only in the GitLab project. Now we need to install Runner on our machine, to do this follow the link.

We follow the instructions: select the operating system, architecture, copy the command and paste it into the terminal.

Next we can install the Runner of our project. To do this, copy the command and enter it into the terminal.

After that we need to specify the URL. We take it directly from the proposed option.

Next, enter a name for Runner. You can enter a project name.

Next, we select the performer, in our case it is Shell.

After this, our Runner should install. If you did everything correctly, you will see a successful installation message in the terminal: “Runner has been successfully registered.” You can run it. But, if it is already running, the configuration should automatically reload. The configuration (with authentication token) is saved in /Users/user/.gitlab-runner/config.toml.

Next we need to launch Runner. To do this, enter the command gitlab-runner run into the terminal and after that we can go to the page of our Runner.

Here we can see that our Runner is running and ready to go.

Preparing to set up CI/CD

Now we are ready to configure our CI script. To begin, create a .gitlab-ci.yml file in the root folder of the project. GitLab will recognize this file and execute the instructions written in it. But before you write instructions, you need to decide Stages And WorkFlow.

Stages

We have added 3 Stages:

  1. static

  2. test

  3. build

static — here we add all checks related to the correctness of code writing, checks for unused files, etc.

test — there will be commands related to testing here, depending on what tests you are using.

build — here we add commands for assemblies. We will collect .abb, .apk and .ipa.

In GitLab we will see it like this:

At the very beginning of the .gitlab-ci.yml file we add:

stages:
  - static
  - test
  - build

WorkFlow

WorkFlow is the condition under which our instruction will be triggered. In this example, we want the Pipeline to run when the developer does an MP. To do this, add after Stages:

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: always

Let’s figure out how we will add tasks to our file. One task is Job, and it looks like this:


job_name:
  stage: 
  before_script:
    - 
  script:
    - 
  tags:
    -

Each Job has a name and parameters. In the process we will add different parameters and consider them individually. There are also parameters for adding before_script, script, after_script commands. Here, I think, everything is clear. There are also Tags – tags that we added when installing Runner.

Setting up CI

Now let’s look at some commands for Static checks.

  1. dart-metrics-analyze


dart-metrics-analyze:
  stage: static
  interruptible: true
  before_script:
    - flutter pub get
  script:
    - flutter pub run dart_code_metrics:metrics analyze --fatal-style --fatal-performance --no-fatal-warnings --reporter=console lib
  tags:
    - ci

This Job is for analyzing Dart code using the dart_code_metrics plugin. It runs in the Static stage and can be interrupted (interruptible: true).

Before executing the script, the task executes the command flutter pub get to update Flutter project dependencies.

The script then does the following:

  • Executes the command flutter pub run dart_code_metrics:metrics analyze –fatal-style –fatal-performance –no-fatal-warnings –reporter=console libwhich analyzes Dart code and outputs the results to the console.

  • Option –fatal-style Causes the command to fail if code style errors are encountered.

  • Option –fatal-performance Causes the command to fail if performance problems are detected in the code.

  • Option –no-fatal-warnings excludes warnings from analysis results.

  • Argument lib points to the folder where the main application code is located.

This will help us analyze Dart code and identify code style errors, performance issues, and other issues that may affect the quality of the code and the difficulty of maintaining it.

  1. dart-metrics-check-unused-files

dart-metrics-check-unused-files:
  stage: static
  interruptible: true
  before_script:
    - flutter pub get
  script:
    - flutter pub run dart_code_metrics:metrics check-unused-files --fatal-unused --exclude="{lib/application/core/bloc/void_action_bloc.dart,lib/util/log.dart}" lib
  tags:
    - ci

This Job is for testing the use of files in Dart code. It is also executed at the Static stage and can be interrupted (interruptible: true).

Before executing the script, the task also executes the command flutter pub get to update Flutter project dependencies.

The script then does the following:

  • Executes the command flutter pub run dart_code_metrics:metrics check-unused-files –fatal-unused –exclude=”{lib/application/core/bloc/void_action_bloc.dart,lib/util/log.dart}” libwhich checks for file usage in Dart code.

  • Option –fatal-unused Causes the command to fail if unused files are found.

  • Option –exclude excludes the specified files from scanning.

  • Argument lib points to the folder where the main application code is located.

Using this task, you can check whether all the files in the code are actually used, and whether there are unnecessary files that can complicate the code and increase its compilation time.

  1. dart-metrics-check-unused-code

dart-metrics-check-unused-code:
  rules:
    - when: never
  stage: static
  interruptible: true
  before_script:
    - flutter pub get
  script:
    - flutter pub run dart_code_metrics:metrics check-unused-code --exclude="{lib/application/core/bloc/void_action_bloc.dart,lib/infrastructure/api/response_parser.dart,lib/util/log.dart}" --fatal-unused lib
  tags:
    - ci

This Job is for testing the use of Dart code. It is executed in the Static stage and can again be interrupted (interruptible: true).

Before executing the script, the task executes the command flutter pub get to update Flutter project dependencies.

The script then does the following:

  • Executes the command flutter pub run dart_code_metrics:metrics check-unused-code –exclude=”{lib/application/core/bloc/void_action_bloc.dart,lib/infrastructure/api/response_parser.dart,lib/util/log.dart}” — fatal-unused libwhich checks the usage of Dart code.

  • Option –exclude excludes the specified files from scanning.

  • Option –fatal-unused Causes the command to fail if unused classes, functions, or variables are encountered.

  • Argument lib points to the folder where the main application code is located.

This task will help check whether all classes, functions and variables in the code are actually used, and whether there are unnecessary code elements that can complicate it and increase its compilation time.

  1. dart-metrics-check-unused-translations

dart-metrics-check-unused-translations:
  stage: static
  interruptible: true
  before_script:
    - flutter pub get
  script:
    - dart run dart_code_metrics:metrics check-unused-l10n --fatal-unused lib
  tags:
    - ci

This Job is for testing the use of translations in Dart code. It is also executed at the Static stage and can be interrupted (interruptible: true).

Before executing the script, the task executes the command flutter pub get to update Flutter project dependencies.

The script then does the following:

  • Executes the command dart run dart_code_metrics:metrics check-unused-l10n –fatal-unused libwhich checks for the use of translations in Dart code.

  • Option –fatal-unused Causes the command to fail if unused translations are detected.

  • Argument lib points to the folder where the main application code is located.

Using this task, we check that all translations in the code are actually used and that there are no unnecessary translations. Unnecessary translations increase the size of the application and make it more difficult to maintain.

  1. code-generation-mismatch-check

code-generation-mismatch-check:
  stage: static
  interruptible: true
  before_script:
    - flutter pub get
  script:
    - dart run build_runner build --delete-conflicting-outputs --fail-on-severe
    - git diff
    - (( $(git status --porcelain|wc -l) == 0 )) || { echo >&2 "Some changes in generated files detected"; exit 1; }
  tags:
    - ci

Before executing the script, the task executes the command flutter pub get to update Flutter project dependencies.

The script then does the following:

  1. Executes the command dart run build_runner build –delete-conflicting-outputs –fail-on-severe, which generates code based on configuration files in the project. Option –delete-conflicting-outputs indicates that, in the event of a conflict between generated and existing code, existing files will be deleted. Option –fail-on-severe causes the command to fail if there are errors at the level severe.

  2. Executes the command git diffwhich shows the difference between the current state of the files in the working directory and the last commit.

  3. Executes the command (( $(git status –porcelain|wc -l) == 0 )) || { echo >&2 “Some changes in generated files detected”; exit 1; }, which checks if there are changes in the files generated by build_runner. If there are changes, it displays a message about detecting changes in the generated files and exits with error code 1.

Using this task, you can check whether the generated code contains changes that were made manually. This can lead to compatibility and code support issues.

In Stage test we added just one command that runs our tests:

flutter-test:
  stage: test
  interruptible: false
  before_script:
    - flutter clean
    - flutter pub get
    - flutter pub run build_runner build --delete-conflicting-outputs
  script:
    - flutter test --update-goldens
  tags:
    - ci

CD setup

Now let’s look at assemblies. Why do we need them? Everything is simple – for convenience and saving time. When Runner makes a build, it archives it. It is called Artifact. Assemblies are at the last stage, after passing all checks. Artifact can be seen and downloaded on the MP page.

Tasks for Android builds .apk and .abb

  1. flutter_build_android_apk


flutter_build_android_apk:
  stage: build
  interruptible: false
  before_script:
    - flutter clean
    - flutter pub get
    - flutter pub run build_runner build --delete-conflicting-outputs
  script:
    - flutter build apk --no-tree-shake-icons --flavor development -t lib/main.dart
  artifacts:
    paths:
      - build/app/outputs/flutter-apk/app-development-release.apk
    expire_in: 7 day
  tags:
    - cd

This Job is for building an Android apk file in a Flutter application. It is executed at the Build stage and cannot be interrupted (interruptible: false).

Parameters used:

  • interruptible – execution progress cannot be stopped.

  • artifacts: paths – path to the folder where the assembly will be stored.

  • artifacts: expire_in is the number of days that the build will be stored on the GitLab server.

Before executing the script, the task processes the following commands:

  1. flutter clean — clears the project’s build folder from previous builds.

  2. flutter pub get — updates Flutter project dependencies.

  3. flutter pub run build_runner build –delete-conflicting-outputs – Executes a command to generate code from configuration files (for example, from .freezed files).

The script then does the following:

  • flutter build apk –no-tree-shake-icons –flavor development -t lib/main.dart runs a command to build an Android apk file for a Flutter application.

  • Option –no-tree-shake-icons disables icon optimization, which may be necessary to resolve problems with their display.

  • Option –flavor development indicates a development build.

  • Argument -t lib/main.dart points to the main application file.

After the build is complete, the apk file is saved in the folder build/app/outputs/flutter-apk/app-development-release.apk and placed in the artifact storage. Artifacts are stored for 7 days (expire_in: 7 day).

  1. flutter_build_android_aab


flutter_build_android_aab:
  stage: build
  interruptible: false
  before_script:
    - flutter clean
    - flutter pub get
    - flutter pub run build_runner build --delete-conflicting-outputs
  script:
    - flutter build appbundle --no-tree-shake-icons --flavor development -t lib/main.dart
  artifacts:
    paths:
      - build/app/outputs/bundle/developmentRelease/app-development-release.aab
    expire_in: 7 day
  tags:
    - cd

This Job is for building an Android App Bundle (AAB) in a Flutter application. It is executed at the Build stage and cannot be interrupted (interruptible: false).

Parameters used:

  • interruptible – execution progress cannot be stopped.

  • artifacts: paths – path to the folder where the assembly will be stored.

  • artifacts: expire_in is the number of days that the build will be stored on the GitLab server.

Before executing the script, the task processes the following commands:

  1. flutter clean — clears the project’s build folder from previous builds.

  2. flutter pub get — updates Flutter project dependencies.

  3. flutter pub run build_runner build –delete-conflicting-outputs – Executes a command to generate code from configuration files (for example, from .freezed files).

The script then does the following:

  • flutter build appbundle –no-tree-shake-icons –flavor development -t lib/main.dart runs a command to build an Android App Bundle (AAB) for a Flutter app.

  • Option –no-tree-shake-icons disables icon optimization, which may be necessary to resolve problems with their display.

  • Option –flavor development indicates a development build. Argument -t lib/main.dart points to the main application file.

After the build is complete, the AAB file is saved in the folder build/app/outputs/bundle/developmentRelease/app-development-release.aab and placed in the artifact storage. Artifacts are stored for 7 days (expire_in: 7 day).

Tasks for IOS build .ipa

To build the .ipa we need a developer account because we will need certificates and permissions. Also for assembly we must specify parameters. To do this, you need to create a file ExportOptions.plist in the iOS folder of our project. Let’s see what needs to be added to the ExportOptions.plist file.

<?xml version="1.0"coding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" " http://www.apple.com/DTDs/PropertyList -1.0.dtd ">
<plist version="1.0">
<dict>
    <key>provisioningProfiles</key>
    <dict>
        <key>com.ci_cd_example</key>
        <string>CI CD Example Disctribution</string>
    </dict>
    <key>signingCertificate</key>
    <string>Apple Distribution: Team, OOO</string>
    <key>method</key>
    <string>app-store</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>teamID</key>
    <string>C75gre4s64</string>
</dict>
</plist>
  • provisioningProfiles – contains information about the provisioning profiles required for signing the application. Here is a profile with name CI CD Example Distribution for application with ID com.ci_cd_example.

  • signingCertificate – Contains information about the signing certificate required to sign the application. Here is a certificate named Apple Distribution: Team, LLC.

  • method – Specifies how the application is deployed. Here it is app-store. This means that the application will be published in the App Store.

  • signingStyle – specifies how the application is signed. Here it is manual. This means that the signature will be done manually.

  • teamID – contains the ID of the development team, also required for signing the application.

signingCertificate and provisioningProfiles can be obtained from Xcode.

teamID can be found in your developer account. To do this you need to go to the page https://developer.apple.com/account/#/membershipand there you will see the following:

When the ExportOptions.plist file is ready, you can run the job:

flutter_build_ios_ipa:
  stage: build
  interruptible: false
  before_script:
    - flutter clean
    - flutter pub get
    - flutter pub run build_runner build --delete-conflicting-outputs
    - cd ios
    - rm -rf Podfile.lock
    - pod install --repo-update
  script:
    - flutter build ipa --no-tree-shake-icons --flavor development --export-options-plist $PWD/ExportOptions.plist
  artifacts:
    paths:
      - build/ios/ipa/*.ipa
    expire_in: 7 day
  tags:
    - cd

Ready! We set up CI/CD on a Flutter project

If you strictly followed my instructions, everything will work like clockwork. The code will be checked and tested, and assemblies will be assembled 🙂 Tested from my own experience.

I would not recommend setting up CI/CD on an already existing project, because then you will have to spend a lot of time fixing errors. But for new projects this is a great option.

Good luck and good projects!

PS My colleague Sasha Vorozhishchev runs a cool telegam channel about Flutter and more. Subscribe!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *