В данной руководстве мы расскажем как автоматизировать процесс релизов Android-приложений в Google Play и Huawei AppStore (пока что без RuStore). Вы навсегда забудете как это делать вручную и сможете потратить время на что-нибудь более полезное, экономя сотни часов в год. Структура Gradle‑проекта Android App Bundles (AAB) vs Android Packages (APK) Генерация номеров версий Получение ключей доступа от Google Play Загрузка сборок в Google Play Управление метаданными Google Play Получение ключей доступа от Huawei AppGallery Загрузка сборок и release notes в Huawei AppGallery Добавление CI/CD Заключение Для начала наведем порядок в самом Android проекте. Вbuild.gradle проекта мы используем весьма стандартную матрицу из flavors x buildTypes: Такая матрица дает комбинацию из GoogleDebug, GoogleBeta, GoogleRelease, HuaweiDebug, и т.д. В целом кастомизация по stores не обязательна, но полезна для управления специальными возможностями, таким как in-app. Настраивайте матрицу исходя из ваших потребностей.  Полезные ссылки: Полный пример build.gradle Настройка build variants С самого момента зарождения 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. Полезные ссылки: Android App Bundles Первая и одна из самых важных задач, которую необходимо решить для реализации continuous deployment хоть для онлайн сервиса, хоть для package software - это обеспечения детерминированного алгоритма генерации версий. Требование простое: для каждого git commit в релизной ветке должен существовать уникальный номер версии. Перестаньте уже делать "bump versions"  (1.0.2 -> 1.0.3 -> 1.0.4 и т.д.) каждый раз вручную. Нужна система, где номер версии (хотя бы последняя его .patch-level часть) будет генерироваться автоматически.  Есть два неплохих подхода, которые позволяют закрыть данный вопрос раз и навсегда полностью.  Особенность Android (и Google Play и AppGallery соответственно) в том, что downgrade версий приложений невозможен. Код номера версии всегда должен возрастать. Но ведь точно также и всегда растёт git история релизной ветки, если же, конечно, вы не хулиганите с rewrite истории и force-pushes.  Крайне логичным является использовать число git commit в ветке для нумерации. Можно, например, сделать так, что вы периодически ставите annotated tag  вида v$major.$minor (например, v1.0, v2.0), тогда как третий компонент (patch) генерируется как число коммитов от предыдущего тега.  Для реализации удобнее всего использовать команду git describe --long: Данный подход дает вполне себе semver v1.0.0, v1.0.1, v1.0.2 и т.д, где последняя цифра версии (patch) назначается автоматически, а major.minor контролируется вами. Для каждого git commit у вас будет уникальный номер версии. Найти git коммит по тегу и числу коммитов от него также не представляется сложным. Просто и практично. Более радикальным подходом является использование даты 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: 2023-04-23 10:37:01 +00002023-04-23 09:02:18 +00002023-04-19 14:43:21 +00002023-04-19 14:43:21 +00002023-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 (смотрите внимательно комментарии): Дальше устанавливаете versionCode и versionName из результата вызова getVersion():  Можете убедиться, что каждый запуск gradle будет генерировать уникальный номер версии для каждого git коммита. Данный подход закрывает вопрос версионирования навсегда и вы не думаете про это. Чтобы загружать что-либо в Google Play автоматизированно, надо для начала получить ключи доступа в API. Кажется, что Google Play весьма плотно интегрировался с IAM Google Cloud и вам нужно создать новый Service Account с необходимыми правами и получить .json файл с credentials. Но давайте по порядку. Вроде как новые  Google Play Developer Accounts должны связываться с Google Cloud как-то полумагически, особенно если вдруг вы использовали Firebase.  Идете в Google Play Console -> Setup -> API Access. Если Linked Project уже есть, то у вас всё хорошо и можно переходить к следующему шагу. Если нет, то проверьте в Google Cloud Console какие у вас уже есть проекты и, при необходимости, создайте новый. Firebase неявно создает Google Cloud проект и вы можете использовать его же.  Service Account это такая сущность для "роботов", которая имеет credentials и на которую можно назначать роли и права доступа. Рекомендуется использовать robot Service Account, а не свой human Google Account. Кликайте на "View in Google Cloud Platform" или же просто переходите в Google Cloud Console и выбирайте нужный проект в списке. Переходите в раздел "Service Accounts". Кликайте "Create Service Account". Введите имя для Service Account, например, Google Play Automatic Upload. Введите ID (email) для Service Account, например google-play-uploaded. Жмите "CREATE AND CONTINUE". Добавьте роль Viewer, без этого Service Account не добавляется в Google Play Console. Жмите "DONE", пропуская третий шаг ( Grant users access to this service account (optional)).  Кликайте по только что созданному Service Account. Переходите на вкладку "Keys". Нажимайте кнопку "ADD KEY" -> "Create new" -> JSON. После клика на "CREATE" будет скачан JSON файл с приватным ключом Service Account: Храните этот ключ в надежном секретном месте! Последний шаг - дать права на загрузку приложений для нового Service Account. Кажется, эта часть пока еще не интегрирована до конца с Google Cloud и права необходимо выдавать в Google Play Console. Переходите в Google Play Console, раздел "Users and permissions". Жмете на меню с трёмя точками, дальше "Invite new users". Вводите google-play-uploaded@<YOUR GCLOUD PROJECT>.iam.gserviceaccount.com Необходимо дать следущие права 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 appsThis permission does not allow users to publish apps on Google Play Release to production, exclude devices and use Play app signingCreate,  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 tracksUpload 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 Developers API Загрузку в 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: Версия в статье быстро устареет. Пожалуйста, всегда используйте последнюю доступную версию. Добавьте вызов плагина com.github.triplet.play в build.gradle сразу после секции `android {}`: Плагин настраивается через глобальную секцию конфигурации play {}, а также дополнительно в каждом flavor: track - говорим в какой track заливать. Конечно же, сразу в продакшен! userFraction - включаем stage rollout на 10% чтобы не быть совсем сумашедшими. defaultToAppBundles - включаем Android App Bundles (AAB)  вместо Android Packages (APK). releaseStatus.set(ReleaseStatus.DRAFT) - создаем черновик, но пока сразу не отправляем на ревью. После тестирования можно будет поменять на ReleaseStatus.IN_PROGRESS. А вы сейчас делаете stage rollout? Расскажите, пожалуйста, об этом в комментариях. Запускаем таск bundleGoogleRelease для сборки релизной версии aab приложения для последующей загрузки: Проверяем, что у нас реально что-то собралось: Запускаем таску publishGoogleReleaseBundle для загрузки cобравшегося aab в Google Play: В целом часть с заливкой готова и  вы сможете увидеть сможете увидеть черновик релиза Google Play Console и отправить его на review.  Если всё соответствует ожиданиями, то дальше можно поменять releaseStatus.set(ReleaseStatus.DRAFT)на releaseStatus.set(ReleaseStatus.IN_PROGRESS) чтобы лишний раз не бегать в Google Play Console для нажатия кнопки. Кнопка должна быть где-то в вашем CI/CD, чтобы вы там не использовали. Полезные ссылки: Полный пример build.gradle Документация по Triple-T Gradle Play Publisher Следующий шаг - это научиться также обновлять метаданные (описание приложения, скриншоты и т.п.) в Google Play.  После добавления данной автоматизации станет возможным обновлять метаданные в git репозитории вместо Google Play. Работу по обновлению в Google Play сделает плагин.  Для начала необходимо скачать уже имеющиеся в Google Play метаданные в репозиторий. Запускаем задачу `bootstrapGoogleReleaseListing`: Пошуршав диском, gradle выкачает все текущие метаданные в репозиторий. Новые файлы появятся в src/<FLAVOR>/play: После скачивания данных из Google Play добавьте их в git и далее используйте git как первоисточник, избегая ручных действий в Google Play Console. Это даёт огромное число плюсов, начиная от понятного версионирования, до возможности добавления инструментов для переводов. Например, мы используем Weblate для перевода описаний Google Play на другие языки. Данные Google Play также будут переиспользованы далее для Huawei AppGallery. Для загрузки обновленных метаданных обратно в Google Play существует задача publishGoogleReleaseListing (где GoogleRelease - ваш flavor): Важно. Обновление метаданных в Google Play не привязано к выпуску релизов. Вы можете обновлять описания и скриншоты приложений в любой момент, вне зависимости от текущего состояния review сборки приложения. Review изменения метаданных происходит отдельно! Мы используем данный подход чтобы обновлять данные на всех 59 языках, поддерживаемых Google Play Console на момент написания статьи. Полезные ссылки: Полный пример build.gradle Документация по Triple-T Gradle Play Publisher Чтобы загружать что-то в Huawei AppGallery нам необходимо сначала получить ключи доступа к Huawei AppGallery Connect. Интерфейс Huawei AppGallery Connect, скажем так, местами весьма запутан. Но надо разобраться один раз чтобы в дальнейшем туда вообще не заходить. Заходите в AppGallery Connect и нажимайте Users and permissions. Переходите к API key > Connect API и нажимайте Create: Можете ввести любое название. Укажите следующие роли: Development Operations Customer service (будет выбрано автоматически) После этого вы сможете скачать .json ключ доступа к Huawei AppGallery Connect: Готово. Данный файл понадобится на следущем этапе для автоматизации загрузки. Полезные ссылки: AppGallery Connect -> Getting Started -> Creating an API client Документация по Huawei App Gallery Publishing Для автоматизации загрузки в Huawei AppGallery будем использовать Gradle plugin ru.cian:huawei-publish-gradle-plugin, разрабатываемый Aleksandr Mirko из Омска. Скажем спасибо Александру за столь прекрасный безальтернативный плагин для загрузки в Huawei AppGallery без которого пришлось бы страдать. Добавляем Gradle-plugin ru.cian:huawei-publish-gradle-plugin в build.gradle: Включаем плагин ru.cian.huawei-publish-gradle-plugin в build.gradle после секции android {}: Плагин имеет отдельную секцию для конфигурции huaweiPublish {}. Добавляем новую секцию с настройками в конец build.gradle: 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 приложения для последующей загрузки: Проверяем, что у нас реально что-то собралось: Запускаем таску publishHuaweiAppGalleryHuaweiRelease для загрузки cобравшегося aab в Huawei AppGalery Connect: Внимание: задача publish почему-то не имеет явной зависимости на bundle, поэтому надо не забывать запускать bundle задачу каждый раз, можно обе команды сразу в одном запуске gradle: Смотрим внимательно, что пишет gradle в процессе загрузки: В случае каких-либо проблем с credentials в файле huawei-appgallery.json плагин зафейлится еще на "Get Access Token".  Также возможны ситуации, когда число символов в тексте release notes превращает лимиты, что приводит к созданию draft без полного обновления release notes. В таком случае лучше исправить проблему, удалить draft релиза вручную в AppGallery Connect и попробовать еще раз. В данном примере мы загружаем release notes почти для всех доступных локализаций, хотя в большинстве случаев текст просто совпадает с английским. Готово. После этого шага заливка в Huawei AppGallery также возможна через Gradle. Полезные ссылки: Документация по Gradle плагину для Huawei AppGallery Последний, но важный шаг - добавить всё вышеописанное в вашу систему 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:  Добавляем одну задачу с матрицу из нескольких вариантов (google, huawei, web): Качаем исходники: В случае shared GitHub Runners все зависимости вроде Android SDK уже установлены, но вы можете установить что-нибудь еще, нужное для вашего проекта: Создаем файлы google-play.json иhuawei-appgallery.json из GitHub Secrets: Также аналогично надо будет добавить ключи для подписи приложений и другие ключи. Можно вместо работы с индивидуальными секретами сделать отдельный репозиторий и применять его поверх основного в момент сборки приложения.  Добавляем задачи для сборки и заливки в Google Play и Huawei AppGallery используя инструкции, описанные ранее в данной статье: В целом готово. При запуске будет выглядеть примерно так: Также можно добавить задачу для обновления метаданных Google Play Store. Можно добавить задачу в тот же workflow файл или сделать отдельный файл, так как в Google Play данная операция может выполняться вне зависимости от текущего состояния релиза. Полезные ссылки: Добавление encrypted secrets для GitHub Actions Пример android-release.yaml для GitHub Actions Выпуск новых релизов в сторах - это рутинная операция, которую приходиться делать на периодической основе - каждую неделю, каждый месяц, год от года. И на каждом шаге постоянно будет что-то забываться или делаться с ошибками. Релиз от релиза. Вы будете тратить время каждый раз. Прежде чем добавлять описанную в статье автоматизацию, мы провели тщательную работу по документированию существующего процесса. Получилось больше 30 страниц текста со скриншотами... Настроенное CI/CD работает уже второй год и позволяет экономить сотни часов в год, которые можно потратить на более полезные вещи.