Привет! Я Александр Омельяненко, 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:
-
static
-
test
-
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-проверок.
-
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 и выявить ошибки стиля кода, проблемы с производительностью и другие проблемы, которые могут повлиять на качество кода и сложность его поддержки.
-
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 указывает на папку, в которой находится основной код приложения.
С помощью этой задачи можно проверить, все ли файлы в коде действительно использованы, и нет ли лишних файлов, которые могут усложнять код и увеличивать время его компиляции.
-
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 указывает на папку, в которой находится основной код приложения.
Эта задача поможет проверить, все ли классы, функции и переменные в коде действительно использованы, и нет ли лишних элементов кода, которые могут усложнять его и увеличивать время его компиляции.
-
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 указывает на папку, в которой находится основной код приложения.
С помощью этой задачи проверяем, чтобы все переводы в коде были действительно использованы и не было лишних переводов. Лишние переводы увеличивают размер приложения и усложняют его поддержку.
-
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.
Затем скрипт выполняет следующие действия:
-
Выполняет команду dart run build_runner build —delete-conflicting-outputs —fail-on-severe, которая генерирует код на основе файлов конфигурации в проекте. Опция —delete-conflicting-outputs указывает, что, в случае конфликта между генерируемым и существующим кодом, будут удалены существующие файлы. Опция —fail-on-severe заставляет команду завершиться с ошибкой, если возникли ошибки на уровне severe.
-
Выполняет команду git diff, которая показывает разницу между текущим состоянием файлов в рабочем каталоге и последним коммитом.
-
Выполняет команду (( $(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
-
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.
Перед выполнением скрипта задача отрабатывает следующие команды:
-
flutter clean — очищает папку build проекта от предыдущих сборок.
-
flutter pub get — обновляет зависимости проекта Flutter.
-
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).
-
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.
Перед выполнением скрипта задача отрабатывает следующие команды:
-
flutter clean — очищает папку build проекта от предыдущих сборок.
-
flutter pub get — обновляет зависимости проекта Flutter.
-
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 и не только. Подписывайтесь!
ссылка на оригинал статьи https://habr.com/ru/articles/779028/
Добавить комментарий