Автоматизація публікації програми в Google Play — прямо з Android Studio

Всім привіт! Пів року тому на хабре стаття про те, як автоматизувати завантаження оновлень програми в 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.

Приступимо до створення самого скрипта, який опублікує наш апк. Створимо файл 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;
// ім'я апк.файлу
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")

// завантаження secret service account для аутентифікації
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}"

// виконуємо завантаження апк
def apkFilePath = new File(dir, apkName)
println " - апк 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, "альфа", 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}"
}

// перемістимо файли в папку 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. Мою попередню статтю — Чоловічий In-app Billing: від мобільного додатку до серверної валідації і тестування
2. Google Play Developer API reference
3. Приклад від Google на GitHub
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.