Привет! Я Александр Омельяненко, Flutter-разработчик в AGIMA. Недавно мне понадобилось быстро настроить CI/CD на Flutter-проекте. Те несколько руководств, что я нашел в интернете по этой теме, были либо с нерабочими примерами, либо запутанные и просто плохого качества. Но всё же какое-то представление я получил. Плюс задал вопросы коллегам. Набивая шишки по пути, я-таки настроил CI/CD на своем проекте. Но мне тогда очень пригодилась бы четкая инструкция. Поэтому я решил написать ее сам по горячим следам. Сегодня делюсь ею с вами и надеюсь, эта инструкция облегчит жизнь тем, кто настраивает CI/CD на Flutter-проекте прямо сейчас.

Кратко о CI/CD

Статья рассчитана на уровень Middle+, поэтому о CI/CD вкратце, просто потому что так уж заведено. CI (Continuous Integration) и CD (Continuous Delivery) — это методология разработки, с помощью которой можно чаще и эффективней выпускать новые версии приложений, автоматизировать рутинные процессы и сэкономить время на тестировании и сборке .apk-, .abb- и .ipa-файлов.

Почему мы выбрали GitLab CI/CD

Просто сравнили GitLab CI/CD с другими популярными платформами для Flutter-проектов — GitHub и CodeMagic. И GitLab победил в разрезе трех показателей :)

Терминология

Эти термины вы встретите по ходу инструкции:

  • Runner — процесс, который выполняет задачи в конвейере. В GitLab CI/CD можно использовать как встроенные, так и внешние Runners.

  • Pipeline — цепочка задач, которые выполняются в определенном порядке. В контексте GitLab CI/CD это набор шагов, которые выполняются после каждого МР в репозитории.

  • Stage — этап в конвейере, который состоит из нескольких задач. Например, этапы могут включать в себя сборку, тестирование, пакеты и развертывание.

  • Job — задача, которая выполняется при определенном этапе. Например, задача может включать в себя сборку кода, запуск тестов или публикацию пакета.

  • Artifact — артефакт, который создается в результате выполнения задачи. Например, это может быть скомпилированный код (сборка), результаты тестов или документация.

Окружающая среда

Мы почти готовы перейти к настройке CI/CD. Но сперва разберемся с окружающей средой. Нам нужно понять, где будет храниться репозиторий, куда будет устанавливаться Runner и есть ли смысл использовать Docker.

Что нам для этого понадобится?

Runner, который запускает Pipelines, репозиторий GitLab, где будет лежать код, и, конечно, сам проект.

  • Runner можно установить на сервер или себе на локальную машину. Важно для тех, кто работает в команде и выбрал второй вариант: Pipelines будут выполняться на вашей машине каждый раз, когда кто-то будет их запускать.

  • Репозиторий можно установить на сервер, хранить локально на своей машине или оставить как есть, на сервере самого GitLab.

  • Изолированные окружения для Pipelines CI/CD можно создать с помощью Docker. Это может быть полезно, если вам нужно запускать задания в одном и том же окружении на разных серверах. Но если вы не знакомы с Docker и нет времени его изучать, все будет работать и без него.

Вводные о проекте из этой статьи: мы использовали удаленный сервер на операционной системе MacOS, установили на него Runner, не устанавливали репозиторий и не использовали Docker. Поэтому учитывайте, что далее будет инструкция по настройке CI/CD именно для такой окружающей среды.

Установка Runner

Теперь мы готовы. Cначала установим Runner. Для этого переходим в GitLab→Settings→CI/CD.

Переходим в Runners.

Тут будут все Runners нашего проекта. Нажимаем New project runner.

На открывшейся странице выбираем платформу — в нашем случае это MacOS.

Добавляем теги. Мы добавили два — ci и cd. Можете создать еще, на ваше усмотрение. Теги могут быть полезны для группировки и управления доступами к задачам. Мы использовали теги просто для удобства.

Остальные поля можете оставить пустыми. Внизу страницы нажимаем Create runner. После этого Runner создается, но только в проекте GitLab. Теперь нам нужно установить Runner на нашу машину, для этого переходим по ссылке.

Следуем инструкции: выбираем операционную систему, архитектуру, копируем команду и вставляем ее в терминал.

Дальше можем установить Runner нашего проекта. Для этого копируем команду и вводим ее в терминал.

После этого нам нужно указать URL. Берем его прямо из предложенного варианта.

Далее вводим имя для Runner. Можно ввести название проекта.

Далее выбираем исполнителя, в нашем случае это Shell.

После этого наш Runner должен установиться. Если вы всё сделали правильно, то в терминале увидите сообщение об успешной установке: «Runner успешно зарегистрирован». Можете запускать его. Но, если он уже запущен, конфигурация должна автоматически перезагрузиться. Конфигурация (с токеном аутентификации) сохранена в /Users/user/.gitlab-runner/config.toml.

Далее нам нужно запустить Runner. Для этого вводим в терминал команду gitlab-runner run и после этого можем перейти на страницу нашего Runner.

Здесь мы видим, что наш Runner запущен и готов к работе.

Подготовка к настройке CI/CD

Теперь мы готовы настроить сценарий нашего CI. Для начала в корневой папке проекта создаем файл .gitlab-ci.yml. GitLab будет распознавать этот файл и выполнять инструкции, написанные в нем. Но прежде чем писать инструкции, нужно определиться со Stages и WorkFlow.

Stages 

Мы добавили 3 Stages:

  1. static

  2. test

  3. build

static — сюда добавляем все проверки, связанные с правильностью написания кода, проверки на неиспользуемые файлы и т. д.

test — здесь будут команды, связанные с тестированием, в зависимости от того, какие тесты вы используете.

build — сюда добавляем команды для сборок. Мы будем собирать .abb, .apk и .ipa.

В GitLab мы увидим это так:

В самое начало файла .gitlab-ci.yml добавляем:

stages:
  - static
  - test
  - build

WorkFlow

WorkFlow — это условие, при котором наша инструкция будет срабатывать. В этом примере мы хотим, чтобы Pipeline запускался, когда разработчик делает МР. Для этого после Stages добавляем:

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

Давайте разберемся, в каком виде мы будем добавлять задачи в наш файл. Одна задача — это Job, и выглядит она так:


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

У каждой Job есть имя и параметры. В процессе мы будем добавлять разные параметры и рассматривать их индивидуально. Также есть параметры для добавления команд before_script, script, after_script. Тут, я думаю, всё понятно. Также есть Tags — теги, которые мы добавляли при установке Runner.

Настройка CI

Теперь давайте рассмотрим несколько команд для Static-проверок.

  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

Эта задача (Job) для анализа кода на Dart с помощью плагина dart_code_metrics. Она выполняется на этапе Static и может быть прервана (interruptible: true).

Перед выполнением скрипта задача отрабатывает команду flutter pub get для обновления зависимостей проекта Flutter.

Затем скрипт выполняет следующие действия:

  • Выполняет команду flutter pub run dart_code_metrics:metrics analyze --fatal-style --fatal-performance --no-fatal-warnings --reporter=console lib, которая анализирует код на Dart и выводит результаты в консоль. 

  • Опция --fatal-style заставляет команду завершиться с ошибкой, если обнаружены ошибки стиля кода. 

  • Опция --fatal-performance заставляет команду завершиться с ошибкой, если обнаружены проблемы с производительностью кода. 

  • Опция --no-fatal-warnings исключает предупреждения из результатов анализа.

  • Аргумент lib указывает на папку, в которой находится основной код приложения.

Это поможет нам проанализировать код на Dart и выявить ошибки стиля кода, проблемы с производительностью и другие проблемы, которые могут повлиять на качество кода и сложность его поддержки.

  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

Эта задача (Job) для проверки использования файлов в коде на Dart. Она тоже выполняется на этапе Static и может быть прервана (interruptible: true).

Перед выполнением скрипта задача также отрабатывает команду flutter pub get для обновления зависимостей проекта Flutter.

Затем скрипт выполняет следующие действия:

  • Выполняет команду 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, которая проверяет использование файлов в коде на Dart. 

  • Опция --fatal-unused заставляет команду завершиться с ошибкой, если обнаружены неиспользуемые файлы. 

  • Опция --exclude исключает указанные файлы из проверки. 

  • Аргумент lib указывает на папку, в которой находится основной код приложения.

С помощью этой задачи можно проверить, все ли файлы в коде действительно использованы, и нет ли лишних файлов, которые могут усложнять код и увеличивать время его компиляции.

  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

Эта задача (Job) для проверки использования кода на Dart. Она выполняется на этапе Static и опять же может быть прервана (interruptible: true).

Перед выполнением скрипта задача отрабатывает команду flutter pub get для обновления зависимостей проекта Flutter.

Затем скрипт выполняет следующие действия:

  • Выполняет команду 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, которая проверяет использование кода на Dart. 

  • Опция --exclude исключает указанные файлы из проверки.

  • Опция --fatal-unused заставляет команду завершиться с ошибкой, если обнаружены неиспользуемые классы, функции или переменные. 

  • Аргумент lib указывает на папку, в которой находится основной код приложения.

Эта задача поможет проверить, все ли классы, функции и переменные в коде действительно использованы, и нет ли лишних элементов кода, которые могут усложнять его и увеличивать время его компиляции.

  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

Эта задача (Job) для проверки использования переводов в коде на Dart. Она тоже выполняется на этапе Static и может быть прервана (interruptible: true).

Перед выполнением скрипта задача отрабатывает команду flutter pub get для обновления зависимостей проекта Flutter.

Затем скрипт выполняет следующие действия:

  • Выполняет команду dart run dart_code_metrics:metrics check-unused-l10n --fatal-unused lib, которая проверяет использование переводов в коде на Dart. 

  • Опция --fatal-unused заставляет команду завершиться с ошибкой, если обнаружены неиспользуемые переводы. 

  • Аргумент lib указывает на папку, в которой находится основной код приложения.

С помощью этой задачи проверяем, чтобы все переводы в коде были действительно использованы и не было лишних переводов. Лишние переводы увеличивают размер приложения и усложняют его поддержку.

  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

Перед выполнением скрипта задача отрабатывает команду flutter pub get для обновления зависимостей проекта Flutter.

Затем скрипт выполняет следующие действия:

  1. Выполняет команду dart run build_runner build --delete-conflicting-outputs --fail-on-severe, которая генерирует код на основе файлов конфигурации в проекте. Опция --delete-conflicting-outputs указывает, что, в случае конфликта между генерируемым и существующим кодом, будут удалены существующие файлы. Опция --fail-on-severe заставляет команду завершиться с ошибкой, если возникли ошибки на уровне severe.

  2. Выполняет команду git diff, которая показывает разницу между текущим состоянием файлов в рабочем каталоге и последним коммитом.

  3. Выполняет команду (( $(git status --porcelain|wc -l) == 0 )) || { echo >&2 "Some changes in generated files detected"; exit 1; }, которая проверяет, есть ли изменения в файлах, генерируемых build_runner. Если изменения есть, то выводит сообщение об обнаружении изменений в генерируемых файлах и завершается с кодом ошибки 1.

С помощью этой задачи можно проверить, нет ли в генерируемом коде изменений, которые внесли вручную. Это может привести к проблемам с совместимостью и поддержкой кода.

В Stage test мы добавили всего одну команду, которая запускает наши тесты:

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

Теперь давайте разберемся со сборками. Зачем они нам нужны? Всё просто — для удобства и экономии времени. Когда Runner делает сборку, он архивирует ее. Это называется Artifact. Сборки идут на последнем этапе, после прохождения всех проверок. Artifact можно увидеть и скачать на странице МР.

Задания для Android-сборок .apk и .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

Эта задача (Job) для сборки Android-apk-файла в приложении на Flutter. Она выполняется на стадии Build и не может быть прервана (interruptible: false).

Используемые параметры:

  • interruptible — ход выполнения нельзя остановить.

  • artifacts: paths — путь к папке, где будет храниться сборка.

  • artifacts: expire_in — это количество дней, которые сборка будет храниться на сервере GitLab.

Перед выполнением скрипта задача отрабатывает следующие команды:

  1. flutter clean — очищает папку build проекта от предыдущих сборок.

  2. flutter pub get — обновляет зависимости проекта Flutter.

  3. flutter pub run build_runner build --delete-conflicting-outputs — выполняет команду для генерации кода из файлов конфигурации (например, из файлов .freezed).

Затем скрипт выполняет следующие действия:

  • flutter build apk --no-tree-shake-icons --flavor development -t lib/main.dart выполняет команду для сборки Android-apk-файла для приложения на Flutter. 

  • Опция --no-tree-shake-icons отключает оптимизацию иконок, что может быть необходимо для устранения проблем с их отображением.

  • Опция --flavor development указывает на сборку для разработки. 

  • Аргумент -t lib/main.dart указывает на основной файл приложения.

После завершения сборки apk-файл сохраняется в папке build/app/outputs/flutter-apk/app-development-release.apk и помещается в хранилище артефактов. Артефакты хранятся в течение 7 дней (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

Эта задача (Job) для сборки Android App Bundle (AAB) в приложении на Flutter. Она выполняется на стадии Build и не может быть прервана (interruptible: false).

Используемые параметры:

  • interruptible — ход выполнения нельзя остановить.

  • artifacts: paths — путь к папке, где будет храниться сборка.

  • artifacts: expire_in — это количество дней, которые сборка будет храниться на сервере GitLab.

Перед выполнением скрипта задача отрабатывает следующие команды:

  1. flutter clean — очищает папку build проекта от предыдущих сборок.

  2. flutter pub get — обновляет зависимости проекта Flutter.

  3. flutter pub run build_runner build --delete-conflicting-outputs — выполняет команду для генерации кода из файлов конфигурации (например, из файлов .freezed).

Затем скрипт выполняет следующие действия:

  • flutter build appbundle --no-tree-shake-icons --flavor development -t lib/main.dart выполняет команду для сборки Android App Bundle (AAB) для приложения на Flutter.

  • Опция --no-tree-shake-icons отключает оптимизацию иконок, что может быть необходимо для устранения проблем с их отображением. 

  • Опция --flavor development указывает на сборку для разработки. Аргумент -t lib/main.dart указывает на основной файл приложения.

После завершения сборки AAB-файл сохраняется в папке build/app/outputs/bundle/developmentRelease/app-development-release.aab и помещается в хранилище артефактов. Артефакты хранятся в течение 7 дней (expire_in: 7 day).

Задания для IOS-сборки .ipa

Для сборки .ipa нужен аккаунт разработчика, потому что нам потребуются сертификаты и разрешения. Также для сборки мы должны указать параметры. Для этого нужно создать файл ExportOptions.plist в папке iOS нашего проекта. Давайте посмотрим, что нужно добавить в файл ExportOptions.plist.

<?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 — содержит информацию о профилях провайдинга, необходимые для подписи приложения. Здесь указан профиль с именем CI CD Example Distribution для приложения с идентификатором com.ci_cd_example.

  • signingCertificate — содержит информацию о сертификате подписи, необходимый для подписи приложения. Здесь указан сертификат с именем Apple Distribution: Team, OOO.

  • method — указывает способ развертывания приложения. Здесь это app-store. Значит, приложение разместят в App Store.

  • signingStyle — указывает способ подписи приложения. Здесь это manual. Значит, подпись будет выполнена вручную.

  • teamID — содержит идентификатор команды разработчиков, также необходимый для подписи приложения.

signingCertificate и provisioningProfiles можно взять в Xcode.

teamID можно найти в аккаунте разработчика. Для этого нужно перейти на страницу https://developer.apple.com/account/#/membership, и там вы увидите следующее:

Когда файл ExportOptions.plist готов, можно запускать задание:

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

Готово! Мы настроили CI/CD на Flutter-проекте

Если вы четко следовали моей инструкции, всё будет работать, как часы. Код будет проверяться и тестироваться, а сборки собираться :) Проверено на собственном опыте.

Я бы не рекомендовал настраивать CI/CD на уже существующем проекте, ведь потом вам придется потратить немало времени на исправление ошибок. А вот для новых проектов это отличный вариант.

Удачи и хороших проектов!

P. S. Мой коллега Саша Ворожищев ведет классный телегам-канал про Flutter и не только. Подписывайтесь!

Комментарии (3)


  1. WondeRu
    08.12.2023 00:01

    Отличная статья. Я бы даже сказал, что слишком много деталей и нет пищи для размышления. Я бы добавил, что Андроид лучше собирать в докере - меньше настраивать.

    Чего нет: это деплой в google play и app store. Мы это делаем через fastlane в gitlab.


    1. LascarDev Автор
      08.12.2023 00:01
      +1

      Спасибо за обратную связь! Я рад что статья понравилась.
      Я согласен, что сборка Android-приложений в Docker может быть более удобной, но статья была написана для тех кому надо быстро произвести настройку, а если разработчик не знаком с Docker, быстро его изучить не получится.
      В будущем я обязательно хочу дополнить статью инструкцией о том как выгружать сборки в google play и app store, но это уже будет отдельно.


  1. comerc
    08.12.2023 00:01

    термин МР не расшифрован