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



Но я с радостью готов сообщить, что это — не правда. Публиковать приложение прямо из Android Studio можно! Более того, можно делать это вообще без Android Studio на вашем CI — так как делаться это будет с помощью обычного Gradle task.

Мое решение похоже на то, что описано в предыдущей статье, но вместо java я использовал groovy-скрипт.

Для того, чтобы публиковать приложения из скрипта, нужно создать пользователя с доступом для публикации и получить .json-файл, который будем использовать в нашем коде для аутентификации. Как его получить и что нужно сделать для активации доступа к Google Play Developer API можно посмотреть в указанной статье, или же можете прочитать мою публикацию о работе с Google Play Billing на стороне сервера, где в части 3 описано создание Service-account-ов для доступа к Google Play.

С данного момента будем считать, что у вас уже есть заветный .json файлик с service account secret.

Для начала подготовим нам проект. Будем работать с build.gradle нашего корневого проекта, а не app. Приведем root/build.gradle к такому виду:

// активируем груви в нашем проекте
apply plugin: 'groovy'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

dependencies {
    // импортируем груви в наш проект
    compile 'org.codehaus.groovy:groovy-all:2.4.7'
    // и библиотеку, с помощью которой будем делать publish
    compile 'com.google.apis:google-api-services-androidpublisher:v2-rev38-1.22.0'
}

Что сделано:

1. apply plugin: 'groovy'
Активируем groovy-компилятор в нашем проекте.

2. dependencies — compile 'org.codehaus.groovy:groovy-all:2.4.7'
Импортируем последнюю версию groovy в наше проект

3. dependencies — compile 'com.google.apis:google-api-services-androidpublisher:v2-rev38-1.22.0'

Импортируем библиотеку от Google, которая, собственно, и предоставляет нам возможность работать с публикациями в Google play (и не только).

Теперь мы можем писать groovy-скрипты и groovy-классы и использовать их в нашем проекте. Но для начала создадим source dir для наших groovy-классов и организуем другие файлы, которые нам будут нужны:

root/
    app/
        ... наше приложение
        .gitignore - добавим файл keystore.jks сюда, чтобы не хранить его в репозитории
        keystore.jks
        build.gradle
    gradle/
    iam/
        .gitignore - добавим файл publisher.json сюда, чтобы не хранить его в репозитории
        publisher.json - файл с service account secret
    src/
        main/
            groovy/
                ... тут будем писать классы и скрипты
    .gitignore - добавим файл signing.properties сюда, чтобы не хранить его в репозитории
    build.gradle
    gradle.properties
    gradlew
    gradlew.bat
    local.properties
    signing.properties - тут будем хранить пароли нашего keystore
    settings.gradle

Для того, чтобы опубликовать приложение в Google Play, нужно подписать его release сертификатом. Но ведь мы не хотим хранить наш keystore, явки и пароли в репозитории? Ипользуйте .gitignore. Сами же пароли поместим в файл root/signing.properties:

keystore.file=keystore.jks
keystore.password=<пароль>
key.alias=<имя_ключа>
key.password=<пароль_ключа>

Прочитаем эти пароли из файла с создадим подходящий signing config в root/app/build.gradle

...
android {
    ...
    Properties signingProperties = new Properties()
    def file = project.rootProject.file('signing.properties')
    if (fixe.exists()) {
        signingProperties.load(file.newDataInputStream())
    }
    def prodSigning_keystoreFile = properties.getProperty('keystore.file')
    def prodSigning_keystorePassword = properties.getProperty('keystore.password')
    def prodSigning_keyAlias = properties.getProperty('key.alias')
    def prodSigning_keyPassword = properties.getProperty('key.password')
    ...
    signingConfigs {
        ...
        production {
            storeFile file(prodSigning_keystoreFile )
            storePassword prodSigning_keystorePassword 
            keyAlias prodSigning_keyAlias 
            keyPassword prodSigning_keyPassword 
        }
    }
    productFlavors {
        ...
        prod {
            ...
        }
    }
    buildTypes {
        ...
        release {
            signingConfig production
        }
    }
}

Теперь мы можем использовать gradle assembleProdRelease чтобы получить apk-файл, который к загрузке в Google Play.

Приступим к созданию самого скрипта, который опубликует наш apk. Создадим файл root/src/main/groovy/ApkPublisher.groovy:

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.http.FileContent
import com.google.api.client.json.jackson2.JacksonFactory
import com.google.api.services.androidpublisher.AndroidPublisher
import com.google.api.services.androidpublisher.AndroidPublisherScopes
import com.google.api.services.androidpublisher.model.Track

class ApkPublisher {

    // имя пакета
    String packageName;
    // имя приложения (в теории оно необязательно, но без него будут warnings)
    String name;
    // имя apk.файла
    String apkName;
    // имя файла proguard-mapping
    String mappingName;

    void publish() {
        assert packageName != null
        assert name != null
        assert apkName != null
        assert mappingName != null

        println "PUBLISHING [ ${packageName} / ${name} ]"

        def dir = new File("assemble")

        // загрузка service account secret для аутентификации
        def inputStream = new FileInputStream("iam/publisher.json");
        def transport = GoogleNetHttpTransport.newTrustedTransport();
        def credential = GoogleCredential.fromStream(inputStream)
            .createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));
        def builder = new AndroidPublisher.Builder(transport,
            JacksonFactory.getDefaultInstance(), credential);
        builder.setApplicationName(name)
        def androidPublisher = builder.build();
        def edits = androidPublisher.edits();

        // создаем запрос на редактирование
        def editRequest = edits.insert(packageName, null);
        def edit = editRequest.execute();
        // получаем уникальный id, который нужен нам чтобы выполнять другие действия
        final String editId = edit.getId();
        println " - edit_id = ${editId}"

        // выполняем загрузку apk
        def apkFilePath = new File(dir, apkName)
        println " - apk file = ${apkFilePath}"
        def apkFile = new FileContent("application/vnd.android.package-archive", apkFilePath);
        def apkUploadRequest = edits.apks().upload(packageName, editId, apkFile);
        def apkUploadResult = apkUploadRequest.execute();
        // в ответе получаем текущий verfsionCode
        int versionCode = apkUploadResult.getVersionCode()
        println " - version code ${versionCode} has been uploaded"

        // загружаем proguard mapping
        def mappingFilePath = new File(dir, mappingName)
        println " - mapping file = ${mappingFilePath}"
        def mappingFile = new FileContent("application/octet-stream", mappingFilePath);
        def mappingUploadRequest = edits.deobfuscationfiles()
            .upload(packageName, editId, versionCode, "proguard", mappingFile);
        mappingUploadRequest.execute();
        println " - mapping for version ${versionCode} has been uploaded"

        // теперь нужно опубликовать загруженный apk
        // в данном примере мы публикуем его в альфа-тестирование
        List apkVersionCodes = [versionCode]
        def track = new Track().setVersionCodes(apkVersionCodes)
        def updateTrackRequest = edits.tracks().update(packageName, editId, "alpha", track);
        def updatedTrack = updateTrackRequest.execute();
        println " - track code ${updatedTrack.getTrack()} has been updated"

        // после того, как все действия выполнены
        // нужно подтвердить, что запрос на редактирование завершен и "закоммитить"
        // его, как транзакцию
        def commitRequest = edits.commit(packageName, editId);
        def appEdit = commitRequest.execute();
        println " - app edit with id ${appEdit.getId()} has been comitted"
        println "APP [ ${packageName} / ${name} / v${versionCode} ] SUCCESSFULLY PUBLISHED"
    }

}

Второй файл root/src/main/groovy/PublishApk.groovy:

def void moveToAssemble(String folder, String name, String newName) {
    def from = new File(folder, name)
    def to = new File("assemble", newName)
    from.renameTo(to)
    println "moved ${from} to ${to}"
}

// переместим файлы в папку root/assemble
// предварительно создадим ее если ее не было
// и удалим старые файлы, если они там были
def destDir = new File("assemble")
destDir.mkdir()
for (def item : destDir.listFiles()) {
    item.delete()
}
moveToAssemble("app/build/outputs/apk", "app-prod-release.apk", "myapp.apk")
moveToAssemble("app/build/outputs/mapping/prod/release", "mapping.txt", "myapp-mapping.txt")

// а теперь опубликуем приложение
new ApkPublisher(
        packageName: "com.example.myapp",
        name: "My app",
        apkName: "myapp.apk",
        mappingName: "myapp-mapping.txt"
).publish()

Скрипт для загрузки файла готов. Теперь перейдем к созданию Gradle task:

root/build.gradle

// запускаем сборку apk
task assembleApk(dependsOn: [
        ':app:assembleProdRelease'
]) << {
    println("APK assembled")
}

// компилируем и выполняем скрипт
task publishApk(dependsOn: 'classes', type: JavaExec) {
    main = 'PublishApk'
    classpath = sourceSets.main.runtimeClasspath
}

task assembleAndPublishApk() {
    dependsOn 'assembleApk'
    dependsOn 'publishApk'
    tasks.findByName('publishApk').mustRunAfter 'assembleApk'
    doLast {
        println("APK successfilly published, find it in /assemble dir")
    }
}

Теперь достаточно выполнить команду gradle assembleAndPublishApk для публикации apk в альфа-канал. И это можно легко сделать хоть после каждого коммита в development. В дополнение мы сразу загружаем proguard-mapping файл.

P. S. Что еще почитать?
1. Мою предыдущую статью — Android In-app Billing: от мобильного приложения до серверной валидации и тестирования
2. Google Play Developer API reference
3. Пример от Google на GitHub
Поделиться с друзьями
-->

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


  1. Zanexes
    28.11.2016 16:59

    Мне кажется fastlane для этих целей тоже весьма хорош. Не пробовали? Хотелось бы увидеть сравнение. Надеюсь скоро Ваш способ попробую.


    1. denonlink
      28.11.2016 17:01

      В данном случае был интерес именно разобраться самому с тем как это работает.


  1. farwayer
    28.11.2016 16:59

    А можно просто использовать fastlane. Он еще и локализированные скриншоты может наделать, release notes обновить и еще много чего.


    1. denonlink
      28.11.2016 17:02

      Для моего проекта это нужно было чтобы быстро загружать новые версии в альфа тестирование. Только apk и mapping.


  1. Z10yTap0k
    28.11.2016 17:32

    Давно уже существует https://github.com/Triple-T/gradle-play-publisher


    1. MakarkinPRO
      28.11.2016 21:48

      А есть что-то похожее для ReactNative? типа сделал код на симуляторах протести и отправил в АльфаКанал на Android и iOS?


      1. megahertz
        28.11.2016 22:17

        Вышеупомянутый fastlane


        1. MakarkinPRO
          28.11.2016 22:19

          То есть прям с ПК получается? или там что то надо куда то загружать все равно?


          1. megahertz
            29.11.2016 07:24

            Все одной командой


            1. MakarkinPRO
              29.11.2016 09:43

              Хех прикольно Спасибо. Изучу.


  1. KuSu
    29.11.2016 12:05

    И это можно легко сделать хоть после каждого коммита в development

    Стоит добавить, что есть ограничение на количество запросов в день. Столкнулся с этим, когда тестировал автопубликацию.


    1. denonlink
      29.11.2016 12:06

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


      1. KuSu
        29.11.2016 12:16

        https://developers.google.com/android-publisher/quotas
        Где-то после 40-50 попыток публикации у меня выскочило «Daily save quota exceeded.»

        И они рекомендуют не публиковать чаще чем один раз в день
        https://developers.google.com/android-publisher/api_usage