В данной руководстве мы расскажем как автоматизировать процесс релизов Android-приложений в Google Play и Huawei AppStore (пока что без RuStore). Вы навсегда забудете как это делать вручную и сможете потратить время на что-нибудь более полезное, экономя сотни часов в год.

Содержание

Структура Gradle-проекта

Для начала наведем порядок в самом Android проекте. Вbuild.gradle проекта мы используем весьма стандартную матрицу из flavors x buildTypes:

  android {
  // <REDACTED>

  flavorDimensions 'default'

  productFlavors {
    google {
      dimension "default"
      versionName = android.defaultConfig.versionName + '-Google'
    }

    huawei {
      dimension "default"
      versionName = android.defaultConfig.versionName + '-Huawei'
    }
  }

  // <REDACTED>

  buildTypes {
    debug {
      applicationIdSuffix '.debug'
      versionNameSuffix '-debug'
      signingConfig signingConfigs.debug
      // <REDACTED>
    }
    beta {
      applicationIdSuffix '.beta'
      versionNameSuffix '-beta'
      signingConfig signingConfigs.release
      // <REDACTED>
    }
    release {
      signingConfig signingConfigs.release
      minifyEnabled true
      shrinkResources true
      // <REDACTED>
    }
}

Такая матрица дает комбинацию из GoogleDebug, GoogleBeta, GoogleRelease, HuaweiDebug, и т.д. В целом кастомизация по stores не обязательна, но полезна для управления специальными возможностями, таким как in-app. Настраивайте матрицу исходя из ваших потребностей.

Полезные ссылки:

Android App Bundles (AAB) vs Android Packages (APK)

С самого момента зарождения Android все приложения распространялись в виде так называемых .apk файлов, которые по сути представляют из себя ZIP архивы с скомпилированным кодом и ресурсами под различные архитектуры, локализации и API-levels. Вы компилируете свой код и ресурсы, собираете архив, подписываете его своей сигнатурой. А дальше можете распространять хоть через Google Play, хоть через RuStore, хоть просто на сайте выкладывать.

Ситуация поменялась в 2021 году. Google решил, что неплохо бы отправлять клиентами Google Play только те ресурсы, которые соответствуют API-level и архитектуре приложения. И действительно, зачем отправлять библиотеки armeabi-v7a для устройств arm64-v8a? Зачем устройствам с API level = 31 нужны compatibility файлы для API level = 21? Зачем локализации на 30 языках, если в системе всего один? В целом идея неплохая, но есть нюансы.

Чтобы всё это работало, Google придумал новый промежуточный формат Android App Bundles (.aab), который аналогично .apk вы собираете и подписываете своим ключом. Однако, .aab файлы не могут быть непосредственно установлены на устройства. Google Play для каждого конкретного устройства создает из .aab файла .apk файл с необходимым ресурсами и отправляет его на девайс. Вместо, условно, 50 мегабайт .apk файла вы качаете условные 20 мегабайт таргетированного .apk файла специально под ваше устройство. Profit!

Важно! При использовании Android App Bundles, финальная сборка и подпись APK будет находится на стороне Google (и, теперь и Huawei). Каждый app store использует свои ключи для подписи. Google - свои, Huawei - свои. Android работает таким образом, что установка новой версии приложения, подписанного другим ключом, невозможна без удаления предыдущей версии приложения вмести со всеми данными. Если ваши пользователи установят bundle приложения из Google Play, то они не смогут обновить его через Huawei.

Классический подход с APK не имеет данной проблемы. Однако, начиная с августа 2021 у вас больше нет выбора и все новые приложения надо загружать только в формате .aab, отдавая подпись на сторону Google Play. Теперь также можно забить на все эти split APK, а также APK expansion files и прочие костыли. Пользователи не будут качать ненужные мегабайты через скоростной EDGE^W5G Интернет. Больше свободного места на устройстве! И, в конце концов, практически полное отсутствие возможности перейти с Google Play на альтернативные сторы для таких приложений. Win-win-win.

Полезные ссылки:

Генерация номеров версий

Первая и одна из самых важных задач, которую необходимо решить для реализации continuous deployment хоть для онлайн сервиса, хоть для package software - это обеспечения детерминированного алгоритма генерации версий. Требование простое: для каждого git commit в релизной ветке должен существовать уникальный номер версии. Перестаньте уже делать "bump versions" (1.0.2 -> 1.0.3 -> 1.0.4 и т.д.) каждый раз вручную. Нужна система, где номер версии (хотя бы последняя его .patch-level часть) будет генерироваться автоматически.

Есть два неплохих подхода, которые позволяют закрыть данный вопрос раз и навсегда полностью.

Подход 1. Использование git describe

Особенность Android (и Google Play и AppGallery соответственно) в том, что downgrade версий приложений невозможен. Код номера версии всегда должен возрастать. Но ведь точно также и всегда растёт git история релизной ветки, если же, конечно, вы не хулиганите с rewrite истории и force-pushes.

Крайне логичным является использовать число git commit в ветке для нумерации. Можно, например, сделать так, что вы периодически ставите annotated tag вида v$major.$minor (например, v1.0, v2.0), тогда как третий компонент (patch) генерируется как число коммитов от предыдущего тега.

Для реализации удобнее всего использовать команду git describe --long:

> git tag -a v1.0 -m "New minor release"
> git describe --long
v1.0-0-gbc2d1a8

> touch test2.txt
> git add .
> git commit -m "Add some changes"
> git describe --long
v1.0-1-gaa7263d
...

> touch test3.txt
> git add .
> git commit -m "Add some more changes"
> git describe --long
v1.0-2-g6a48c84

Данный подход дает вполне себе semver v1.0.0, v1.0.1, v1.0.2 и т.д, где последняя цифра версии (patch) назначается автоматически, а major.minor контролируется вами. Для каждого git commit у вас будет уникальный номер версии. Найти git коммит по тегу и числу коммитов от него также не представляется сложным. Просто и практично.

Подход 2. Использование даты

Более радикальным подходом является использование даты git коммита в качестве версии. Например, 2023.04.23-2. А как же semver, спросите вы? Ну а вот а зачем он вам в мире Android приложений, если у вас всё равно идет непрерывное обновление приложений из Google Play и нет никакого способа откатиться назад или обеспечить доступность нескольких версий? Можете жарко поспорить об этом в комментариях.

В git есть две даты:

  • Author Date - то, что указал автор и то, что вы можете видеть в git log и git show. Может быть насколько угодно в прошлом, особенно если вы мержите какой-нибудь PR годовалой давности.

  • Commit Date - дата, когда коммит был добавлен в бранч. Обычно соответствует реальному времени, когда merge произошел в бранч. Время всегда идет вперед и Commit Date не исключение, если же, конечно, вы не будете намеренно экспериментировать с показанием ваших часов.

Мы будем использовать Commit Date, которую можно получить через %cd форматинг в git log:

TZ=UTC0 git log --max-count=128 --pretty=format:%cd --date=iso-local

2023-04-23 10:37:01 +0000
2023-04-23 09:02:18 +0000
2023-04-19 14:43:21 +0000
2023-04-19 14:43:21 +0000
2023-04-19 01:39:03 +0000

<REDACTED>

Одной даты недостаточно, если же, конечно, вы не хотите себя ограничить одним коммитом в день. В качестве дополнительной цифры будем считать число коммитов с начала дня. Получим что-то вроде 2023.04.23-1, 2023.04.23-3, 2023.04.23-4 и т.д. Также можно взять в качестве последней цифры час x 60 + минуты или даже просто час (up to you). Но нам вариант с числом коммитов с начала дня показался вполне рабочим.

Не забывайте, в конечном итоге всё это надо запихать в т.н. versionCode, который ограничен пространством 0 - 2_100_000_000 (чуть меньше 31 бита). Version Code - это именно то, с чем работает Android и stores для версионирования, тогда как Version Name это просто строка с текстом где может быть всё, что вы пожелаете показать пользователю.

Для генерации версии будем использовать следующий код в Gradle (смотрите внимательно комментарии):

def getVersion() {
  def time = Integer.parseInt(run(['git', 'log', '-1', '--format=%ct']).trim())
  def cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.ENGLISH)
  cal.setTimeInMillis((long) time * 1000)
  def year = cal.get(Calendar.YEAR)
  def month = cal.get(Calendar.MONTH) + 1
  def day = cal.get(Calendar.DAY_OF_MONTH)
  def date_of_last_commit = String.format("%04d-%02d-%02d", year, month, day)
  def build = Integer.parseInt(run(['git', 'rev-list', '--count', '--after="' + date_of_last_commit + 'T00:00:00Z"', 'HEAD']).trim())

  // Use the last git commit date to generate the version code:
  // RR_yy_MM_dd_CC
  // - RR - reserved to identify special markets, max value is 21.
  // - yy - year
  // - MM - month
  // - dd - day
  // - CC - the number of commits from the current day
  // 21_00_00_00_00 is the the greatest value Google Play allows for versionCode.
  // See https://developer.android.com/studio/publish/versioning for details.
  def versionCode = (year - 2000) * 1_00_00_00 + month * 1_00_00 + day * 1_00 + build

  // Use the current date to generate the version name:
  // 2021.04.11-12-Google (-Google added by flavor)
  def versionName = String.format("%04d.%02d.%02d-%d", year, month, day, build)

  return new Tuple2(versionCode, versionName)
 }

Дальше устанавливаете versionCode и versionName из результата вызова getVersion():

android {
  // <REDACTED>
  defaultConfig {
    def ver = getVersion()
    versionCode = ver.V1
    versionName = ver.V2
    println('Version: ' + versionName)
    println('VersionCode: ' + versionCode)
    // <REDACTED>
  }
  // <REDACTED>
}

Можете убедиться, что каждый запуск gradle будет генерировать уникальный номер версии для каждого git коммита. Данный подход закрывает вопрос версионирования навсегда и вы не думаете про это.

Получение ключей доступа от Google Play

Чтобы загружать что-либо в Google Play автоматизированно, надо для начала получить ключи доступа в API. Кажется, что Google Play весьма плотно интегрировался с IAM Google Cloud и вам нужно создать новый Service Account с необходимыми правами и получить .json файл с credentials. Но давайте по порядку.

Шаг 1. Связь Google Play Developer account Google Cloud project

Вроде как новые Google Play Developer Accounts должны связываться с Google Cloud как-то полумагически, особенно если вдруг вы использовали Firebase.

Идете в Google Play Console -> Setup -> API Access. Если Linked Project уже есть, то у вас всё хорошо и можно переходить к следующему шагу. Если нет, то проверьте в Google Cloud Console какие у вас уже есть проекты и, при необходимости, создайте новый. Firebase неявно создает Google Cloud проект и вы можете использовать его же.

Связывание Google Play Console и Google Cloud Console (не перепутайте)
Связывание Google Play Console и Google Cloud Console (не перепутайте)

Шаг 2. Создание Google Service Account

Service Account это такая сущность для "роботов", которая имеет credentials и на которую можно назначать роли и права доступа. Рекомендуется использовать robot Service Account, а не свой human Google Account.

Кликайте на "View in Google Cloud Platform" или же просто переходите в Google Cloud Console и выбирайте нужный проект в списке. Переходите в раздел "Service Accounts". Кликайте "Create Service Account".

Service Accounts в Google Cloud
Service Accounts в Google Cloud

Введите имя для Service Account, например, Google Play Automatic Upload. Введите ID (email) для Service Account, например google-play-uploaded. Жмите "CREATE AND CONTINUE".

Создание Service Account в Google Cloud COnsole
Создание Service Account в Google Cloud COnsole

Добавьте роль Viewer, без этого Service Account не добавляется в Google Play Console. Жмите "DONE", пропуская третий шаг ( Grant users access to this service account (optional)).

Добавление роли Viewer
Добавление роли Viewer

Шаг 3. Получение ключа доступа

Кликайте по только что созданному Service Account. Переходите на вкладку "Keys". Нажимайте кнопку "ADD KEY" -> "Create new" -> JSON.

Создание JSON ключа
Создание JSON ключа

После клика на "CREATE" будет скачан JSON файл с приватным ключом Service Account:

{
  "type": "service_account",
  "project_id": "<YOUR GCLOUD PROJECT>",
  "private_key_id": "<REDACTED>",
  "private_key": "-----BEGIN PRIVATE KEY-----\n<REDACTED>\n-----END PRIVATE KEY-----\n",
  "client_email": "google-play-uploaded@<YOUR GCLOUD PROJECT>.iam.gserviceaccount.com",
  "client_id": "<SOME BIG NUMBER>",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-play%40<YOUR GCLOUD PROJECT>.iam.gserviceaccount.com"
}

Храните этот ключ в надежном секретном месте!

Шаг 4. Добавление Google Cloud Service Account в Google Play Console

Последний шаг - дать права на загрузку приложений для нового Service Account. Кажется, эта часть пока еще не интегрирована до конца с Google Cloud и права необходимо выдавать в Google Play Console.

Переходите в Google Play Console, раздел "Users and permissions". Жмете на меню с трёмя точками, дальше "Invite new users".

Добавление Google Cloud Service Account в Google Play Console
Добавление Google Cloud Service Account в Google Play Console

Вводите google-play-uploaded@<YOUR GCLOUD PROJECT>.iam.gserviceaccount.com

Настройка прав для Google Service Account в Google Play Consolе
Настройка прав для Google Service Account в Google Play Consolе

Необходимо дать следущие права

  • View app information and download bulk reports (read only)
    View all app information, including any associated Play Games services projects – but not financial data. Users with this permission can also download bulk reports, and will be able to view any new apps that you add to Play Console in the future.

  • Create, edit and delete draft apps
    This permission does not allow users to publish apps on Google Play

  • Release to production, exclude devices and use Play app signing
    Create, edit and roll out releases to production, unpublish and republish apps, exclude devices in the device catalogue, and use app signing by Google Play to sign APKs. Users with this permission can publish apps to users on Google Play.

  • Release apps to testing tracks
    Upload draft apps; create, edit and rollout releases to testing tracks; unpublish and republish apps that have already been published to a testing track; upload and modify .obb files; edit release notes for apps which are not active in production; and upload app bundles for internal sharing. This permission does not allow users to publish apps to production on Google Play.

Готово. В результате выполнения инструкций в данном разделе у вас должен появиться .json файл с credentials доступа к Google Play, позволяющие загружать приложения.

Полезные ссылки:

Загрузка сборок в Google Play

Загрузку в Google Play будем осуществлять через Triple T Gradle Play Publisher плагин для Gradle. Альтернативой является всем известный Fastlane, который мы тоже используем, но только для iOS. Triple-T Play Publisher субъективно кажется более удобным как минимум за счет за счет интеграции с Gradle и отсутствия необходимости возиться с набором портянок на Ruby.

Добавление библиотеки

Добавим в build.gradle Gradle плагин com.github.triplet.gradle:play-publisher:

buildscript {
  repositories {
    google()
    mavenCentral()
  }

  // <REDACTED>

  dependencies {
    classpath 'com.android.tools.build:gradle:7.4.2'

    // <REDACTED>

    classpath("com.github.triplet.gradle:play-publisher:3.8.1")
  }
}

Версия в статье быстро устареет. Пожалуйста, всегда используйте последнюю доступную версию.

Добавление вызова плагина

Добавьте вызов плагина com.github.triplet.play в build.gradle сразу после секции `android {}`:

android {
  // <REDACTED>
}
apply plugin: 'com.github.triplet.play'

Настройка плагина

Плагин настраивается через глобальную секцию конфигурации play {}, а также дополнительно в каждом flavor:

android {
  defaultConfig {
  // <REDACTED>

  } // defaultConfig

  // <REDACTED>

  playConfigs {
    googleRelease {
      enabled.set(true)
    }
  } // playConfigs

} // android

play {
  enabled.set(false)
  track.set("production")
  userFraction.set(Double.valueOf(0.10)) // 10%
  defaultToAppBundles.set(true)
  releaseStatus.set(ReleaseStatus.DRAFT)
  serviceAccountCredentials.set(file("google-play.json"))
}
  • track - говорим в какой track заливать. Конечно же, сразу в продакшен!

  • userFraction - включаем stage rollout на 10% чтобы не быть совсем сумашедшими.

  • defaultToAppBundles - включаем Android App Bundles (AAB) вместо Android Packages (APK).

  • releaseStatus.set(ReleaseStatus.DRAFT) - создаем черновик, но пока сразу не отправляем на ревью. После тестирования можно будет поменять на ReleaseStatus.IN_PROGRESS.

А вы сейчас делаете stage rollout? Расскажите, пожалуйста, об этом в комментариях.

Тестирование загрузки в Google Play

Запускаем таск bundleGoogleRelease для сборки релизной версии aab приложения для последующей загрузки:

gradle bundleGoogleRelease

Проверяем, что у нас реально что-то собралось:

ls build/outputs/bundle/googleRelease
OrganicMaps-23043001-google-release.aab

Запускаем таску publishGoogleReleaseBundle для загрузки cобравшегося aab в Google Play:

gradle publishGoogleReleaseBundle
> Task :publishGoogleReleaseBundle
Starting App Bundle upload: OrganicMaps-23043001-google-release.aab
Uploading App Bundle: 10% complete
Uploading App Bundle: 19% complete
Uploading App Bundle: 29% complete
Uploading App Bundle: 39% complete
Uploading App Bundle: 49% complete
Uploading App Bundle: 58% complete
Uploading App Bundle: 68% complete
Uploading App Bundle: 78% complete
Uploading App Bundle: 88% complete
Uploading App Bundle: 97% complete
App Bundle upload complete
Updating [completed, inProgress] release (app.organicmaps:[23030505, 23040207]) in track 'production'

В целом часть с заливкой готова и вы сможете увидеть сможете увидеть черновик релиза Google Play Console и отправить его на review.

Сборка после заливки в Google Play
Сборка после заливки в Google Play

Если всё соответствует ожиданиями, то дальше можно поменять releaseStatus.set(ReleaseStatus.DRAFT)на releaseStatus.set(ReleaseStatus.IN_PROGRESS) чтобы лишний раз не бегать в Google Play Console для нажатия кнопки. Кнопка должна быть где-то в вашем CI/CD, чтобы вы там не использовали.

Полезные ссылки:

Управление метаданными Google Play

Следующий шаг - это научиться также обновлять метаданные (описание приложения, скриншоты и т.п.) в Google Play. После добавления данной автоматизации станет возможным обновлять метаданные в git репозитории вместо Google Play. Работу по обновлению в Google Play сделает плагин.

Скачивание метаданных из Google Play

Для начала необходимо скачать уже имеющиеся в Google Play метаданные в репозиторий. Запускаем задачу `bootstrapGoogleReleaseListing`:

gradlew bootstrapGoogleReleaseListing

Пошуршав диском, gradle выкачает все текущие метаданные в репозиторий.

> Configure project :
Building with Google Mobile Services
Building without Google Firebase Services
Version: 2023.04.23-2
VersionCode: 23042302
Building for [armeabi-v7a, arm64-v8a, x86_64] archs.
Create separate apks: false

> Task :bootstrapGoogleReleaseListing
Downloading app details
Downloading listings
Downloading in-app products
Downloading release notes
Downloading en-US listing
Downloading tr-TR listing
<REDACTED>

Downloading en-US listing graphics for type 'featureGraphic'
Downloading en-US listing graphics for type 'icon'
Downloading en-US listing graphics for type 'phoneScreenshots'
Downloading en-US listing graphics for type 'tenInchScreenshots'
Downloading en-US listing graphics for type 'sevenInchScreenshots'
Downloading tr-TR listing graphics for type 'sevenInchScreenshots'
Downloading tr-TR listing graphics for type 'phoneScreenshots'
<-<----<
BUILD SUCCESSFUL in 44s
1 actionable task: 1 executed

Новые файлы появятся в src/<FLAVOR>/play:

contact-email.txt
contact-website.txt
default-language.txt
listings/
  en-US
    full-description.txt
    graphics/
      icon/
        1.png
      phone-screenshots/
        1.jpg
        2.jpg
        3.jpg
        4.jpg
    release-notes.txt
    short-description.txt
    title.txt
    video-url.txt
release-notes/
  en-US/
    default.txt

Редактирование данных

После скачивания данных из Google Play добавьте их в git и далее используйте git как первоисточник, избегая ручных действий в Google Play Console. Это даёт огромное число плюсов, начиная от понятного версионирования, до возможности добавления инструментов для переводов. Например, мы используем Weblate для перевода описаний Google Play на другие языки. Данные Google Play также будут переиспользованы далее для Huawei AppGallery.

Загрузка метаданных в Google Play

Для загрузки обновленных метаданных обратно в Google Play существует задача publishGoogleReleaseListing (где GoogleRelease - ваш flavor):

./gradlew publishGoogleReleaseListing

<REDACTED>

> Configure project :
Building with Google Mobile Services
Building without Google Firebase Services
Version: 2023.05.01-1
VersionCode: 23050101
Building for [armeabi-v7a, arm64-v8a, x86_64] archs.
Create separate apks: false

> Task :publishGoogleReleaseListing
Uploading app details
Uploading ar listing
<REDACTED>
Uploading zh-TW listing

Важно. Обновление метаданных в Google Play не привязано к выпуску релизов. Вы можете обновлять описания и скриншоты приложений в любой момент, вне зависимости от текущего состояния review сборки приложения. Review изменения метаданных происходит отдельно!

Метаданные в Google Play Console
Метаданные в Google Play Console

Мы используем данный подход чтобы обновлять данные на всех 59 языках, поддерживаемых Google Play Console на момент написания статьи.

Полезные ссылки:

Получение ключей доступа от Huawei AppGallery

Чтобы загружать что-то в Huawei AppGallery нам необходимо сначала получить ключи доступа к Huawei AppGallery Connect. Интерфейс Huawei AppGallery Connect, скажем так, местами весьма запутан. Но надо разобраться один раз чтобы в дальнейшем туда вообще не заходить.

Заходите в AppGallery Connect и нажимайте Users and permissions. Переходите к API key > Connect API и нажимайте Create:

Создание API client для Huawei App Gallery Connect
Создание API client для Huawei App Gallery Connect

Можете ввести любое название. Укажите следующие роли:

  • Development

  • Operations

  • Customer service (будет выбрано автоматически)

Создание нового API Client
Создание нового API Client

После этого вы сможете скачать .json ключ доступа к Huawei AppGallery Connect:

{
        "type":"team_client_id",
        "developer_id":"<BIG NUMBER>",
        "client_id":"<ANOTHER BIG NUMBER>",
        "client_secret":"<REDACATED>",
        "configuration_version":"3.0"
}

Готово. Данный файл понадобится на следущем этапе для автоматизации загрузки.

Полезные ссылки:

Загрузка сборок и release notes в Huawei AppGallery

Для автоматизации загрузки в Huawei AppGallery будем использовать Gradle plugin ru.cian:huawei-publish-gradle-plugin, разрабатываемый Aleksandr Mirko из Омска. Скажем спасибо Александру за столь прекрасный безальтернативный плагин для загрузки в Huawei AppGallery без которого пришлось бы страдать.

Добавление плагина в Gradle

Добавляем Gradle-plugin ru.cian:huawei-publish-gradle-plugin в build.gradle:

buildscript {
  repositories {
    google()
    mavenCentral()
  }

  // <REDACTED>

  dependencies {
    classpath 'com.android.tools.build:gradle:7.4.2'

    // <REDACTED>

    classpath("ru.cian:huawei-publish-gradle-plugin:1.3.6")
  }
}

Включение плагина

Включаем плагин ru.cian.huawei-publish-gradle-plugin в build.gradle после секции android {}:

apply plugin: 'ru.cian.huawei-publish-gradle-plugin'

Настройка плагина

Плагин имеет отдельную секцию для конфигурции huaweiPublish {}. Добавляем новую секцию с настройками в конец build.gradle:

huaweiPublish {
  instances {
    huaweiRelease {
      credentialsPath = "$rootDir/huawei-appgallery.json"
      buildFormat = 'aab'
      deployType = 'draft' // confirm manually
      releaseNotes = []
      def localeOverride = [
          'am' : 'am-ET',
          'gu': 'gu_IN',
          'iw-IL': 'he_IL',
          'kn-IN': 'kn_IN',
          'ml-IN': 'ml_IN',
          'mn-MN': 'mn_MN',
          'mr-IN': 'mr_IN',
          'ta-IN': 'ta_IN',
          'te-IN': 'te_IN',
      ]
      def files = fileTree(dir: "$projectDir/src/google/play/listings",
          include: '**/release-notes.txt')
      files.each { File file ->
        def path = file.getPath()
        def locale = file.parentFile.getName()
        locale = localeOverride.get(locale, locale)
        releaseNotes.add(new ru.cian.huawei.publish.ReleaseNote(locale, path))
      }
    }
  }
}
  • credentialsPath - задает путь к файлу с ключами от AppGallery Connect

  • deployType = 'draft' - говорим плагину создавать черновик релиза, но не отправлять сразу на review. После тестирования и обкатки можно будет поменять на publish.

  • buildFormat = 'aab' - также как и в Google Play используем для Huawei AppGallery Android App Bundles (AAB) вместо Android Packages (APK).

  • localeOverride - здесь и ниже делается делается магия для переиспользования release notes из плагина для Google Play. Есть небольшие неудобства из-за того, что небольшая группа кодов локалей для Huawei AppGallery почему-то пишется с _ (underscore) вместо - (dash) как Google Play. Остальные совпадают.

Подробнее по параметрам настройки можно посмотреть в README плагина на GitHub.

Тестирование загрузки

Запускаем таск bundleHuaweiRelease для сборки релизной версии aab приложения для последующей загрузки:

gradle bundleHuaweiRelease

Проверяем, что у нас реально что-то собралось:

ls build/outputs/bundle/huaweiRelease
OrganicMaps-23043001-huawei-release.aab

Запускаем таску publishHuaweiAppGalleryHuaweiRelease для загрузки cобравшегося aab в Huawei AppGalery Connect:

gradle publishHuaweiAppGalleryHuaweiRelease

Внимание: задача publish почему-то не имеет явной зависимости на bundle, поэтому надо не забывать запускать bundle задачу каждый раз, можно обе команды сразу в одном запуске gradle:

gradle bundleHuaweiRelease publishHuaweiAppGalleryHuaweiRelease

Смотрим внимательно, что пишет gradle в процессе загрузки:

> Task :publishHuaweiAppGalleryHuaweiRelease
Huawei AppGallery Publishing API: Prepare input config
Huawei AppGallery Publishing API: Found build file: `OrganicMaps-23043001-huawei-release.aab`
Huawei AppGallery Publishing API: Get Access Token
Huawei AppGallery Publishing API: Get App ID
Huawei AppGallery Publishing API: Get Upload Url
Huawei AppGallery Publishing API: Upload build file '/home/runner/work/organicmaps/organicmaps/android/build/outputs/bundle/huaweiRelease/OrganicMaps-23040207-huawei-release.aab'
Huawei AppGallery Publishing API: Upload release notes: 1/59, lang=et
Huawei AppGallery Publishing API: Upload release notes: 2/59, lang=kk

[REDACTED]

Huawei AppGallery Publishing API: Upload release notes: 59/59, lang=ar
Huawei AppGallery Publishing API: Update App File Info
Huawei AppGallery Publishing API: Upload build file draft without submit on users - Successfully Done!

BUILD SUCCESSFUL in 1m 36s
1 actionable task: 1 executed

В случае каких-либо проблем с credentials в файле huawei-appgallery.json плагин зафейлится еще на "Get Access Token".

Проверка черновика релиза
Проверка черновика релиза

Также возможны ситуации, когда число символов в тексте release notes превращает лимиты, что приводит к созданию draft без полного обновления release notes. В таком случае лучше исправить проблему, удалить draft релиза вручную в AppGallery Connect и попробовать еще раз. В данном примере мы загружаем release notes почти для всех доступных локализаций, хотя в большинстве случаев текст просто совпадает с английским.

Проверка обновленных release notes
Проверка обновленных release notes
Отправка на review
Отправка на review

Готово. После этого шага заливка в Huawei AppGallery также возможна через Gradle.

Полезные ссылки:

Добавление CI/CD

Последний, но важный шаг - добавить всё вышеописанное в вашу систему CI/CD. Здесь нет особых сложностей, так как вся автоматизация уже сделана в gradle и вам надо лишь запускать соответствующие задачи.

Мы используем GitHub Actions, поэтому расскажем кратко про них.

В первую очередь надо будет добавить файлы с ключами google-play.json иhuawei-appgallery.json в GitHub Actions Secrets. Также вам понадобятся ключи для подписи, которые можно сохранить в base64. Переходите в Settings -> Secrets and variables -> Actions и добавляйте две новых переменных для каждого файла:

Дальше создаем новый workflow файл .github/workflows/android-release.yaml:

name: Android Release
on:
  workflow_dispatch:  # Manual trigger

Добавляем одну задачу с матрицу из нескольких вариантов (google, huawei, web):

jobs:
  android-release:
    name: Android Release
    runs-on: ubuntu-latest
    environment: production
    needs: tag
    strategy:
      fail-fast: false
      matrix:
        include:
          - flavor: google
          - flavor: huawei

Качаем исходники:

    steps:
      - name: Checkout sources
        uses: actions/checkout@v3
        with:
          fetch-depth: 200 # enough to get all commits for the current day

В случае shared GitHub Runners все зависимости вроде Android SDK уже установлены, но вы можете установить что-нибудь еще, нужное для вашего проекта:

      - name: Install build tools and dependencies
        shell: bash
        run: |
          sudo apt-get update -y
          sudo apt-get install -y ninja-build

Создаем файлы google-play.json иhuawei-appgallery.json из GitHub Secrets:

      - name: Get credenials
        shell: bash
        run: |
          echo "${{ secrets.GOOGLE_PLAY_JSON }}" > google-play.json
          echo "${{ secrets.HUAWEI_APPGALLERY_JSON }}" > huawei-appgallery.json

Также аналогично надо будет добавить ключи для подписи приложений и другие ключи. Можно вместо работы с индивидуальными секретами сделать отдельный репозиторий и применять его поверх основного в момент сборки приложения.

Добавляем задачи для сборки и заливки в Google Play и Huawei AppGallery используя инструкции, описанные ранее в данной статье:

      - name: Compile and upload to Google Play
        shell: bash
        working-directory: android
        run: |
          gradle bundleGoogleRelease publishGoogleReleaseBundle
        if: ${{ matrix.flavor == 'google' }}

      - name: Compile and upload to Huawei AppGallery
        shell: bash
        working-directory: android
        run: |
          gradle bundleHuaweiRelease
          gradle publishHuaweiAppGalleryHuaweiRelease
        if: ${{ matrix.flavor == 'huawei' }}

В целом готово. При запуске будет выглядеть примерно так:

Матрица в GitHub Actions
Матрица в GitHub Actions

Также можно добавить задачу для обновления метаданных Google Play Store. Можно добавить задачу в тот же workflow файл или сделать отдельный файл, так как в Google Play данная операция может выполняться вне зависимости от текущего состояния релиза.

      - name: Update Google Play Metadata
        shell: bash
        run: ./gradlew publishGoogleReleaseListing
        working-directory: android
        timeout-minutes: 5

Полезные ссылки:

Заключение

Выпуск новых релизов в сторах - это рутинная операция, которую приходиться делать на периодической основе - каждую неделю, каждый месяц, год от года. И на каждом шаге постоянно будет что-то забываться или делаться с ошибками. Релиз от релиза. Вы будете тратить время каждый раз.

Прежде чем добавлять описанную в статье автоматизацию, мы провели тщательную работу по документированию существующего процесса. Получилось больше 30 страниц текста со скриншотами... Настроенное CI/CD работает уже второй год и позволяет экономить сотни часов в год, которые можно потратить на более полезные вещи.

Комментарии (10)