Привет! Я Александр Омельяненко, 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 и не только. Подписывайтесь!
WondeRu
Отличная статья. Я бы даже сказал, что слишком много деталей и нет пищи для размышления. Я бы добавил, что Андроид лучше собирать в докере - меньше настраивать.
Чего нет: это деплой в google play и app store. Мы это делаем через fastlane в gitlab.
LascarDev Автор
Спасибо за обратную связь! Я рад что статья понравилась.
Я согласен, что сборка Android-приложений в Docker может быть более удобной, но статья была написана для тех кому надо быстро произвести настройку, а если разработчик не знаком с Docker, быстро его изучить не получится.
В будущем я обязательно хочу дополнить статью инструкцией о том как выгружать сборки в google play и app store, но это уже будет отдельно.