
Flutter & GitLab CI/CD. Доставка мобильного приложения в Significa, TestFlight и Google Play
Привет! Меня зовут Данил Абдрафиков, я мобильный разработчик в компании TAGES. После успешной настройки сборки и подписания Flutter-приложений во второй части, остался последний, но не менее важный этап — дистрибуция приложения.
В современных условиях автоматизация деплоя на такие платформы, как Google Play, TestFlight и Significa, становится необходимостью. В этой статье мы разберем, как с помощью GitLab CI/CD настроить автоматическую отправку ваших приложений в магазины, чтобы вы могли сосредоточиться на разработке, а не на рутинных задачах.
Введение
Когда речь заходит о «доставке» мобильного приложения, большинство думает только о публикации в App Store или Google Play. На практике же доставка — это не одно действие, а набор процессов, которые зависят от разных задач: нужно ли выкатить сборку коллегам внутри компании или же отправить релиз миллионам пользователей. В первом случае важны скорость, простота и доступность сборок, во втором — изолированность, прозрачный процесс и контроль релизов.
Чтобы достичь этой цели, мы охватим следующие темы:
Все подробно разберем на примерах и готовых конфигурациях.
Подготовка сервера дистрибуции Significa
Significa App Distribution Server — это минималистичный сервер для внутренней дистрибуции мобильных сборок (.ipa и .apk). Его преимущество в том, что он решает одну конкретную задачу максимально просто: принимает готовые артефакты, хранит их и выдает простую ссылку/QR‑код для быстрой установки на устройстве.
Если хотите сильнее погрузиться в эту тему, то у проекта есть подробный материал с описанием данного инструмента и пошаговым разбором использования. Важно понимать, что это не замена дистрибуции приложения через TestFlight/Google Play, а легкий инструмент для оперативной доставки мобильных приложений из CI (подходит для Ad‑hoc/Enterprise сценариев).
В рамках статьи мы не будем углубляться в подробности настройки и развертывания самого сервера Significa — это хорошо описано в официальной документации. Вашим результатом на этом этапе должен стать запущенный и доступный экземпляр Significa, в который мы далее настроим автоматическую отправку сборок из GitLab CI.
Добавление скриптов деплоя в репозиторий с шаблонами
В прошлой части статьи мы уже создали децентрализованный репозиторий с общими скриптами сборки, следующий шаг — добавить скрипты для доставки артефактов в Significa, TestFlight и Google Play. Для этого создадим папку deploy и поместим туда следующие файлы:
. │ └── deploy │ └── google_play.gitlab-ci.yml │ └── significa.gitlab-ci.yml │ └── testflight.gitlab-ci.yml .
./deploy/google_play.gitlab-ci.yml
Этот файл помогает автоматизировать отправку AAB-файла в Google Play, загружая его в указанную группу распространения.
google_play.gitlab-ci.yml
include: - local: '/common.gitlab-ci.yml' .supply_track: script: - bundle exec fastlane run supply track:"$SUPPLY_TRACK" aab:"$BUILD_AAB_FILE_PATH" .variables:deploy_google_play: variables: GOOGLE_PLAY_REQUIRED: true .before_script:deploy_google_play: before_script: - !reference [ .clone_project, before_script ] - !reference [ .configure_fastlane, before_script ] .script:deploy_google_play: script: - !reference [ .supply_track, script ]
./deploy/significa.gitlab-ci.yml
Данный файл автоматизирует выгрузку артефакта на сервер Significa с целью внутренней дистрибуции и выдачи ссылки для установки:
significa.gitlab-ci.yml
include: - local: '/common.gitlab-ci.yml' .configure_significa: before_script: - export SIGNIFICA_ENABLED=$([ -n "$SIGNIFICA_SERVER_URL" ] && [ -n "$SIGNIFICA_SECRET" ] && echo "true" || echo "false") - | if ! $SIGNIFICA_ENABLED; then echo "❌️ Error: App distribution server is disabled. Please specify the required variables: SIGNIFICA_SERVER_URL, SIGNIFICA_SECRET" echo "Read more: https://github.com/significa/app-distribution-server" exit 1 fi .upload_to_significa: script: - echo -e "\e[0Ksection_start:`date +%s`:upload_to_significa_section\r\e[0KDistribute app via Significa" - | i=1 while [ $i -le "$SIGNIFICA_UPLOAD_MAX_RETRIES" ]; do echo "ℹ️ Attempt $i to upload to Significa..." BUILD=$(curl -X "POST" \ "$SIGNIFICA_SERVER_URL/api/upload" \ -H "Accept: application/json" \ -H "X-Auth-Token: $SIGNIFICA_SECRET" \ -H "Content-Type: multipart/form-data" \ -F "app_file=@$APP_FILE") if echo "$BUILD" | jq empty >/dev/null 2>&1; then BUNDLE_VERSION=$(echo "$BUILD" | jq -r '.bundle_version // empty') UPLOAD_ID=$(echo "$BUILD" | jq -r '.upload_id // empty') PLATFORM_NAME=$(echo "$BUILD" | jq -r '.platform // empty') else echo "⚠️ Warning: Received invalid JSON response or no response at all:" echo "$BUILD" fi if [[ -n "$BUNDLE_VERSION" && -n "$UPLOAD_ID" && -n "$PLATFORM_NAME" ]]; then echo "✅ Upload successful!" break else echo "⚠️ Warning: Upload failed. Missing required fields. Retrying in $SIGNIFICA_UPLOAD_RETRY_DELAY seconds..." sleep "$SIGNIFICA_UPLOAD_RETRY_DELAY" fi i=$((i + 1)) done - | if [ -z "$BUNDLE_VERSION" ] || [ -z "$UPLOAD_ID" ] || [ -z "$PLATFORM_NAME" ]; then echo "❌ Error: All attempts to upload to Significa failed." exit 1 fi - | echo "ℹ️ Info: Distribute app version $BUNDLE_VERSION ($UPLOAD_ID)" - echo "UPLOAD_ID=$UPLOAD_ID" > "$DEPLOY_ENV_PATH" - | if [[ -n "$CI_PROJECT_TOKEN" ]]; then if [ -z "${!SIGNIFICA_UPLOADS_VAR}" ]; then export ${SIGNIFICA_UPLOADS_VAR}='{}' fi SIGNIFICA_UPLOADS_NEW_VALUE=$(echo "${!SIGNIFICA_UPLOADS_VAR}" | jq --arg branch "$CI_COMMIT_REF_SLUG" --arg upload_id "$UPLOAD_ID" ' .[$branch] = (.[$branch] // []) + [$upload_id] ') eval "export $SIGNIFICA_UPLOADS_VAR='$SIGNIFICA_UPLOADS_NEW_VALUE'" echo "✅ Updated ${SIGNIFICA_UPLOADS_VAR}: ${!SIGNIFICA_UPLOADS_VAR}" curl --request POST --header "PRIVATE-TOKEN: $CI_PROJECT_TOKEN" \ "$CI_API_V4_URL/projects/$CI_PROJECT_ID/variables" \ --form "key=$SIGNIFICA_UPLOADS_VAR" \ --form "value=${!SIGNIFICA_UPLOADS_VAR}" \ --form "description=These upload IDs are used to manage files, particularly for deletion or cleanup operations. Variable auto synced." \ --form "raw=true" curl --request PUT --header "PRIVATE-TOKEN: $CI_PROJECT_TOKEN" \ "$CI_API_V4_URL/projects/$CI_PROJECT_ID/variables/$SIGNIFICA_UPLOADS_VAR" \ --form "value=${!SIGNIFICA_UPLOADS_VAR}" else echo "⚠️ Warning: The `CI_PROJECT_TOKEN` variable is required to enable automatic cleanup of builds in Significa." echo "If this variable is not set, builds will not be deleted automatically when environment stopped." fi - echo -e "\e[0Ksection_end:`date +%s`:upload_to_significa_section\r\e[0K" .cleanup_significa: script: - echo -e "\e[0Ksection_start:`date +%s`:cleanup_significa_section\r\e[0KCleaning up Significa resources" - | if [[ -z "$CI_PROJECT_TOKEN" ]]; then echo "⚠️ Warning: The `CI_PROJECT_TOKEN` variable is required to enable automatic cleanup of builds in Significa." echo "If this variable is not set, builds will not be deleted automatically when environment stopped." exit 1 fi - | if [ -z "${!SIGNIFICA_UPLOADS_VAR}" ]; then export ${SIGNIFICA_UPLOADS_VAR}='{}' fi UPLOAD_IDS=$(echo "${!SIGNIFICA_UPLOADS_VAR}" | jq -r --arg branch "$CI_COMMIT_REF_SLUG" '.[$branch] // []') - | if [ "$(echo "$UPLOAD_IDS" | jq length)" -gt 0 ]; then echo "ℹ️ Info: Found uploads to delete: $UPLOAD_IDS" for UPLOAD_ID in $(echo "$UPLOAD_IDS" | jq -r '.[]'); do echo "ℹ️ Info: Deleting upload $UPLOAD_ID" curl -X "DELETE" \ "$SIGNIFICA_SERVER_URL/api/delete/$UPLOAD_ID" \ -H "Accept: application/json" \ -H "X-Auth-Token: $SIGNIFICA_SECRET" done else echo "ℹ️ Info: No uploads found for this branch." exit 0 fi - | eval "SIGNIFICA_UPLOADS_VALUE=\${$SIGNIFICA_UPLOADS_VAR}" SIGNIFICA_UPLOADS_VALUE=$(echo "$SIGNIFICA_UPLOADS_VALUE" | jq --arg branch "$CI_COMMIT_REF_SLUG" 'del(.[$branch])') if ! curl --request PUT --header "PRIVATE-TOKEN: $CI_PROJECT_TOKEN" \ "$CI_API_V4_URL/projects/$CI_PROJECT_ID/variables/$SIGNIFICA_UPLOADS_VAR" \ --form "value=$SIGNIFICA_UPLOADS_VALUE"; then echo "❌ Failed to update variable $SIGNIFICA_UPLOADS_VAR" exit 1 fi - echo -e "\e[0Ksection_end:`date +%s`:cleanup_significa_section\r\e[0K" # Deploy android .variables:deploy_android: variables: APP_FILE: $BUILD_APK_FILE_PATH DEPENDENCIES_TO_INSTALL: "curl jq" DEPLOY_ENV_PATH: "./deploy.env" SIGNIFICA_UPLOAD_MAX_RETRIES: 3 SIGNIFICA_UPLOAD_RETRY_DELAY: 10 SIGNIFICA_UPLOADS_VAR: "SIGNIFICA_ANDROID_UPLOADS" .variables:cleanup_android: variables: DEPENDENCIES_TO_INSTALL: "curl jq" SIGNIFICA_UPLOADS_VAR: "SIGNIFICA_ANDROID_UPLOADS" .before_script:deploy_android: before_script: - !reference [ .setup_environment, before_script ] - !reference [ .configure_significa, before_script ] .script:deploy_android: script: - !reference [ .upload_to_significa, script ] .before_script:cleanup_android: before_script: - !reference [ .setup_environment, before_script ] - !reference [ .configure_significa, before_script ] .script:cleanup_android: script: - !reference [ .cleanup_significa, script ] # Deploy iOS .variables:deploy_ios: variables: APP_FILE: $BUILD_IPA_FILE_PATH DEPENDENCIES_TO_INSTALL: "curl jq" DEPLOY_ENV_PATH: "./deploy.env" SIGNIFICA_UPLOAD_MAX_RETRIES: 3 SIGNIFICA_UPLOAD_RETRY_DELAY: 10 SIGNIFICA_UPLOADS_VAR: "SIGNIFICA_IOS_UPLOADS" .variables:cleanup_ios: variables: DEPENDENCIES_TO_INSTALL: "curl jq" SIGNIFICA_UPLOADS_VAR: "SIGNIFICA_IOS_UPLOADS" .before_script:deploy_ios: before_script: - !reference [ .setup_environment, before_script ] - !reference [ .configure_significa, before_script ] .script:deploy_ios: script: - !reference [ .upload_to_significa, script ] .before_script:cleanup_ios: before_script: - !reference [ .setup_environment, before_script ] - !reference [ .configure_significa, before_script ] .script:cleanup_ios: script: - !reference [ .cleanup_significa, script ]
./deploy/testflight.gitlab-ci.yml
Этот файл автоматизирует загрузку iOS‑сборки в App Store Connect и ее публикацию в TestFlight:
testflight.gitlab-ci.yml
include: - local: '/common.gitlab-ci.yml' .upload_to_testflight: script: - | bundle exec fastlane run upload_to_testflight \ api_key_path:"$APP_STORE_CONNECT_API_KEY_PATH" \ ipa:"$BUILD_IPA_FILE_PATH" \ skip_submission:true \ skip_waiting_for_build_processing:true .variables:deploy_testflight: variables: APP_STORE_CONNECT_REQUIRED: true .before_script:deploy_testflight: before_script: - !reference [ .clone_project, before_script ] - !reference [ .configure_fastlane, before_script ] .script:deploy_testflight: script: - !reference [ .upload_to_testflight, script ]
После этого коммитим, создаем новый тег и пушим изменения в удаленный репозиторий:
git commit -a -m "Добавить шаблоны деплоя для Android и iOS" git tag v1.1.0 git push origin main v1.1.0
Деплой приложения в Significa
Мы уже на полпути к полной автоматизации! Осталось научить наш CI/CD-пайплайн не только собирать приложение, но и автоматически загружать его на наш сервер дистрибуции Significa. Для этого нужно выполнить всего два ключевых шага.
Шаг 1. Добавить секреты в GitLab
Чтобы GitLab Runner мог безопасно общаться с нашим сервером Significa, нам нужно добавить в проект три переменные окружения. Сделать это можно в настройках проекта: Settings → CI/CD → Variables.
Название | Тип | Описание |
CI_PROJECT_TOKEN | Переменная | Токен проекта GitLab с правами API (Settings → Access tokens → Add new token). Нужен для управления сборками после загрузки. |
SIGNIFICA_SECRET | Переменная | Секретный ключ для доступа к Significa API. |
SIGNIFICA_SERVER_URL | Переменная | Базовый адрес сервера Significa (например, https://example.com). |
Зачем нужен CI_PROJECT_TOKEN?
Это «умная» часть автоматизации. После успешной загрузки сборки наш скрипт сохранит ее уникальный ID из Significa в переменную проекта GitLab. Это позвол��т позже, при закрытии Merge Request или удалении ветки, автоматически удалить устаревшую сборку с сервера дистрибуции.
Шаг 2. Подключить новые шаги деплоя в .gitlab-ci.yml
Теперь нужно расширить конфигурацию, использовав новые шаблоны для деплоя. Обновляем файл .gitlab-ci.yml нашего Flutter-проекта:
.gitlab-ci.yml
include: - project: 'project-group/flutter-ci-templates' ref: v1.1.0 file: - '/build/android.gitlab-ci.yml' - '/build/ios.gitlab-ci.yml' - '/deploy/significa.gitlab-ci.yml' - '/common.gitlab-ci.yml' deploy_android: image: node:$NODE_VERSION stage: deploy needs: - build_android <<: *common_rules artifacts: reports: dotenv: $DEPLOY_ENV_PATH extends: - .before_script:deploy_android - .script:deploy_android - .variables:deploy_android environment: name: android/$CI_COMMIT_REF_SLUG url: $SIGNIFICA_SERVER_URL/get/$UPLOAD_ID on_stop: cleanup_android auto_stop_in: 1 month tags: - $MACOS_RUNNER_TAG cleanup_android: image: node:$NODE_VERSION stage: cleanup needs: [ ] rules: - if: | $CI_PROJECT_TOKEN != "" && ($CI_MERGE_REQUEST_IID || $CI_COMMIT_BRANCH =~ $CI_BRANCHES) when: manual allow_failure: true - when: never extends: - .before_script:cleanup_android - .script:cleanup_android - .variables:cleanup_android environment: name: android/$CI_COMMIT_REF_SLUG action: stop tags: - $MACOS_RUNNER_TAG deploy_ios: image: node:$NODE_VERSION stage: deploy needs: - build_ios rules: - if: $CI_MERGE_REQUEST_IID || $CI_COMMIT_BRANCH =~ $CI_BRANCHES when: on_success - when: never extends: - .before_script:deploy_ios - .script:deploy_ios - .variables:deploy_ios artifacts: reports: dotenv: $DEPLOY_ENV_PATH environment: name: ios/$CI_COMMIT_REF_SLUG url: $SIGNIFICA_SERVER_URL/get/$UPLOAD_ID on_stop: cleanup_ios auto_stop_in: 1 month tags: - $MACOS_RUNNER_TAG cleanup_ios: image: node:$NODE_VERSION stage: cleanup needs: [ ] rules: - if: | $CI_PROJECT_TOKEN != "" && ($CI_MERGE_REQUEST_IID || $CI_COMMIT_BRANCH =~ $CI_BRANCHES) when: manual allow_failure: true - when: never extends: - .before_script:cleanup_ios - .script:cleanup_ios - .variables:cleanup_ios environment: name: ios/$CI_COMMIT_REF_SLUG action: stop tags: - $MACOS_RUNNER_TAG
Далее коммитим и пушим новые изменения в удаленный репозиторий, создаем Merge Request и смотрим на результат выполненной работы.
Шаг 3. Проверить интеграцию
Автоматический деплой после успешной сборки
В пайплайне появятся две новые ключевые задачи: deploy_android и deploy_ios. Они будут автоматически запускаться после успешной сборки, загружать артефакт (.apk/.ipa) на сервер Significa и выдавать вам готовую ссылку для установки.
Наглядный пример: у запущенного для Merge Request пайплайна видна стадия deploy, которая автоматически выполняется после стадии build.

Автоматическая очистка и управление артефактами
Чтобы не захламлять сервер устаревшими сборками, мы также реализовали стадию cleanup. Она срабатывает автоматически в одном из двух сценариев:
При вливании Merge Request. Сборки, созданные для целей код-ревью и тестирования этой ветки, становятся неактуальными и удаляются.
При удалении ветки. Если ветка была удалена вручную, то все связанные с ней артефакты также очищаются.
Наглядный пример: стадия cleanup, которую можно выполнить вручную или дождаться когда сработает автоматический триггер для ее запуска.

Следовательно, сервер всегда будет содержать только актуальные версии приложения, что экономит дисковое пространство и снижает риск скачать устаревшую версию.
Централизованное управление окружениями в GitLab
Теперь каждое выполнение пайплайна, связанное с деплоем, автоматически создает или обновляет запись в соответствующем окружении, используя встроенные возможности GitLab Environments:
Все сборки, их статус (active, stopped), коммит и ссылка для скачивания доступны в одном месте.
Не нужно искать ссылки в логах пайплайна или чатах. Достаточно пе��ейти в раздел Environments в вашем проекте GitLab.
Можно вручную остановить старую среду, чтобы удалить все сборки, связанные с ней.
В интерфейсе Merge Request отображается ссылка на развернутую сборку, что ускоряет процесс проверки.
Наглядный пример: список всех активных окружений в проекте с прямыми ссылками на последние развернутые сборки.

Таким образом, интеграция с Significa сокращает ваше время от коммита до получения сборки с часов до минут, минимизирует человеческие ошибки и предоставляет команде удобный инструмент для проверки промежуточных версий приложения.
Деплой приложения в Google Play и App Store
На этом этапе мы автоматизируем процесс выкладки приложения в официальные магазины. Для обеспечения чистоты и безопасности производственного процесса мы создадим отдельный репозиторий, который будет отвечать исключительно за публикацию и управление релизами.
Шаг 1. Создать новый репозиторий
Создайте новый проект (например, flutter-infra) в GitLab и в корне этого репозитория поместите файл .gitlab-ci.yml со следующим содержимым:
.gitlab-ci.yml
stages: - build - deploy include: - project: 'project-group/flutter-ci-templates' ref: v1.1.0 file: - '/build/android.gitlab-ci.yml' - '/build/ios.gitlab-ci.yml' - '/deploy/google_play.gitlab-ci.yml' - '/deploy/testflight.gitlab-ci.yml' - '/common.gitlab-ci.yml' variables: BUILD_AAB_ARCHIVE: true BUILD_AAB_FILE_NAME: "app-$BUILD_TYPE.aab" BUILD_AAB_FILE_PATH: "$CI_PROJECT_DIR/build/$BUILD_AAB_FILE_NAME" BUILD_APK_FILE_NAME: "app-$BUILD_TYPE.apk" BUILD_APK_FILE_PATH: "$CI_PROJECT_DIR/build/$BUILD_APK_FILE_NAME" BUILD_IPA_FILE_NAME: "Runner.ipa" BUILD_IPA_FILE_PATH: "$CI_PROJECT_DIR/build/$BUILD_IPA_FILE_NAME" BUILD_TYPE: release DART_DEFINE_JSON_REQUIRED_KEYS: '[ "ENV_EXAMPLE_1", "ENV_EXAMPLE_2" ]' DART_DEFINE_PATH: "./launch.json" FASTLANE_TEAM_ID: "EXAMPLE_TEAM_ID" FLUTTER_SDK_VERSION: 3.35.1 GIT_CLONE_PROJECT_PATH: "${CI_PROJECT_NAMESPACE}/flutter-example" GIT_CLONE_REF_NAME: "v1.0.0" KEYCHAIN_NAME: "${CI_PROJECT_NAME}-${CI_COMMIT_REF_SLUG}.keychain-db" LINUX_RUNNER_TAG: linux-runner-tag-name MACOS_RUNNER_TAG: macos-runner-tag-name PROJECT_ENTRY_POINT: "./lib/main.dart" SIGN_TYPE: "appstore" SUPPLY_TRACK: "beta" TARGET_BUNDLE_MAPPING: | [ { "bundle_id": "com.example.myapp", "target_name": "Runner" } ] default: interruptible: true .build_project: &build_project - echo -e "\e[0Ksection_start:`date +%s`:build_project_section[collapsed=true]\r\e[0KBuild project" - flutter pub get - dart run build_runner build --delete-conflicting-outputs - echo -e "\e[0Ksection_end:`date +%s`:build_project_section\r\e[0K" build_android: image: ghcr.io/cirruslabs/flutter:$FLUTTER_SDK_VERSION stage: build needs: [ ] script: - echo -e "\e[0Ksection_start:`date +%s`:build_app_section\r\e[0KBuild & sign app in $BUILD_TYPE mode" - *build_project - !reference [ .build_android, script ] - echo -e "\e[0Ksection_end:`date +%s`:build_app_section\r\e[0K" when: manual allow_failure: false artifacts: name: "android-$CI_COMMIT_REF_SLUG" expire_in: 1 week paths: - $BUILD_APK_FILE_PATH - $BUILD_AAB_FILE_PATH extends: - .before_script:build_android - .variables:build_android tags: - $MACOS_RUNNER_TAG build_ios: stage: build needs: [ ] script: - *build_project - !reference [ .build_ios, script ] when: manual allow_failure: false artifacts: name: "ios-$CI_COMMIT_REF_SLUG" expire_in: 1 week paths: - $BUILD_IPA_FILE_PATH extends: - .before_script:build_ios - .after_script:build_ios tags: - $MACOS_RUNNER_TAG deploy_testflight: stage: deploy needs: - build_ios when: manual extends: - .before_script:deploy_testflight - .script:deploy_testflight - .variables:deploy_testflight tags: - $MACOS_RUNNER_TAG deploy_google_play: stage: deploy needs: - build_android when: manual extends: - .before_script:deploy_google_play - .script:deploy_google_play - .variables:deploy_google_play tags: - $MACOS_RUNNER_TAG
Важно: на этом этапе главное — создать структуру пайплайна. Конкретные команды сборки и переменные нужно настроить на следующем шаге.
Шаг 2. Задать необходимые переменные
Рассмотрим переменные окружения, которые непосредственно участвуют при сборке и дистрибуции приложения.
Название | Описание |
BUILD_AAB_ARCHIVE | Определяет, нужно ли собирать Android App Bundle (AAB) в дополнение к APK. |
BUILD_AAB_FILE_NAME | Имя итогового AAB-файла (Android App Bundle) после сборки. |
BUILD_AAB_FILE_PATH | Полный путь до папки с AAB-файлом. |
BUILD_APK_FILE_NAME | Имя итогового APK-файла (Android Package) после сборки. |
BUILD_APK_FILE_PATH | Полный путь до папки с APK-файлом. |
BUILD_IPA_FILE_NAME | Имя итогового IPA-файла после сборки. |
BUILD_IPA_FILE_PATH | Полный путь до папки с IPA файлом. |
BUILD_TYPE | Тип сборки проекта. Возможные значения: debug, release. |
DART_DEFINE_JSON_REQUIRED_KEYS | Список ключей переменных окружений, которые обязательно должны быть переданы во время сборки приложения. |
DART_DEFINE_PATH | Путь, по которому будет формироваться временный файл с переменными окружения из DART_DEFINE_JSON (см. подробнее в следующем разделе). |
FASTLANE_TEAM_ID | Идентификатор команды разработчика на портале Apple Developer (в разделе Membership details). |
FLUTTER_SDK_VERSION | Версия Flutter SDK, используемая для сборки. |
GIT_CLONE_PROJECT_PATH | Полный путь в GitLab до репозитория с мобильным приложением для клонирования проекта. |
GIT_CLONE_REF_NAME | Название тега, хэша коммита или ветки для сборки проекта из репозитория с мобильным приложением. |
KEYCHAIN_NAME | Имя для создания временной связки ключей для импортирования iOSPKCS#12. |
LINUX_RUNNER_TAG | Опционально. Имя тега для использования раннера на машине под управлением Linux. Можно использовать в build_android и других процессах, которые не требуют выполнения команд на macOS. |
MACOS_RUNNER_TAG | Имя тега для использования раннера на машине под управлением macOS (как создать Gitlab Runner см. в предыдущей части статьи). |
PROJECT_ENTRY_POINT | Путь к главному файлу для запуска приложения. |
SIGN_TYPE | Режим подписи iOS приложения: adhoc, development, appstore. |
SUPPLY_TRACK | Группа дистрибуции приложения после публикации в Google Play Console. Доступные значения: production, beta, alpha, internal. |
TARGET_BUNDLE_MAPPING | Сопоставление iOS Target проекта к его Bundle ID и Profile Name в формате JSON (подробнее см. в следующем шаге). |
Ключевой момент: как можно заметить, некоторые переменные явно задаются в файле .gitlab-ci.yml, а не на уровне проекта GitLab. Это нужно для того, чтобы было проще отслеживать изменения в системе контроля версий.
Шаг 3. Настроить доступ к репозиторию приложения
Инфраструктура требует наличие доступа на уровне CI_JOB_TOKEN к репозиторию с Flutter-приложением. Для этого:
Перейдите в основной репозиторий вашего приложения в GitLab.
Откройте раздел Settings → CI/CD.
В подразделе Job token permissions найдите таблицу CI/CD job token allowlist.
Добавьте в нее путь к вашему репозиторию инфраструктуры в формате «группа/проект» (например, project-group/flutter-infra) и нажмите на кнопку Save changes.

После этого пайплайн в flutter-infra сможет склонировать код вашего приложения с помощью CI_JOB_TOKEN.
Шаг 4. Загрузить секретные ключи в Secure Files
Для подписи приложения и публикации в магазинах нужны закрытые ключи. Загрузите их через интерфейс GitLab в разделе Settings → CI/CD → Secure Files (см. подробнее в предыдущей части).
Шаг 5. Нас��роить интеграции с магазинами приложений в GitLab
Для автоматической загрузки сборок в Google Play и App Store GitLab предоставляет встроенные интеграции. Их нужно активировать и настроить один раз в вашем проекте flutter-infra.
Общий принцип:
Вы создаете сервисный аккаунт (для Google Play) или ключ API (для App Store Connect) в соответствующих консолях разработчика.
Вы вводите данные этого аккаунта (файл JSON или ключ) в настройках проекта GitLab.
GitLab CI/CD получает безопасный доступ к магазинам и может использовать команды (вроде fastlane supply) для публикации.
Google Play
Настроить интеграцию можно на уровне проекта GitLab в разделе: Settings → Integrations → Google Play, следуя официальной документации.

App Store Connect
Настроить интеграцию можно на уровне проекта GitLab в разделе: Settings → Integrations → Apple App Store Connect, следуя официальной документации.

Шаг 6. Проверить работу пайплайна
Теперь протестируем настройку. Отправьте изменения в репозиторий GitLab и создайте новый Merge Request. При успешной настройке в интерфейсе GitLab отобразятся все запланированные этапы:

После успешного завершения стадий build_android или build_ios в пайплайне появятся две ручные стадии — deploy_google_play и deploy_testflight. Они отвечают за публикацию собранных артефактов (APK/IPA) в соответствующие магазины приложений. Запустить их нужно вручную через интерфейс GitLab, что позволяет контролировать момент выпуска новой версии.
Заключение
Вот и все! Вы прошли весь путь от настройки физической машины до полной автоматизации доставки мобильных Flutter-приложений. Теперь ваш процесс разработки стал быстрее, эффективнее и надежнее. Вы можете быть уверены, что каждая новая версия вашего приложения попадет к вашим пользователям без задержек и ошибок.
Надеюсь, инструкция была понятной и у вас все взлетело. Если в процессе настройки у вас возникли вопросы или нужны уточнения – задавайте их в комментариях, я буду рад помочь.
Удачи в разработке и пусть выпуск ваших релизов будет быстрым!
