Инструкция: как быстро настроить GitLab CI/CD на Flutter-проекте

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

0b9dd151536d10fd57979fde09265553.png

Кратко о CI/CD

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

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

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

5a7fa8dd5e532c51f791465d78a05d69.png

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

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

  • 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.

e09dfc39f960c442be8c0a0c26536307.png

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

5652947ff7d108722c672a9c9ed9145f.png

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

748ce39246fd53efc78dc7fc217ee203.png

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

d55a56b15403724acf8d72d87fab2671.png

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

6f7797740af45d1c39a92a2845bb8eb4.png

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

5d18717d5bd45ece8d9dc151aa6702de.png

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

144b25a19045dc3d71e388383b764bf9.png

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

99071aa1ba4e1799a504f5470fe7d7fd.png

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

c9dc71355bcd02e5278463e7e3355a1b.png

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

7ab11c5288fef79e24ae9f3ff46d0c0c.png

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

aedf026c64318b5b93a1eb7151e869da.png

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

d672c8b243880461fae0ba63ea5aea35.png

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

ffc4ef4e273917d988cbe7bf7bae6d69.png

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

d78f50c456bc015b3f7de1710badd7a9.png

Подготовка к настройке 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 мы увидим это так:

7d1ba9fc42c6de7a287b621795be1dfe.png

В самое начало файла .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 можно увидеть и скачать на странице МР.

cd724fcd301584663a9ed0642c110f14.png

Задания для 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.





    provisioningProfiles
    
        com.ci_cd_example
        CI CD Example Disctribution
    
    signingCertificate
    Apple Distribution: Team, OOO
    method
    app-store
    signingStyle
    manual
    teamID
    C75gre4s64

  • 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.

85cc074bac7601431fc52ac70607c3c5.png

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

26d906eea56607374825b47451d81018.png

Когда файл 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 и не только. Подписывайтесь!

© Habrahabr.ru