Автоматизация развертывания Android-приложений в Google Play Store
Настройка автоматизированных сборок и развертываний Android — от конфигурации Gradle до публикации в Play Store через CI/CD.

Выпуск Android-приложения в Play Store кажется простым — собрать APK, загрузить, готово. На практике процесс включает конфигурации подписи, варианты сборки, форматы пакетов, треки Play Store, поэтапные развертывания и достаточно конфигурации Gradle, чтобы заставить любого усомниться в выборе своей карьеры. После автоматизации развертываний Android в нескольких проектах, включая Flutter-приложения, которым нужны как iOS, так и Android-пайплайны, у меня есть настройка, которая обрабатывает все от коммита до выпуска в продакшн без ручного вмешательства.
Система сборки Android
Прежде чем что-либо автоматизировать, вам необходимо хорошо понимать, как работают сборки Android. В отличие от iOS, где Xcode обрабатывает большую часть сложности через графический интерфейс, система сборки Android полностью основана на Gradle и настраивается с помощью кода. Это на самом деле преимущество для автоматизации — все явно и контролируется версиями.
Конфигурация Gradle для релизных сборок
Ваш android/app/build.gradle (или build.gradle.kts, если вы перешли на Kotlin DSL) — это центральный файл конфигурации. Для релизных сборок вам необходимо настроить подпись, минификацию и оптимизацию.
android {
compileSdkVersion 35
defaultConfig {
applicationId "com.yourcompany.yourapp"
minSdkVersion 24
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
signingConfigs {
release {
keyAlias System.getenv("KEY_ALIAS") ?: properties["keyAlias"]
keyPassword System.getenv("KEY_PASSWORD") ?: properties["keyPassword"]
storeFile file(System.getenv("KEYSTORE_PATH") ?: properties["keystorePath"] ?: "keystore.jks")
storePassword System.getenv("STORE_PASSWORD") ?: properties["storePassword"]
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Конфигурация подписи сначала считывается из переменных окружения, затем возвращается к локальному файлу свойств. Этот шаблон позволяет разработчикам подписывать сборки локально с помощью файла key.properties, в то время как CI использует переменные окружения — один и тот же файл Gradle работает в обоих контекстах без изменений.
Локальный файл свойств
Для локальной разработки создайте файл key.properties в вашей директории android/ (и немедленно добавьте его в .gitignore):
storePassword=your_store_password
keyPassword=your_key_password
keyAlias=your_key_alias
keystorePath=../keys/release-keystore.jks
Затем сошлитесь на него в вашем build.gradle:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
Конфигурация подписи
Подписание Android-приложений проще, чем подписание кода iOS, но оно несет постоянный риск: если вы потеряете свой ключ загрузки, вы потеряете возможность обновлять свое приложение. Google представил Play App Signing для смягчения этой проблемы, и я настоятельно рекомендую его использовать.
Подписание приложений Google Play
При включенном Play App Signing Google хранит фактический ключ распространения. Вы подписываете свои загрузки ключом загрузки, а Google повторно подписывает их ключом распространения перед доставкой пользователям. Преимущества:
- Восстановление при потере ключа: Если вы потеряете свой ключ загрузки, Google может его сбросить. Без Play App Signing потеря ключа означает создание нового листинга приложения.
- Меньшие APK: Google может оптимизировать приложение для каждой конфигурации устройства, используя ключ распространения.
- Ротация ключей: Вы можете ротировать свой ключ загрузки без влияния на установленных пользователей.
Включите его в Play Console в разделе «Настройка» > «Подписание приложений». Для новых приложений он включен по умолчанию.
Генерация хранилища ключей
Если вам нужно создать новое хранилище ключей:
keytool -genkey -v \
-keystore release-keystore.jks \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias your_key_alias
Надежно храните этот файл хранилища ключей. Для CI я кодирую его в base64 и храню как секрет GitHub Actions:
base64 -i release-keystore.jks | pbcopy
Затем декодируйте его в рабочем процессе CI перед сборкой:
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
AAB против APK
Google теперь требует Android App Bundles (AAB) для всех новых приложений в Play Store. Разница важна для вашего конвейера сборки.
APK (Android Package) — это единый файл, содержащий все для всех конфигураций устройств. Он больше, но универсален — любое Android-устройство может установить любой APK. APK по-прежнему полезны для внутреннего распространения, тестирования на физических устройствах и распространения за пределами Play Store.
AAB (Android App Bundle) содержит весь код и ресурсы, но позволяет Google Play генерировать оптимизированные APK для каждого устройства. Пользователь с телефоном Pixel загружает только те ресурсы, которые ему нужны, а не ресурсы для каждой плотности экрана и архитектуры ЦП. Пакеты обычно на 15-30% меньше универсальных APK.
Для вашего конвейера CI соберите оба:
# Для Play Store
flutter build appbundle --release
# Для внутреннего тестирования / прямой установки
flutter build apk --release --split-per-abi
Флаг --split-per-abi генерирует отдельные APK для каждой архитектуры ЦП (arm64-v8a, armeabi-v7a, x86_64), что уменьшает размер файла для прямого распространения.
Fastlane Supply для Play Store
Действие supply Fastlane обрабатывает загрузки в Play Store и управление метаданными, отражая то, что deliver делает для iOS.
Настройка Supply
Во-первых, вам нужна учетная запись службы Google Play с соответствующими разрешениями.
- Перейдите в Google Cloud Console и создайте учетную запись службы.
- Загрузите файл JSON-ключа.
- В Play Console перейдите в «Настройки» > «Доступ к API» и свяжите учетную запись службы.
- Предоставьте ей разрешения "Менеджер релизов" для вашего приложения.
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_KEY"] || "path/to/service-account.json")
package_name("com.yourcompany.yourapp")
Базовая загрузка
default_platform(:android)
platform :android do
desc "Deploy to Play Store Internal Testing"
lane :internal do
gradle(
task: "bundle",
build_type: "Release",
project_dir: "./"
)
supply(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
end
Для проектов Flutter я предпочитаю запускать команду сборки Flutter напрямую, а не использовать задачу Gradle через Fastlane, потому что процесс сборки Flutter включает шаги компиляции Dart, которые Gradle сам по себе не обрабатывает корректно:
lane :internal do
Dir.chdir("..") do
sh("flutter build appbundle --release")
end
supply(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
Треки Play Store
В Play Store есть четыре трека, и понимание того, когда использовать каждый из них, важно для вашей стратегии выпуска.
Внутреннее тестирование
- До 100 тестировщиков.
- Проверка не требуется — сборки доступны почти мгновенно.
- Лучше всего подходит для: тестирования командой разработки, циклов QA, проверки сборок перед продвижением.
Закрытое тестирование (Альфа)
- Неограниченное количество тестировщиков, но вы управляете списком.
- Требует краткой проверки от Google (обычно несколько часов).
- Лучше всего подходит для: бета-программ с приглашенными пользователями, предварительного просмотра для клиентов.
Открытое тестирование (Бета)
- Любой может присоединиться из листинга Play Store.
- Требует проверки.
- Лучше всего подходит для: публичных бета-программ, сбора отзывов перед широким выпуском.
Продакшн
- Доступно всем пользователям.
- Требуется полная проверка.
- Поддерживает поэтапные развертывания.
Продвижение между треками
Fastlane делает продвижение по трекам простым:
lane :promote_to_beta do
supply(
track: "internal",
track_promote_to: "beta",
skip_upload_changelogs: false,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
lane :promote_to_production do
supply(
track: "beta",
track_promote_to: "production",
rollout: "0.1", # 10% staged rollout
skip_upload_changelogs: false,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
Поэтапные развертывания
Поэтапные развертывания — одно из самых больших преимуществ Android перед iOS. Вы можете выпускать приложение для определенного процента пользователей и постепенно увеличивать его, отслеживая частоту сбоев и отзывы пользователей на каждом этапе.
lane :staged_rollout do |options|
percentage = options[:percentage] || "0.1"
supply(
track: "production",
rollout: percentage,
aab: "../build/app/outputs/bundle/release/app-release.aab"
)
end
lane :increase_rollout do |options|
percentage = options[:percentage] || "0.5"
supply(
track: "production",
rollout: percentage,
skip_upload_aab: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
lane :complete_rollout do
supply(
track: "production",
rollout: "1.0",
skip_upload_aab: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
Типичный график развертывания:
- День 1: 10% — отслеживание частоты сбоев в Firebase Crashlytics.
- Дни 2-3: 25% — проверка отзывов пользователей и обращений в поддержку.
- Дни 4-5: 50% — проверка отсутствия регрессий производительности в масштабе.
- Дни 6-7: 100% — полный выпуск.
Если что-то пойдет не так на любом этапе, вы можете остановить развертывание и выпустить исправление, не затрагивая всех пользователей.
Рабочий процесс GitHub Actions
Вот полный рабочий процесс CI/CD для Android:
name: Deploy to Play Store
on:
push:
branches: [main]
workflow_dispatch:
inputs:
track:
description: 'Play Store track'
required: true
default: 'internal'
type: choice
options:
- internal
- alpha
- beta
- production
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.x'
channel: stable
cache: true
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: android
- name: Flutter dependencies
run: flutter pub get
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
- name: Create service account file
run: echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}' > android/play-store-key.json
- name: Build and deploy
working-directory: android
env:
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEYSTORE_PATH: app/keystore.jks
PLAY_STORE_JSON_KEY: play-store-key.json
run: bundle exec fastlane ${{ github.event.inputs.track || 'internal' }}
- name: Cleanup secrets
if: always()
run: |
rm -f android/app/keystore.jks
rm -f android/play-store-key.json
Несколько моментов, которые следует отметить в этом рабочем процессе:
Ubuntu-раннеры отлично подходят для Android. В отличие от iOS, которая требует macOS, сборки Android работают на Linux. Ubuntu-раннеры дешевле и запускаются быстрее в GitHub Actions.
Требуется Java 17 для текущих версий Android Gradle Plugin. Если ваш проект использует более старую версию AGP, вам может понадобиться Java 11, но это становится все реже.
Шаг очистки удаляет декодированные секреты, даже если сборка завершается неудачей. Это мера глубокой защиты — раннеры GitHub Actions являются эфемерными, но это хорошая практика.
Управление версиями
Android использует два идентификатора версии: versionCode (целое число, которое должно увеличиваться с каждой загрузкой) и versionName (человекочитаемая строка версии).
Для проектов Flutter оба берутся из pubspec.yaml:
version: 1.2.3+45
# 1.2.3 = versionName
# 45 = versionCode
Я автоматизирую увеличение кода версии, используя номер запуска CI:
lane :set_version_code do
build_number = ENV["GITHUB_RUN_NUMBER"] || "1"
Dir.chdir("..") do
current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
sh("sed -i 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
end
end
Это гарантирует, что код версии всегда увеличивается, даже между ветками. Если вам нужен больший контроль, вы можете получить последний код версии из Play Store:
lane :smart_version_code do
current = google_play_track_version_codes(track: "internal").max || 0
new_code = current + 1
Dir.chdir("..") do
current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
sh("sed -i 's/^version: .*/version: #{current_version}+#{new_code}/' pubspec.yaml")
end
end
Управление листингами Play Store
Supply может управлять всем вашим листингом Play Store из файлов, контролируемых версиями:
android/fastlane/metadata/android/
├── en-US/
│ ├── title.txt
│ ├── short_description.txt
│ ├── full_description.txt
│ ├── changelogs/
│ │ ├── default.txt
│ │ └── 45.txt # Changelog for versionCode 45
│ └── images/
│ ├── phoneScreenshots/
│ │ ├── 1_home.png
│ │ └── 2_detail.png
│ ├── featureGraphic.png
│ └── icon.png
└── ru-RU/
├── title.txt
├── short_description.txt
└── full_description.txt
Чтобы загрузить ваш текущий листинг в качестве отправной точки:
fastlane supply init
Затем обновите файлы и отправьте изменения:
lane :update_listing do
supply(
skip_upload_aab: true,
skip_upload_apk: true
)
end
Конфигурация ProGuard и R8
Когда minifyEnabled имеет значение true, R8 (преемник ProGuard) сжимает, обфусцирует и оптимизирует ваш код. Это важно для производственных сборок — это уменьшает размер APK и затрудняет обратную разработку — но также может сломать что-либо, если не настроено должным образом.
Распространенные проблемы и их правила ProGuard:
# Keep Flutter's classes
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# Keep Firebase classes
-keep class com.google.firebase.** { *; }
# Keep models used with JSON serialization
-keep class com.yourcompany.yourapp.models.** { *; }
# Keep classes referenced via reflection
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
# Common crash fix: OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
Тщательно протестируйте свою релизную сборку — проблемы R8 часто проявляются в виде сбоев во время выполнения, которые не появляются в отладочных сборках. Флаги --obfuscate и --split-debug-info во Flutter позволяют проводить дальнейшие оптимизации:
flutter build appbundle --release --obfuscate --split-debug-info=build/symbols
Сохраните директорию build/symbols — она вам понадобится для символизации трассировок стека из отчетов о сбоях.
Полный Fastfile
default_platform(:android)
platform :android do
desc "Deploy to internal testing"
lane :internal do
set_version_code
build_release
supply(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
desc "Deploy to closed beta"
lane :beta do
set_version_code
build_release
supply(
track: "beta",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
desc "Deploy to production with staged rollout"
lane :production do
set_version_code
build_release
supply(
track: "production",
rollout: "0.1",
aab: "../build/app/outputs/bundle/release/app-release.aab"
)
end
desc "Promote internal to beta"
lane :promote_to_beta do
supply(
track: "internal",
track_promote_to: "beta"
)
end
desc "Increase production rollout"
lane :increase_rollout do |options|
supply(
track: "production",
rollout: options[:percentage] || "0.5",
skip_upload_aab: true
)
end
private_lane :build_release do
Dir.chdir("..") do
sh("flutter build appbundle --release --obfuscate --split-debug-info=build/symbols")
end
end
private_lane :set_version_code do
build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
Dir.chdir("..") do
current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
sh("sed -i '' 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
end
end
end
Отличия от автоматизации iOS
Автоматизировав обе платформы, стоит отметить несколько ключевых отличий.
- Подписание проще на Android. Один файл хранилища ключей против танца с сертификатами и профилями подготовки на iOS. Нет необходимости в match или любом другом инструменте управления сертификатами.
- Нет обязательного требования macOS. Android собирается на Linux, что означает более дешевые и быстрые CI-раннеры.
- Более быстрое время проверки. Процесс проверки Google обычно быстрее, чем у Apple, а загрузки во внутренний трек вообще не требуют проверки.
- Поэтапные развертывания являются нативными. На iOS вы можете выполнять поэтапные выпуски, но вы не можете контролировать процент или останавливать развертывание в середине с такой же детализацией.
- API Play Store более разрешительный. Вы можете управлять почти всем программно, включая эксперименты с листингом магазина и ценообразование. API App Store Connect от Apple имеет больше ограничений.
Основной недостаток — это время сборки Gradle. Чистая сборка Android с оптимизацией R8 может занять значительно больше времени, чем эквивалентная сборка iOS. Кэширование помогает — как встроенный кэш Gradle, так и кэширование директории .gradle на уровне CI.
Как выглядит зрелый пайплайн
После всей настройки повседневный опыт должен быть незаметным. Отправьте изменения в main, запустится сборка, и через несколько минут у тестировщиков будет новая версия во внутреннем треке. Отметьте релиз тегом, и производственная сборка пройдет с поэтапным развертыванием.
Пайплайн устраняет целую категорию проблем «работает на моей машине», гарантирует воспроизводимость каждой сборки и дает команде уверенность в том, что выпуск — это обыденное событие. Последняя часть — это настоящая цель — сделать развертывание настолько рутинным, чтобы никто о нем не думал.