From de05342d030f01bd76f437fc1c81562d14f0c024 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Mon, 15 Apr 2024 16:58:26 +0200 Subject: [PATCH 1/2] Implement Google Play upload task --- gradle/libs.versions.toml | 4 + plugin-build/plugin/build.gradle.kts | 6 +- .../publish/plugin/BuildPublishPlugin.kt | 77 +++++- .../plugin/extension/BuildPublishExtension.kt | 4 + .../plugin/extension/config/PlayConfig.kt | 37 +++ .../plugin/task/play/PlayDistributionTask.kt | 96 +++++++ .../publish/plugin/task/play/PlayPublisher.kt | 88 +++++++ .../task/play/publisher/AndroidPublisher.kt | 102 ++++++++ .../play/publisher/DefaultPlayPublisher.kt | 240 +++++++++++++++++ .../play/publisher/InternalPlayPublisher.kt | 51 ++++ .../task/play/publisher/PublisherModels.kt | 51 ++++ .../plugin/task/play/publisher/Responses.kt | 124 +++++++++ .../task/play/track/DefaultEditManager.kt | 221 ++++++++++++++++ .../plugin/task/play/track/EditManager.kt | 76 ++++++ .../plugin/task/play/track/TrackManager.kt | 242 ++++++++++++++++++ .../plugin/task/play/work/PlayUploadWork.kt | 81 ++++++ 16 files changed, 1493 insertions(+), 7 deletions(-) create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9220db..717e47e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,8 @@ moshi = "1.15.0" retrofit = "2.9.0" junit = "4.13.2" ksp = "1.9.23-1.0.19" +play-publish = "v3-rev20231115-2.0.0" +google-auth = "1.20.0" [libraries] agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } @@ -29,6 +31,8 @@ kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version. ktlint-plugin = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint" } detekt-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } ksp-plugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } +play-publish = { module = "com.google.apis:google-api-services-androidpublisher", version.ref = "play-publish" } +google-auth = { module = "com.google.auth:google-auth-library-oauth2-http", version.ref = "google-auth" } [plugins] agp = { id = "com.android.application", version.ref = "agp" } diff --git a/plugin-build/plugin/build.gradle.kts b/plugin-build/plugin/build.gradle.kts index 1509f7b..91b9db2 100644 --- a/plugin-build/plugin/build.gradle.kts +++ b/plugin-build/plugin/build.gradle.kts @@ -16,6 +16,8 @@ dependencies { implementation(libs.retrofitMoshi) implementation(libs.grgitCore) implementation(libs.grgitGradle) + implementation(libs.play.publish) + implementation(libs.google.auth) testImplementation(libs.junit) @@ -31,7 +33,7 @@ gradlePlugin { id = "ru.kode.android.build-publish" displayName = "Configure project with Firebase App Distribution and changelogs" implementationClass = "ru.kode.android.build.publish.plugin.BuildPublishPlugin" - version = project.version + version = "1.3.2" description = "Android plugin to publish bundles and apks to Firebase App Distribution with changelogs" tags.set(listOf("firebase", "publish", "changelog", "build")) } @@ -43,7 +45,7 @@ publishing { create("maven") { groupId = project.group.toString() artifactId = "ru.kode.android.build-publish".removePrefix("$groupId.") - version = project.version.toString() + version = "1.3.2" from(components["java"]) } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt index c84ef3f..0f5b730 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt @@ -6,6 +6,7 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.impl.VariantOutputImpl import com.android.build.gradle.AppExtension import com.android.build.gradle.AppPlugin +import com.android.build.gradle.internal.tasks.PackageBundleTask import com.android.build.gradle.tasks.PackageAndroidArtifact import com.android.builder.model.Version.ANDROID_GRADLE_PLUGIN_VERSION import com.google.firebase.appdistribution.gradle.AppDistributionExtension @@ -18,6 +19,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.StopExecutionException @@ -33,11 +35,13 @@ import ru.kode.android.build.publish.plugin.extension.config.ChangelogConfig import ru.kode.android.build.publish.plugin.extension.config.FirebaseAppDistributionConfig import ru.kode.android.build.publish.plugin.extension.config.JiraConfig import ru.kode.android.build.publish.plugin.extension.config.OutputConfig +import ru.kode.android.build.publish.plugin.extension.config.PlayConfig import ru.kode.android.build.publish.plugin.extension.config.SlackConfig import ru.kode.android.build.publish.plugin.extension.config.TelegramConfig import ru.kode.android.build.publish.plugin.task.appcenter.AppCenterDistributionTask import ru.kode.android.build.publish.plugin.task.changelog.GenerateChangelogTask import ru.kode.android.build.publish.plugin.task.jira.JiraAutomationTask +import ru.kode.android.build.publish.plugin.task.play.PlayDistributionTask import ru.kode.android.build.publish.plugin.task.slack.changelog.SendSlackChangelogTask import ru.kode.android.build.publish.plugin.task.slack.distribution.SlackDistributionTask import ru.kode.android.build.publish.plugin.task.tag.GetLastTagTask @@ -47,6 +51,7 @@ import ru.kode.android.build.publish.plugin.util.capitalizedName import java.io.File import java.time.LocalDate import java.time.format.DateTimeFormatter +import java.util.logging.Logger internal const val SEND_SLACK_CHANGELOG_TASK_PREFIX = "sendSlackChangelog" internal const val SEND_TELEGRAM_CHANGELOG_TASK_PREFIX = "sendTelegramChangelog" @@ -59,6 +64,7 @@ internal const val DEFAULT_VERSION_CODE = 1 internal const val DEFAULT_BASE_FILE_NAME = "dev-build" internal const val CHANGELOG_FILENAME = "changelog.txt" internal const val APP_CENTER_DISTRIBUTION_UPLOAD_TASK_PREFIX = "appCenterDistributionUpload" +internal const val PLAY_DISTRIBUTION_UPLOAD_TASK_PREFIX = "playUpload" internal const val SLACK_DISTRIBUTION_UPLOAD_TASK_PREFIX = "slackDistributionUpload" internal const val JIRA_AUTOMATION_TASK = "jiraAutomation" internal const val DEFAULT_CONTAINER_NAME = "default" @@ -87,6 +93,10 @@ abstract class BuildPublishPlugin : Plugin { androidExtension.onVariants( callback = { variant -> + Logging.getLogger(this::class.java).info("FOUND VARIANT: $variant WITH OUTPUT NAME ${variant.outputs + .find { it is VariantOutputImpl && it.fullName == variant.name } + as? VariantOutputImpl}") + val output = variant.outputs .find { it is VariantOutputImpl && it.fullName == variant.name } @@ -239,6 +249,21 @@ abstract class BuildPublishPlugin : Plugin { ) tasks.registerAppCenterDistributionTask(params) } + val playConfig = + with(buildPublishExtension.play) { + findByName(buildVariant.name) ?: findByName(DEFAULT_CONTAINER_NAME) + } + if (playConfig != null) { + val params = + PlayTaskParams( + config = playConfig, + buildVariant = buildVariant, + buildVariantOutputFileProvider = outputFileProvider, + tagBuildProvider = tagBuildProvider, + outputConfig = outputConfig, + ) + tasks.registerPlayTask(params) + } val jiraConfig = with(buildPublishExtension.jira) { findByName(buildVariant.name) ?: findByName(DEFAULT_CONTAINER_NAME) @@ -461,6 +486,25 @@ abstract class BuildPublishPlugin : Plugin { it.uploadStatusRequestDelayCoefficient.set(config.uploadStatusRequestDelayCoefficient) } } + + private fun TaskContainer.registerPlayTask( + params: PlayTaskParams, + ): TaskProvider { + val buildVariant = params.buildVariant + val config = params.config + + return register( + "$PLAY_DISTRIBUTION_UPLOAD_TASK_PREFIX${buildVariant.capitalizedName()}", + PlayDistributionTask::class.java, + ) { + it.tagBuildFile.set(params.tagBuildProvider) + it.buildVariantOutputFile.set(params.buildVariantOutputFileProvider) + it.apiTokenFile.set(config.apiTokenFile) + it.appId.set(config.appId) + it.trackId.set(config.trackId) + it.updatePriority.set(config.updatePriority) + } + } } private fun Project.configurePlugins( @@ -552,6 +596,15 @@ private data class AppCenterDistributionTaskParams( val outputConfig: OutputConfig, ) + +private data class PlayTaskParams( + val config: PlayConfig, + val buildVariant: BuildVariant, + val buildVariantOutputFileProvider: Provider, + val tagBuildProvider: Provider, + val outputConfig: OutputConfig, +) + private fun mapToVersionCode(tagBuildFile: RegularFile): Int { val file = tagBuildFile.asFile return if (file.exists()) { @@ -573,8 +626,15 @@ private fun mapToOutputFileName( val versionName = tagBuild.buildVariant val versionCode = tagBuild.buildNumber "$baseFileName-$versionName-vc$versionCode-$formattedDate.apk" + } else if (file.exists() && outputFileName.endsWith(".aab")) { + val tagBuild = fromJson(file) + val versionName = tagBuild.buildVariant + val versionCode = tagBuild.buildNumber + "$baseFileName-$versionName-vc$versionCode-$formattedDate.aab" } else if (!file.exists() && outputFileName.endsWith(".apk")) { "$baseFileName-$formattedDate.apk" + } else if (!file.exists() && outputFileName.endsWith(".aab")) { + "$baseFileName-$formattedDate.aab" } else { createDefaultOutputFileName(baseFileName, outputFileName) } @@ -604,9 +664,16 @@ private fun Project.mapToOutputFile( buildVariant: BuildVariant, fileName: String, ): Provider { - return project.tasks.withType(PackageAndroidArtifact::class.java) - .firstOrNull { it.variantName == buildVariant.name } - ?.outputDirectory - ?.map { directory -> directory.file(fileName) } - ?: throw GradleException("no output for variant ${buildVariant.name}") + return if (fileName.endsWith("aab")) { + project.tasks.withType(PackageBundleTask::class.java) + .firstOrNull { it.variantName == buildVariant.name } + ?.bundleFile + ?: throw GradleException("no output for variant ${buildVariant.name}") + } else { + project.tasks.withType(PackageAndroidArtifact::class.java) + .firstOrNull { it.variantName == buildVariant.name } + ?.outputDirectory + ?.map { directory -> directory.file(fileName) } + ?: throw GradleException("no output for variant ${buildVariant.name}") + } } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt index 77b72b4..5c53e64 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt @@ -7,8 +7,10 @@ import ru.kode.android.build.publish.plugin.extension.config.ChangelogConfig import ru.kode.android.build.publish.plugin.extension.config.FirebaseAppDistributionConfig import ru.kode.android.build.publish.plugin.extension.config.JiraConfig import ru.kode.android.build.publish.plugin.extension.config.OutputConfig +import ru.kode.android.build.publish.plugin.extension.config.PlayConfig import ru.kode.android.build.publish.plugin.extension.config.SlackConfig import ru.kode.android.build.publish.plugin.extension.config.TelegramConfig +import ru.kode.android.build.publish.plugin.task.play.PlayDistributionTask import javax.inject.Inject const val EXTENSION_NAME = "buildPublish" @@ -31,4 +33,6 @@ abstract class BuildPublishExtension objectFactory.domainObjectContainer(FirebaseAppDistributionConfig::class.java) val appCenterDistribution: NamedDomainObjectContainer = objectFactory.domainObjectContainer(AppCenterDistributionConfig::class.java) + val play: NamedDomainObjectContainer = + objectFactory.domainObjectContainer(PlayConfig::class.java) } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt new file mode 100644 index 0000000..c18da4c --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt @@ -0,0 +1,37 @@ +package ru.kode.android.build.publish.plugin.extension.config + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile + +interface PlayConfig { + val name: String + + /** + * The path to file with token for Google Play project + */ + @get:InputFile + val apiTokenFile: RegularFileProperty + + /** + * appId in Google Play + */ + @get:Input + val appId: Property + + /** + * Track name of target app. Defaults to "internal" + */ + @get:Input + val trackId: Property + + /** + * Test groups for app distribution + * + * For example: [android-testers] + */ + @get:Input + val updatePriority: Property +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt new file mode 100644 index 0000000..ba83483 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt @@ -0,0 +1,96 @@ +package ru.kode.android.build.publish.plugin.task.play + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.plugins.BasePlugin +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.workers.WorkQueue +import org.gradle.workers.WorkerExecutor +import ru.kode.android.build.publish.plugin.enity.mapper.fromJson +import ru.kode.android.build.publish.plugin.extension.config.MAX_REQUEST_COUNT +import ru.kode.android.build.publish.plugin.extension.config.MAX_REQUEST_DELAY_MS +import ru.kode.android.build.publish.plugin.task.appcenter.work.AppCenterUploadWork +import ru.kode.android.build.publish.plugin.task.play.work.PlayUploadWork +import ru.kode.android.build.publish.plugin.util.capitalized +import javax.inject.Inject + +abstract class PlayDistributionTask + @Inject + constructor( + private val workerExecutor: WorkerExecutor, + ) : DefaultTask() { + init { + description = "Task to send apk to Google Play" + group = BasePlugin.BUILD_GROUP + } + + @get:InputFile + @get:Option( + option = "buildVariantOutputFile", + description = "Artifact output file (absolute path is expected)", + ) + abstract val buildVariantOutputFile: RegularFileProperty + + @get:InputFile + @get:Option(option = "tagBuildFile", description = "Json contains info about tag build") + abstract val tagBuildFile: RegularFileProperty + + @get:InputFile + @get:Option( + option = "apiTokenFile", + description = "API token for google play console", + ) + abstract val apiTokenFile: RegularFileProperty + + @get:Input + @get:Option( + option = "appId", + description = "appId from Play Console", + ) + abstract val appId: Property + + + @get:Input + @get:Option( + option = "trackId", + description = "Track name of target app. Defaults to internal", + ) + abstract val trackId: Property + + @get:Input + @get:Optional + @get:Option( + option = "updatePriority", + description = "Update priority (0..5)", + ) + abstract val updatePriority: Property + + @TaskAction + fun upload() { + val outputFile = buildVariantOutputFile.asFile.get() + if (outputFile.extension != "aab") throw GradleException("file ${outputFile.path} is not bundle") + val tag = fromJson(tagBuildFile.asFile.get()) + val releaseName = "${tag.name}(${tag.buildVersion}.${tag.buildNumber})" + val apiTokenFile = apiTokenFile.asFile.get() + val workQueue: WorkQueue = workerExecutor.noIsolation() + val trackId = trackId.orNull ?: "internal" + val appId = appId.get() + val updatePriority = updatePriority.orNull ?: 0 + + workQueue.submit(PlayUploadWork::class.java) { parameters -> + parameters.appId.set(appId) + parameters.apiToken.set(apiTokenFile) + parameters.trackId.set(trackId) + parameters.updatePriority.set(updatePriority) + parameters.releaseName.set(releaseName) + parameters.outputFile.set(outputFile) + } + } + } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt new file mode 100644 index 0000000..5e654bd --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt @@ -0,0 +1,88 @@ +package ru.kode.android.build.publish.plugin.task.play + +import ru.kode.android.build.publish.plugin.task.play.publisher.CommitResponse +import ru.kode.android.build.publish.plugin.task.play.publisher.EditResponse +import ru.kode.android.build.publish.plugin.task.play.publisher.GppProduct +import ru.kode.android.build.publish.plugin.task.play.publisher.UpdateProductResponse +import ru.kode.android.build.publish.plugin.task.play.publisher.UploadInternalSharingArtifactResponse +import java.io.File + +/** + * Proxy for the AndroidPublisher API. Separate the build side configuration from API dependencies + * to make testing easier. + * + * For the full API docs, see [here](https://developers.google.com/android-publisher/api-ref). + */ +interface PlayPublisher { + /** + * Creates a new edit. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/edits/insert). + */ + fun insertEdit(): EditResponse + + /** + * Retrieves an existing edit with the given [id]. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/edits/get). + */ + fun getEdit(id: String): EditResponse + + /** + * Commits an edit with the given [id]. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/edits/commit). + */ + fun commitEdit(id: String, sendChangesForReview: Boolean = true): CommitResponse + + /** + * Validates an edit with the given [id]. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/edits/validate). + */ + fun validateEdit(id: String) + + /** + * Uploads the given [bundleFile] as an Internal Sharing artifact. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/internalappsharingartifacts/uploadbundle). + */ + fun uploadInternalSharingBundle(bundleFile: File): UploadInternalSharingArtifactResponse + + /** + * Uploads the given [apkFile] as an Internal Sharing artifact. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/internalappsharingartifacts/uploadapk). + */ + fun uploadInternalSharingApk(apkFile: File): UploadInternalSharingArtifactResponse + + /** + * Get all current products. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/inappproducts/list). + */ + fun getInAppProducts(): List + + /** + * Creates a new product from the given [productFile]. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/inappproducts/insert). + */ + fun insertInAppProduct(productFile: File) + + /** + * Updates an existing product from the given [productFile]. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/inappproducts/update). + */ + fun updateInAppProduct(productFile: File): UpdateProductResponse +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt new file mode 100644 index 0000000..cf62305 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt @@ -0,0 +1,102 @@ +package ru.kode.android.build.publish.plugin.task.play.publisher + +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport +import com.google.api.client.googleapis.json.GoogleJsonResponseException +import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler +import com.google.api.client.http.HttpRequest +import com.google.api.client.http.HttpTransport +import com.google.api.client.http.apache.v2.ApacheHttpTransport +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.gson.GsonFactory +import com.google.api.client.util.ExponentialBackOff +import com.google.api.services.androidpublisher.AndroidPublisher +import com.google.api.services.androidpublisher.AndroidPublisherScopes +import com.google.auth.http.HttpCredentialsAdapter +import com.google.auth.oauth2.GoogleCredentials +import org.apache.http.auth.AuthScope +import org.apache.http.auth.UsernamePasswordCredentials +import org.apache.http.impl.client.BasicCredentialsProvider +import org.apache.http.impl.client.ProxyAuthenticationStrategy +import java.io.FileInputStream +import java.io.InputStream +import java.security.KeyStore +import java.util.concurrent.TimeUnit + +internal fun createPublisher(credentials: InputStream): AndroidPublisher { + val transport = buildTransport() + val credential = try { + GoogleCredentials.fromStream(credentials) { transport } + } catch (e: Exception) { + throw Exception( + "Credential parsing may have failed. " + + "Ensure credential files supplied in the DSL contain valid JSON " + + "and/or the ANDROID_PUBLISHER_CREDENTIALS envvar contains valid JSON " + + "(not a file path).", e) + }.createScoped(listOf(AndroidPublisherScopes.ANDROIDPUBLISHER)) + + return AndroidPublisher.Builder( + transport, + GsonFactory.getDefaultInstance(), + AndroidPublisherAdapter(credential) + ).setApplicationName("PLUGIN_NAME").build() +} + +internal infix fun GoogleJsonResponseException.has(error: String) = + details?.errors.orEmpty().any { it.reason == error } + +private fun buildTransport(): HttpTransport { + val trustStore: String? = System.getProperty("javax.net.ssl.trustStore", null) + val trustStorePassword: String? = + System.getProperty("javax.net.ssl.trustStorePassword", null) + + return if (trustStore == null) { + createHttpTransport() + } else { + val ks = KeyStore.getInstance(KeyStore.getDefaultType()) + FileInputStream(trustStore).use { fis -> + ks.load(fis, trustStorePassword?.toCharArray()) + } + NetHttpTransport.Builder().trustCertificates(ks).build() + } +} + +private fun createHttpTransport(): HttpTransport { + val protocols = arrayOf("http", "https") + for (protocol in protocols) { + val proxyHost = System.getProperty("$protocol.proxyHost") + val proxyUser = System.getProperty("$protocol.proxyUser") + val proxyPassword = System.getProperty("$protocol.proxyPassword") + if (proxyHost != null && proxyUser != null && proxyPassword != null) { + val defaultProxyPort = if (protocol == "http") "80" else "443" + val proxyPort = Integer.parseInt(System.getProperty("$protocol.proxyPort", defaultProxyPort)) + val credentials = BasicCredentialsProvider() + credentials.setCredentials( + AuthScope(proxyHost, proxyPort), + UsernamePasswordCredentials(proxyUser, proxyPassword) + ) + val httpClient = ApacheHttpTransport.newDefaultHttpClientBuilder() + .setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE) + .setDefaultCredentialsProvider(credentials) + .build() + return ApacheHttpTransport(httpClient) + } + } + return GoogleNetHttpTransport.newTrustedTransport() +} + +private class AndroidPublisherAdapter( + credential: GoogleCredentials, +) : HttpCredentialsAdapter(credential) { + override fun initialize(request: HttpRequest) { + val backOffHandler = HttpBackOffUnsuccessfulResponseHandler( + ExponentialBackOff.Builder() + .setMaxElapsedTimeMillis(TimeUnit.MINUTES.toMillis(3).toInt()) + .build() + ) + + super.initialize( + request.setReadTimeout(0) + .setUnsuccessfulResponseHandler(backOffHandler) + ) + } +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt new file mode 100644 index 0000000..6b9ceae --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt @@ -0,0 +1,240 @@ +package ru.kode.android.build.publish.plugin.task.play.publisher + +import com.google.api.client.googleapis.json.GoogleJsonResponseException +import com.google.api.client.googleapis.media.MediaHttpUploader +import com.google.api.client.googleapis.services.AbstractGoogleClientRequest +import com.google.api.client.http.FileContent +import com.google.api.client.json.gson.GsonFactory +import com.google.api.services.androidpublisher.AndroidPublisher +import com.google.api.services.androidpublisher.model.Apk +import com.google.api.services.androidpublisher.model.AppDetails +import com.google.api.services.androidpublisher.model.Bundle +import com.google.api.services.androidpublisher.model.DeobfuscationFilesUploadResponse +import com.google.api.services.androidpublisher.model.ExpansionFile +import com.google.api.services.androidpublisher.model.Image +import com.google.api.services.androidpublisher.model.InAppProduct +import com.google.api.services.androidpublisher.model.Listing +import com.google.api.services.androidpublisher.model.Track +import java.io.File +import kotlin.math.roundToInt + +internal class DefaultPlayPublisher( + private val publisher: AndroidPublisher, + override val appId: String, +) : InternalPlayPublisher { + override fun insertEdit(): EditResponse { + return try { + EditResponse.Success(publisher.edits().insert(appId, null).execute().id) + } catch (e: GoogleJsonResponseException) { + EditResponse.Failure(e) + } + } + + override fun getEdit(id: String): EditResponse { + return try { + EditResponse.Success(publisher.edits().get(appId, id).execute().id) + } catch (e: GoogleJsonResponseException) { + EditResponse.Failure(e) + } + } + + override fun commitEdit(id: String, sendChangesForReview: Boolean): CommitResponse { + return try { + publisher.edits().commit(appId, id) + .setChangesNotSentForReview(!sendChangesForReview) + .execute() + CommitResponse.Success + } catch (e: GoogleJsonResponseException) { + CommitResponse.Failure(e) + } + } + + override fun validateEdit(id: String) { + publisher.edits().validate(appId, id).execute() + } + + override fun getAppDetails(editId: String): AppDetails { + return publisher.edits().details().get(appId, editId).execute() + } + + override fun getListings(editId: String): List { + return publisher.edits().listings().list(appId, editId).execute()?.listings.orEmpty() + } + + override fun getImages(editId: String, locale: String, type: String): List { + val response = publisher.edits().images().list(appId, editId, locale, type).execute() + return response?.images.orEmpty() + } + + override fun updateDetails(editId: String, details: AppDetails) { + publisher.edits().details().update(appId, editId, details).execute() + } + + override fun updateListing(editId: String, locale: String, listing: Listing) { + publisher.edits().listings().update(appId, editId, locale, listing).execute() + } + + override fun deleteImages(editId: String, locale: String, type: String) { + publisher.edits().images().deleteall(appId, editId, locale, type).execute() + } + + override fun uploadImage(editId: String, locale: String, type: String, image: File) { + val content = FileContent(MIME_TYPE_IMAGE, image) + publisher.edits().images().upload(appId, editId, locale, type, content).execute() + } + + override fun getTrack(editId: String, track: String): Track { + return try { + publisher.edits().tracks().get(appId, editId, track).execute() + } catch (e: GoogleJsonResponseException) { + if (e has "notFound") { + Track().setTrack(track) + } else { + throw e + } + } + } + + override fun listTracks(editId: String): List { + return publisher.edits().tracks().list(appId, editId).execute()?.tracks.orEmpty() + } + + override fun updateTrack(editId: String, track: Track) { + println("Updating ${track.releases.map { it.status }.distinct()} release " + + "($appId:${track.releases.flatMap { it.versionCodes.orEmpty() }}) " + + "in track '${track.track}'") + publisher.edits().tracks().update(appId, editId, track.track, track).execute() + } + + override fun uploadBundle(editId: String, bundleFile: File): Bundle { + val content = FileContent(MIME_TYPE_STREAM, bundleFile) + return publisher.edits().bundles().upload(appId, editId, content) + .trackUploadProgress("App Bundle", bundleFile) + .execute() + } + + override fun uploadApk(editId: String, apkFile: File): Apk { + val content = FileContent(MIME_TYPE_APK, apkFile) + return publisher.edits().apks().upload(appId, editId, content) + .trackUploadProgress("APK", apkFile) + .execute() + } + + override fun attachObb(editId: String, type: String, appVersion: Int, obbVersion: Int) { + val obb = ExpansionFile().also { it.referencesVersion = obbVersion } + publisher.edits().expansionfiles() + .update(appId, editId, appVersion, type, obb) + .execute() + } + + override fun uploadDeobfuscationFile( + editId: String, + file: File, + versionCode: Int, + type: String, + ): DeobfuscationFilesUploadResponse { + val mapping = FileContent(MIME_TYPE_STREAM, file) + val humanFileName = when (type) { + "proguard" -> "mapping" + "nativeCode" -> "native debug symbols" + else -> type + } + return publisher.edits().deobfuscationfiles() + .upload(appId, editId, versionCode, type, mapping) + .trackUploadProgress("$humanFileName file", file) + .execute() + } + + override fun uploadInternalSharingBundle(bundleFile: File): UploadInternalSharingArtifactResponse { + val bundle = publisher.internalappsharingartifacts() + .uploadbundle(appId, FileContent(MIME_TYPE_STREAM, bundleFile)) + .trackUploadProgress("App Bundle", bundleFile) + .execute() + + return UploadInternalSharingArtifactResponse(bundle.toPrettyString(), bundle.downloadUrl) + } + + override fun uploadInternalSharingApk(apkFile: File): UploadInternalSharingArtifactResponse { + val apk = publisher.internalappsharingartifacts() + .uploadapk(appId, FileContent(MIME_TYPE_APK, apkFile)) + .trackUploadProgress("APK", apkFile) + .execute() + + return UploadInternalSharingArtifactResponse(apk.toPrettyString(), apk.downloadUrl) + } + + override fun getInAppProducts(): List { + fun AndroidPublisher.Inappproducts.List.withToken(token: String?) = apply { + this.token = token + } + + val products = mutableListOf() + + var token: String? = null + do { + val response = publisher.inappproducts().list(appId).withToken(token).execute() + products += response.inappproduct.orEmpty() + token = response.tokenPagination?.nextPageToken + } while (token != null) + + return products.map { + GppProduct(it.sku, it.toPrettyString()) + } + } + + override fun insertInAppProduct(productFile: File) { + publisher.inappproducts().insert(appId, readProductFile(productFile)) + .apply { autoConvertMissingPrices = true } + .execute() + } + + override fun updateInAppProduct(productFile: File): UpdateProductResponse { + val product = readProductFile(productFile) + try { + publisher.inappproducts().update(appId, product.sku, product) + .apply { autoConvertMissingPrices = true } + .execute() + } catch (e: GoogleJsonResponseException) { + if (e.statusCode == 404) { + return UpdateProductResponse(true) + } else { + throw e + } + } + + return UpdateProductResponse(false) + } + + private fun readProductFile(product: File) = product.inputStream().use { + GsonFactory.getDefaultInstance() + .createJsonParser(it) + .parse(InAppProduct::class.java) + } + + private fun > R.trackUploadProgress( + thing: String, + file: File, + ): R { + mediaHttpUploader?.setProgressListener { + when (it.uploadState) { + MediaHttpUploader.UploadState.INITIATION_STARTED -> + println("Starting $thing upload: $file") + + MediaHttpUploader.UploadState.MEDIA_IN_PROGRESS -> + println("Uploading $thing: ${(it.progress * 100).roundToInt()}% complete") + + MediaHttpUploader.UploadState.MEDIA_COMPLETE -> + println("${thing.capitalize()} upload complete") + + else -> {} + } + } + return this + } + + private companion object { + const val MIME_TYPE_STREAM = "application/octet-stream" + const val MIME_TYPE_APK = "application/vnd.android.package-archive" + const val MIME_TYPE_IMAGE = "image/*" + } +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt new file mode 100644 index 0000000..2690433 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt @@ -0,0 +1,51 @@ +package ru.kode.android.build.publish.plugin.task.play.publisher + +import com.google.api.services.androidpublisher.model.Apk +import com.google.api.services.androidpublisher.model.AppDetails +import com.google.api.services.androidpublisher.model.Bundle +import com.google.api.services.androidpublisher.model.DeobfuscationFilesUploadResponse +import com.google.api.services.androidpublisher.model.Image +import com.google.api.services.androidpublisher.model.Listing +import com.google.api.services.androidpublisher.model.Track +import ru.kode.android.build.publish.plugin.task.play.PlayPublisher +import java.io.File +import java.io.IOException + +internal interface InternalPlayPublisher : PlayPublisher { + val appId: String + + fun getAppDetails(editId: String): AppDetails + + fun getListings(editId: String): List+ + fun getImages(editId: String, locale: String, type: String): List + + fun updateDetails(editId: String, details: AppDetails) + + fun updateListing(editId: String, locale: String, listing: Listing) + + fun deleteImages(editId: String, locale: String, type: String) + + fun uploadImage(editId: String, locale: String, type: String, image: File) + + fun getTrack(editId: String, track: String): Track + + fun listTracks(editId: String): List + + fun updateTrack(editId: String, track: Track) + + @Throws(IOException::class) + fun uploadBundle(editId: String, bundleFile: File): Bundle + + @Throws(IOException::class) + fun uploadApk(editId: String, apkFile: File): Apk + + fun attachObb(editId: String, type: String, appVersion: Int, obbVersion: Int) + + fun uploadDeobfuscationFile( + editId: String, + file: File, + versionCode: Int, + type: String, + ): DeobfuscationFilesUploadResponse +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt new file mode 100644 index 0000000..4b34a59 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt @@ -0,0 +1,51 @@ +package ru.kode.android.build.publish.plugin.task.play.publisher + +/** + * Models the possible release statuses for the track API. + * + * More docs are available + * [here](https://developers.google.com/android-publisher/api-ref/edits/tracks). + */ +enum class ReleaseStatus( + /** The API name of the status. */ + val publishedName: String, +) { + /** The release is live. */ + COMPLETED("completed"), + + /** The release is in draft mode. */ + DRAFT("draft"), + + /** The release was aborted. */ + HALTED("halted"), + + /** The release is still being rolled out. */ + IN_PROGRESS("inProgress") +} + +/** + * Models the possible resolution strategies for handling artifact upload conflicts. + * + * More docs are available + * [here](https://github.com/Triple-T/gradle-play-publisher#handling-version-conflicts). + */ +enum class ResolutionStrategy( + /** The API name of the strategy. */ + val publishedName: String, +) { + /** Conflicts should be automagically resolved. */ + AUTO("auto"), + + /** + * Unlike [AUTO] which diffs your Play Store version code with the local one, [AUTO_OFFSET] is + * much simpler and just adds the local version code to the Play Store one when + * `local <= play_store`. + */ + AUTO_OFFSET("auto_offset"), + + /** Fail the build at the first sign of conflict. */ + FAIL("fail"), + + /** Keep going and pretend like nothing happened. */ + IGNORE("ignore") +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt new file mode 100644 index 0000000..34d4cdb --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt @@ -0,0 +1,124 @@ +package ru.kode.android.build.publish.plugin.task.play.publisher + +import com.google.api.client.googleapis.json.GoogleJsonResponseException + +/** Response for an app details request. */ +data class GppAppDetails internal constructor( + /** The default language. */ + val defaultLocale: String?, + /** Developer contact email. */ + val contactEmail: String?, + /** Developer contact phone. */ + val contactPhone: String?, + /** Developer contact website. */ + val contactWebsite: String?, +) + +/** Response for an app listing request. */ +data class GppListing internal constructor( + /** The listing's language. */ + val locale: String, + /** The app description. */ + val fullDescription: String?, + /** The app tagline. */ + val shortDescription: String?, + /** The app title. */ + val title: String?, + /** The app promo url. */ + val video: String?, +) + +/** Response for an app graphic request. */ +data class GppImage internal constructor( + /** The image's download URL. */ + val url: String, + /** The image's SHA256 hash. */ + val sha256: String, +) + +/** Response for a track release note request. */ +data class ReleaseNote internal constructor( + /** The release note's track. */ + val track: String, + /** The release note's language. */ + val locale: String, + /** The release note. */ + val contents: String, +) + +/** Response for an edit request. */ +sealed class EditResponse { + /** Response for a successful edit request. */ + data class Success internal constructor( + /** The id of the edit in question. */ + val id: String, + ) : EditResponse() + + /** Response for an unsuccessful edit request. */ + data class Failure internal constructor( + private val e: GoogleJsonResponseException, + ) : EditResponse() { + /** @return true if the app wasn't found in the Play Console, false otherwise */ + fun isNewApp(): Boolean = e has "applicationNotFound" + + /** @return true if the provided edit is invalid for any reason, false otherwise */ + fun isInvalidEdit(): Boolean = + e has "editAlreadyCommitted" || e has "editNotFound" || e has "editExpired" + + /** @return true if the user doesn't have permission to access this app, false otherwise */ + fun isUnauthorized(): Boolean = e.statusCode == 401 + + /** Cleanly rethrows the error. */ + fun rethrow(): Nothing = throw e + + /** Wraps the error in a new exception with the provided [newMessage]. */ + fun rethrow(newMessage: String): Nothing = throw IllegalStateException(newMessage, e) + } +} + +/** Response for an commit request. */ +sealed class CommitResponse { + /** Response for a successful commit request. */ + object Success : CommitResponse() + + /** Response for an unsuccessful commit request. */ + data class Failure internal constructor( + private val e: GoogleJsonResponseException, + ) : CommitResponse() { + /** @return true if the changes cannot be sent for review, false otherwise */ + fun failedToSendForReview(): Boolean = + e has "badRequest" && e.details.message.orEmpty().contains("changesNotSentForReview") + + /** Cleanly rethrows the error. */ + fun rethrow(suppressed: Failure? = null): Nothing { + if (suppressed != null) { + e.addSuppressed(suppressed.e) + } + + throw e + } + } +} + +/** Response for an internal sharing artifact upload. */ +data class UploadInternalSharingArtifactResponse internal constructor( + /** The response's full JSON payload. */ + val json: String, + + /** The download URL of the uploaded artifact. */ + val downloadUrl: String, +) + +/** Response for a product request. */ +data class GppProduct internal constructor( + /** The product ID. */ + val sku: String, + /** The response's full JSON payload. */ + val json: String, +) + +/** Response for a product update request. */ +data class UpdateProductResponse internal constructor( + /** @return true if the product doesn't exist and needs to be created, false otherwise. */ + val needsCreating: Boolean, +) diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt new file mode 100644 index 0000000..29cafe2 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt @@ -0,0 +1,221 @@ +package ru.kode.android.build.publish.plugin.task.play.track + +import com.google.api.client.googleapis.json.GoogleJsonResponseException +import com.google.api.services.androidpublisher.model.AppDetails +import com.google.api.services.androidpublisher.model.Listing +import org.slf4j.LoggerFactory +import ru.kode.android.build.publish.plugin.task.play.publisher.GppAppDetails +import ru.kode.android.build.publish.plugin.task.play.publisher.GppImage +import ru.kode.android.build.publish.plugin.task.play.publisher.GppListing +import ru.kode.android.build.publish.plugin.task.play.publisher.InternalPlayPublisher +import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseNote +import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseStatus +import ru.kode.android.build.publish.plugin.task.play.publisher.ResolutionStrategy +import ru.kode.android.build.publish.plugin.task.play.publisher.has +import java.io.File + +internal class DefaultEditManager( + private val publisher: InternalPlayPublisher, + private val tracks: TrackManager, + private val editId: String, +) : EditManager { + override fun getAppDetails(): GppAppDetails { + val details = publisher.getAppDetails(editId) + return GppAppDetails( + details.defaultLanguage, + details.contactEmail, + details.contactPhone, + details.contactWebsite + ) + } + + override fun getListings(): List { + return publisher.getListings(editId).map { + GppListing( + it.language, + it.fullDescription, + it.shortDescription, + it.title, + it.video + ) + } + } + + override fun getImages(locale: String, type: String): List { + return publisher.getImages(editId, locale, type).map { + GppImage(it.url + HIGH_RES_IMAGE_REQUEST, it.sha256) + } + } + + override fun findMaxAppVersionCode(): Long { + return tracks.findHighestTrack()?.releases.orEmpty() + .flatMap { it.versionCodes.orEmpty() } + .maxOrNull() ?: 1 + } + + override fun findLeastStableTrackName(): String? { + return tracks.findHighestTrack()?.track + } + + override fun getReleaseNotes(): List { + return tracks.getReleaseNotes().map { (track, notes) -> + notes.map { ReleaseNote(track, it.language, it.text) } + }.flatten() + } + + override fun publishAppDetails( + defaultLocale: String?, + contactEmail: String?, + contactPhone: String?, + contactWebsite: String?, + ) { + publisher.updateDetails(editId, AppDetails().apply { + this.defaultLanguage = defaultLocale + this.contactEmail = contactEmail + this.contactPhone = contactPhone + this.contactWebsite = contactWebsite + }) + } + + override fun publishListing( + locale: String, + title: String?, + shortDescription: String?, + fullDescription: String?, + video: String?, + ) { + publisher.updateListing(editId, locale, Listing().apply { + this.title = title + this.shortDescription = shortDescription + this.fullDescription = fullDescription + this.video = video + }) + } + + override fun publishImages(locale: String, type: String, images: List) { + publisher.deleteImages(editId, locale, type) + for (image in images) { + println("Uploading $locale listing graphic for type '$type': ${image.name}") + // These can't be uploaded in parallel because order matters + publisher.uploadImage(editId, locale, type, image) + } + } + + override fun promoteRelease( + promoteTrackName: String, + fromTrackName: String, + releaseStatus: ReleaseStatus?, + releaseName: String?, + releaseNotes: Map?, + userFraction: Double?, + updatePriority: Int?, + retainableArtifacts: List?, + versionCode: Long?, + ) { + tracks.promote(TrackManager.PromoteConfig( + promoteTrackName, + fromTrackName, + versionCode, + TrackManager.BaseConfig( + releaseStatus, + userFraction, + updatePriority, + releaseNotes, + retainableArtifacts, + releaseName + ) + )) + } + + override fun uploadBundle( + bundleFile: File, + strategy: ResolutionStrategy, + ): Long? { + val bundle = try { + publisher.uploadBundle(editId, bundleFile) + } catch (e: GoogleJsonResponseException) { + handleUploadFailures(e, strategy, bundleFile) + return null + } + + return bundle.versionCode.toLong() + } + + override fun publishArtifacts( + versionCodes: List, + didPreviousBuildSkipCommit: Boolean, + trackName: String, + releaseStatus: ReleaseStatus?, + releaseName: String?, + releaseNotes: Map?, + userFraction: Double?, + updatePriority: Int?, + retainableArtifacts: List?, + ) { + if (versionCodes.isEmpty()) return + + tracks.update(TrackManager.UpdateConfig( + trackName, + versionCodes, + didPreviousBuildSkipCommit, + TrackManager.BaseConfig( + releaseStatus, + userFraction, + updatePriority, + releaseNotes, + retainableArtifacts, + releaseName + ) + )) + } + + private fun uploadMappingFile(versionCode: Int, mappingFile: File?) { + if (mappingFile != null && mappingFile.length() > 0) { + publisher.uploadDeobfuscationFile(editId, mappingFile, versionCode, "proguard") + } + } + + private fun Int.attachObb(type: String, versionCode: Int) { + println("Attaching $type OBB ($this) to APK $versionCode") + publisher.attachObb(editId, type, versionCode, this) + } + + private fun handleUploadFailures( + e: GoogleJsonResponseException, + strategy: ResolutionStrategy, + artifact: File, + ): Nothing? = if ( + e has "apkNotificationMessageKeyUpgradeVersionConflict" || + e has "apkUpgradeVersionConflict" || + e has "apkNoUpgradePath" || + e has "forbidden" && e.details.message.orEmpty().let { m -> + // Bundle message: APK specifies a version code that has already been used. + // APK message: Cannot update a published APK. + m.contains("version code", ignoreCase = true) || m.contains("Cannot update", ignoreCase = true) + } + ) { + when (strategy) { + ResolutionStrategy.AUTO, ResolutionStrategy.AUTO_OFFSET -> throw IllegalStateException( + "Concurrent uploads for app ${publisher.appId} (version code " + + "already used). Make sure to synchronously upload your APKs such " + + "that they don't conflict. If this problem persists, delete your " + + "drafts in the Play Console's artifact library.", + e + ) + + ResolutionStrategy.FAIL -> throw IllegalStateException( + "Version code is too low or has already been used for app " + + "${publisher.appId}.", + e + ) + + ResolutionStrategy.IGNORE -> LoggerFactory.getLogger(EditManager::class.java).warn( + "Ignoring artifact ($artifact)") + } + null + } else { + throw e + } +} + +private const val HIGH_RES_IMAGE_REQUEST = "=h16383" // Max res: 2^14 - 1 diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt new file mode 100644 index 0000000..5ba181d --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt @@ -0,0 +1,76 @@ +package ru.kode.android.build.publish.plugin.task.play.track + +import ru.kode.android.build.publish.plugin.task.play.PlayPublisher +import ru.kode.android.build.publish.plugin.task.play.publisher.GppAppDetails +import ru.kode.android.build.publish.plugin.task.play.publisher.GppImage +import ru.kode.android.build.publish.plugin.task.play.publisher.GppListing +import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseNote +import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseStatus +import ru.kode.android.build.publish.plugin.task.play.publisher.ResolutionStrategy +import java.io.File +import java.util.ServiceLoader + +interface EditManager { + fun getAppDetails(): GppAppDetails + fun getListings(): List + fun getImages(locale: String, type: String): List + fun findMaxAppVersionCode(): Long + fun findLeastStableTrackName(): String? + fun getReleaseNotes(): List + fun publishAppDetails( + defaultLocale: String?, + contactEmail: String?, + contactPhone: String?, + contactWebsite: String?, + ) + + fun publishListing( + locale: String, + title: String?, + shortDescription: String?, + fullDescription: String?, + video: String?, + ) + + fun publishImages(locale: String, type: String, images: List) + fun promoteRelease( + promoteTrackName: String, + fromTrackName: String, + releaseStatus: ReleaseStatus?, + releaseName: String?, + releaseNotes: Map?, + userFraction: Double?, + updatePriority: Int?, + retainableArtifacts: List?, + versionCode: Long? + ) + + fun uploadBundle( + bundleFile: File, + strategy: ResolutionStrategy, + ): Long? + + fun publishArtifacts( + versionCodes: List, + didPreviousBuildSkipCommit: Boolean, + trackName: String, + releaseStatus: ReleaseStatus?, + releaseName: String?, + releaseNotes: Map?, + userFraction: Double?, + updatePriority: Int?, + retainableArtifacts: List?, + ) + + interface Factory { + fun create(publisher: PlayPublisher, editId: String): EditManager + } + + companion object { + operator fun invoke( + publisher: PlayPublisher, + editId: String, + ): EditManager = ServiceLoader.load(Factory::class.java).last() + .create(publisher, editId) + } +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt new file mode 100644 index 0000000..fa614da --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt @@ -0,0 +1,242 @@ +package ru.kode.android.build.publish.plugin.task.play.track + +import com.google.api.services.androidpublisher.model.LocalizedText +import com.google.api.services.androidpublisher.model.Track +import com.google.api.services.androidpublisher.model.TrackRelease +import ru.kode.android.build.publish.plugin.task.play.publisher.InternalPlayPublisher +import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseStatus + +internal interface TrackManager { + fun findHighestTrack(): Track? + + fun getReleaseNotes(): Map> + + fun update(config: UpdateConfig) + + fun promote(config: PromoteConfig) + + data class BaseConfig( + val releaseStatus: ReleaseStatus? = null, + val userFraction: Double?, + val updatePriority: Int?, + val releaseNotes: Map? = emptyMap(), + val retainableArtifacts: List? = null, + val releaseName: String?, + ) + + data class UpdateConfig( + val trackName: String, + val versionCodes: List, + val didPreviousBuildSkipCommit: Boolean, + val base: BaseConfig, + ) + + data class PromoteConfig( + val promoteTrackName: String, + val fromTrackName: String, + val versionCode: Long?, + val base: BaseConfig, + ) +} + +internal class DefaultTrackManager( + private val publisher: InternalPlayPublisher, + private val editId: String, +) : TrackManager { + override fun findHighestTrack(): Track? { + return publisher.listTracks(editId).maxByOrNull { + it.releases.orEmpty().flatMap { it.versionCodes.orEmpty() }.maxOrNull() ?: 0 + } + } + + override fun getReleaseNotes(): Map> { + val releaseNotes = mutableMapOf>() + + val tracks = publisher.listTracks(editId) + for (track in tracks) { + val notes = track.releases?.maxByOrNull { + it.versionCodes?.maxOrNull() ?: Long.MIN_VALUE + }?.releaseNotes.orEmpty() + + releaseNotes[track.track] = notes + } + + return releaseNotes + } + + override fun update(config: TrackManager.UpdateConfig) { + val track = if (config.didPreviousBuildSkipCommit) { + createTrackForSkippedCommit(config) + } else if (config.base.releaseStatus.orDefault().isRollout()) { + createTrackForRollout(config) + } else { + createDefaultTrack(config) + } + + publisher.updateTrack(editId, track) + } + + override fun promote(config: TrackManager.PromoteConfig) { + val track = publisher.getTrack(editId, config.fromTrackName) + check(track.releases.orEmpty().flatMap { it.versionCodes.orEmpty() }.isNotEmpty()) { + "Track '${config.fromTrackName}' has no releases. Did you mean to run publish?" + } + + // Update the track + for (release in track.releases) { + release.mergeChanges(config.versionCode?.let { listOf(it) }, config.base) + } + // Only keep the unique statuses from the highest version code since duplicate statuses are + // not allowed. This is how we deal with an update from inProgress -> completed. We update + // all the tracks to completed, then get rid of the one that used to be inProgress. + track.releases = track.releases.sortedByDescending { + it.versionCodes?.maxOrNull() + }.distinctBy { + it.status + } + + println("Promoting release from track '${track.track}'") + track.track = config.promoteTrackName + publisher.updateTrack(editId, track) + } + + private fun createTrackForSkippedCommit(config: TrackManager.UpdateConfig): Track { + val track = publisher.getTrack(editId, config.trackName) + + if (track.releases.isNullOrEmpty()) { + track.releases = listOf(TrackRelease().mergeChanges(config.versionCodes, config.base)) + } else { + val hasReleaseToBeUpdated = track.releases.firstOrNull { + it.status == config.base.releaseStatus.orDefault().publishedName + } != null + + if (hasReleaseToBeUpdated) { + for (release in track.releases) { + if (release.status == config.base.releaseStatus.orDefault().publishedName) { + release.mergeChanges( + release.versionCodes.orEmpty() + config.versionCodes, config.base) + } + } + } else { + val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply { + maybeCopyChangelogFromPreviousRelease(config.trackName) + } + track.releases = track.releases + release + } + } + + return track + } + + private fun createTrackForRollout(config: TrackManager.UpdateConfig): Track { + val track = publisher.getTrack(editId, config.trackName) + + val keep = track.releases.orEmpty().filterNot { it.isRollout() } + val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply { + maybeCopyChangelogFromPreviousRelease(config.trackName) + } + track.releases = keep + release + + return track + } + + private fun createDefaultTrack(config: TrackManager.UpdateConfig) = Track().apply { + track = config.trackName + val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply { + maybeCopyChangelogFromPreviousRelease(config.trackName) + } + releases = listOf(release) + } + + private fun TrackRelease.maybeCopyChangelogFromPreviousRelease(trackName: String) { + if (!releaseNotes.isNullOrEmpty()) return + + val previousRelease = publisher.getTrack(editId, trackName) + .releases.orEmpty() + .maxByOrNull { it.versionCodes.orEmpty().maxOrNull() ?: 1 } + releaseNotes = previousRelease?.releaseNotes + } + + private fun TrackRelease.mergeChanges( + versionCodes: List?, + config: TrackManager.BaseConfig, + ) = apply { + updateVersionCodes(versionCodes, config.retainableArtifacts) + updateStatus(config.releaseStatus, config.userFraction != null) + updateConsoleName(config.releaseName) + updateReleaseNotes(config.releaseNotes) + updateUserFraction(config.userFraction) + updateUpdatePriority(config.updatePriority) + } + + private fun TrackRelease.updateVersionCodes(versionCodes: List?, retainableArtifacts: List?) { + val newVersions = versionCodes ?: this.versionCodes.orEmpty() + this.versionCodes = newVersions + retainableArtifacts.orEmpty() + } + + private fun TrackRelease.updateStatus(releaseStatus: ReleaseStatus?, hasUserFraction: Boolean) { + if (releaseStatus != null) { + status = releaseStatus.publishedName + } else if (hasUserFraction) { + status = ReleaseStatus.IN_PROGRESS.publishedName + } else if (status == null) { + status = DEFAULT_RELEASE_STATUS.publishedName + } + } + + private fun TrackRelease.updateConsoleName(releaseName: String?) { + if (releaseName != null) name = releaseName + } + + private fun TrackRelease.updateReleaseNotes(rawReleaseNotes: Map?) { + val releaseNotes = rawReleaseNotes.orEmpty().map { (locale, notes) -> + LocalizedText().apply { + language = locale + text = notes + } + } + val existingReleaseNotes = this.releaseNotes.orEmpty() + + this.releaseNotes = if (existingReleaseNotes.isEmpty()) { + releaseNotes + } else { + val merged = releaseNotes.toMutableList() + + for (existing in existingReleaseNotes) { + if (merged.none { it.language == existing.language }) merged += existing + } + + merged + } + } + + private fun TrackRelease.updateUserFraction(userFraction: Double?) { + if (userFraction != null) { + this.userFraction = userFraction.takeIf { isRollout() } + } else if (isRollout() && this.userFraction == null) { + this.userFraction = DEFAULT_USER_FRACTION + } else if (!isRollout()) { + this.userFraction = null + } + } + + private fun TrackRelease.updateUpdatePriority(updatePriority: Int?) { + if (updatePriority != null) { + inAppUpdatePriority = updatePriority + } + } + + private fun ReleaseStatus.isRollout() = + this == ReleaseStatus.IN_PROGRESS || this == ReleaseStatus.HALTED + + private fun TrackRelease.isRollout() = + status == ReleaseStatus.IN_PROGRESS.publishedName || + status == ReleaseStatus.HALTED.publishedName + + private fun ReleaseStatus?.orDefault() = this ?: DEFAULT_RELEASE_STATUS + + private companion object { + const val DEFAULT_USER_FRACTION = 0.1 + val DEFAULT_RELEASE_STATUS = ReleaseStatus.COMPLETED + } +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt new file mode 100644 index 0000000..30dfd84 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt @@ -0,0 +1,81 @@ +package ru.kode.android.build.publish.plugin.task.play.work + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.logging.Logging +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import ru.kode.android.build.publish.plugin.task.play.publisher.DefaultPlayPublisher +import ru.kode.android.build.publish.plugin.task.play.publisher.EditResponse +import ru.kode.android.build.publish.plugin.task.play.publisher.ResolutionStrategy +import ru.kode.android.build.publish.plugin.task.play.publisher.createPublisher +import ru.kode.android.build.publish.plugin.task.play.track.DefaultEditManager +import ru.kode.android.build.publish.plugin.task.play.track.DefaultTrackManager +import ru.kode.android.build.publish.plugin.task.play.track.TrackManager + +interface PlayUploadParameters : WorkParameters { + val appId: Property + val apiToken: RegularFileProperty + val outputFile: RegularFileProperty + val trackId: Property + val releaseName: Property + val versionCode: Property + val updatePriority: Property +} + +abstract class PlayUploadWork : WorkAction { + private val logger = Logging.getLogger(this::class.java) + + override fun execute() { + + val track = parameters.trackId.get() + val priority = parameters.updatePriority.orNull ?: 0 + val releaseName = parameters.releaseName.get() + val file = parameters.outputFile.asFile.get() + + val publisher = DefaultPlayPublisher( + publisher = createPublisher(parameters.apiToken.asFile.get().inputStream()), + appId = parameters.appId.get() + ) + logger.info("Step 1/3: Requesting track edit...") + val editId = when (val result = publisher.insertEdit()) { + is EditResponse.Success -> result.id + is EditResponse.Failure -> null + } + + if (editId == null) { + logger.error("Failed to fetch edit id to upload bundle. Check your credentials") + return + } + + val trackManager = DefaultTrackManager(publisher, editId) + val editManager = DefaultEditManager(publisher, trackManager, editId) + + logger.info("Step 2/3: Upload bundle for $editId") + + val versionCode = editManager.uploadBundle(file, ResolutionStrategy.IGNORE) + + if (versionCode == null) { + logger.error("Failed to upload bundle. Check your credentials") + return + } + + logger.info("Step 3/3: Pushing $releaseName to $track at P=$priority V=$versionCode") + + trackManager.update( + config = TrackManager.UpdateConfig( + trackName = track, + versionCodes = listOf(versionCode), + didPreviousBuildSkipCommit = false, + TrackManager.BaseConfig( + userFraction = parameters.versionCode.orNull ?: 0.1, + updatePriority = priority, + releaseName = parameters.releaseName.get() + ) + ) + ) + logger.info("Step 3/3: Bundle upload successful") + } + +} From 5bab5f3cc7cc1c487837832f540e263987fd343f Mon Sep 17 00:00:00 2001 From: rinekri Date: Mon, 15 Apr 2024 18:18:50 +0200 Subject: [PATCH 2/2] Add logic to get and apply bundle path to PlayPublishTask --- plugin-build/plugin/build.gradle.kts | 4 +- .../publish/plugin/BuildPublishPlugin.kt | 91 +++---- .../plugin/extension/BuildPublishExtension.kt | 1 - .../plugin/extension/config/PlayConfig.kt | 1 - .../plugin/task/play/PlayDistributionTask.kt | 11 +- .../publish/plugin/task/play/PlayPublisher.kt | 5 +- .../task/play/publisher/AndroidPublisher.kt | 52 ++-- .../play/publisher/DefaultPlayPublisher.kt | 120 +++++---- .../play/publisher/InternalPlayPublisher.kt | 44 ++-- .../task/play/publisher/PublisherModels.kt | 14 +- .../plugin/task/play/publisher/Responses.kt | 91 ++++--- .../task/play/track/DefaultEditManager.kt | 237 ++++++------------ .../plugin/task/play/track/EditManager.kt | 46 +--- .../plugin/task/play/track/TrackManager.kt | 144 +++++------ .../plugin/task/play/work/PlayUploadWork.kt | 53 ++-- 15 files changed, 383 insertions(+), 531 deletions(-) diff --git a/plugin-build/plugin/build.gradle.kts b/plugin-build/plugin/build.gradle.kts index 91b9db2..734ff52 100644 --- a/plugin-build/plugin/build.gradle.kts +++ b/plugin-build/plugin/build.gradle.kts @@ -33,7 +33,7 @@ gradlePlugin { id = "ru.kode.android.build-publish" displayName = "Configure project with Firebase App Distribution and changelogs" implementationClass = "ru.kode.android.build.publish.plugin.BuildPublishPlugin" - version = "1.3.2" + version = project.version description = "Android plugin to publish bundles and apks to Firebase App Distribution with changelogs" tags.set(listOf("firebase", "publish", "changelog", "build")) } @@ -45,7 +45,7 @@ publishing { create("maven") { groupId = project.group.toString() artifactId = "ru.kode.android.build-publish".removePrefix("$groupId.") - version = "1.3.2" + version = project.version.toString() from(components["java"]) } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt index 0f5b730..6fd4e6b 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt @@ -2,11 +2,11 @@ package ru.kode.android.build.publish.plugin +import com.android.build.api.artifact.SingleArtifact import com.android.build.api.variant.ApplicationAndroidComponentsExtension import com.android.build.api.variant.impl.VariantOutputImpl import com.android.build.gradle.AppExtension import com.android.build.gradle.AppPlugin -import com.android.build.gradle.internal.tasks.PackageBundleTask import com.android.build.gradle.tasks.PackageAndroidArtifact import com.android.builder.model.Version.ANDROID_GRADLE_PLUGIN_VERSION import com.google.firebase.appdistribution.gradle.AppDistributionExtension @@ -19,7 +19,6 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty -import org.gradle.api.logging.Logging import org.gradle.api.provider.Provider import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.StopExecutionException @@ -51,7 +50,6 @@ import ru.kode.android.build.publish.plugin.util.capitalizedName import java.io.File import java.time.LocalDate import java.time.format.DateTimeFormatter -import java.util.logging.Logger internal const val SEND_SLACK_CHANGELOG_TASK_PREFIX = "sendSlackChangelog" internal const val SEND_TELEGRAM_CHANGELOG_TASK_PREFIX = "sendTelegramChangelog" @@ -93,27 +91,26 @@ abstract class BuildPublishPlugin : Plugin { androidExtension.onVariants( callback = { variant -> - Logging.getLogger(this::class.java).info("FOUND VARIANT: $variant WITH OUTPUT NAME ${variant.outputs - .find { it is VariantOutputImpl && it.fullName == variant.name } - as? VariantOutputImpl}") - val output = variant.outputs .find { it is VariantOutputImpl && it.fullName == variant.name } as? VariantOutputImpl if (output != null) { val buildVariant = BuildVariant(variant.name, variant.flavorName, variant.buildType) - val outputFileName = output.outputFileName.get() + val apkOutputFileName = output.outputFileName.get() + + val bundleFile = variant.artifacts.get(SingleArtifact.BUNDLE) val outputProviders = project.registerVariantTasks( buildPublishExtension, buildVariant, changelogFile, - outputFileName, + apkOutputFileName, + bundleFile, grgitService, ) output.versionCode.set(outputProviders.versionCode) - output.outputFileName.set(outputProviders.outputFileName) + output.outputFileName.set(outputProviders.apkOutputFileName) output.versionName.set(outputProviders.versionName) } }, @@ -138,7 +135,8 @@ abstract class BuildPublishPlugin : Plugin { buildPublishExtension: BuildPublishExtension, buildVariant: BuildVariant, changelogFile: Provider, - outputFileName: String, + apkOutputFileName: String, + bundleFile: Provider, grgitService: Provider, ): OutputProviders { val outputConfig = @@ -160,15 +158,15 @@ abstract class BuildPublishPlugin : Plugin { project.provider { DEFAULT_VERSION_CODE } } } - val outputFileNameProvider = + val apkOutputFileNameProvider = useVersionsFromTagProvider.flatMap { useVersionsFromTag -> if (useVersionsFromTag) { outputConfig.baseFileName.zip(tagBuildProvider) { baseFileName, tagBuildFile -> - mapToOutputFileName(tagBuildFile, outputFileName, baseFileName) + mapToOutputApkFileName(tagBuildFile, apkOutputFileName, baseFileName) } } else { outputConfig.baseFileName.map { baseFileName -> - createDefaultOutputFileName(baseFileName, outputFileName) + createDefaultOutputFileName(baseFileName, apkOutputFileName) } } } @@ -182,9 +180,9 @@ abstract class BuildPublishPlugin : Plugin { project.provider { DEFAULT_VERSION_NAME } } } - val outputFileProvider = - outputFileNameProvider.flatMap { fileName -> - mapToOutputFile(buildVariant, fileName) + val apkOutputFileProvider = + apkOutputFileNameProvider.flatMap { fileName -> + mapToOutputApkFile(buildVariant, fileName) } tasks.registerPrintLastIncreasedTagTask( buildVariant, @@ -230,7 +228,7 @@ abstract class BuildPublishPlugin : Plugin { buildVariant, generateChangelogFileProvider, tagBuildProvider, - outputFileProvider, + apkOutputFileProvider, ) } val appCenterDistributionConfig = @@ -243,7 +241,7 @@ abstract class BuildPublishPlugin : Plugin { config = appCenterDistributionConfig, buildVariant = buildVariant, changelogFileProvider = generateChangelogFileProvider, - buildVariantOutputFileProvider = outputFileProvider, + apkOutputFileProvider = apkOutputFileProvider, tagBuildProvider = tagBuildProvider, outputConfig = outputConfig, ) @@ -258,7 +256,7 @@ abstract class BuildPublishPlugin : Plugin { PlayTaskParams( config = playConfig, buildVariant = buildVariant, - buildVariantOutputFileProvider = outputFileProvider, + bundleOutputFileProvider = bundleFile, tagBuildProvider = tagBuildProvider, outputConfig = outputConfig, ) @@ -281,7 +279,7 @@ abstract class BuildPublishPlugin : Plugin { return OutputProviders( versionName = versionNameProvider, versionCode = versionCodeProvider, - outputFileName = outputFileNameProvider, + apkOutputFileName = apkOutputFileNameProvider, ) } @@ -323,7 +321,7 @@ abstract class BuildPublishPlugin : Plugin { buildVariant: BuildVariant, generateChangelogFileProvider: Provider, tagBuildProvider: Provider, - outputFileProvider: Provider, + apkOutputFileProvider: Provider, ) { registerSendSlackChangelogTask( outputConfig, @@ -341,7 +339,7 @@ abstract class BuildPublishPlugin : Plugin { slackConfig.uploadApiTokenFile, slackConfig.uploadChannels, buildVariant, - outputFileProvider, + apkOutputFileProvider, ) } } @@ -350,13 +348,13 @@ abstract class BuildPublishPlugin : Plugin { apiTokenFile: RegularFileProperty, channels: SetProperty, buildVariant: BuildVariant, - outputFileProvider: Provider, + apkOutputFileProvider: Provider, ) { register( "$SLACK_DISTRIBUTION_UPLOAD_TASK_PREFIX${buildVariant.capitalizedName()}", SlackDistributionTask::class.java, ) { - it.buildVariantOutputFile.set(outputFileProvider) + it.buildVariantOutputFile.set(apkOutputFileProvider) it.apiTokenFile.set(apiTokenFile) it.channels.set(channels) } @@ -474,7 +472,7 @@ abstract class BuildPublishPlugin : Plugin { AppCenterDistributionTask::class.java, ) { it.tagBuildFile.set(params.tagBuildProvider) - it.buildVariantOutputFile.set(params.buildVariantOutputFileProvider) + it.buildVariantOutputFile.set(params.apkOutputFileProvider) it.changelogFile.set(params.changelogFileProvider) it.apiTokenFile.set(config.apiTokenFile) it.ownerName.set(config.ownerName) @@ -487,9 +485,7 @@ abstract class BuildPublishPlugin : Plugin { } } - private fun TaskContainer.registerPlayTask( - params: PlayTaskParams, - ): TaskProvider { + private fun TaskContainer.registerPlayTask(params: PlayTaskParams): TaskProvider { val buildVariant = params.buildVariant val config = params.config @@ -498,7 +494,7 @@ abstract class BuildPublishPlugin : Plugin { PlayDistributionTask::class.java, ) { it.tagBuildFile.set(params.tagBuildProvider) - it.buildVariantOutputFile.set(params.buildVariantOutputFileProvider) + it.buildVariantOutputFile.set(params.bundleOutputFileProvider) it.apiTokenFile.set(config.apiTokenFile) it.appId.set(config.appId) it.trackId.set(config.trackId) @@ -584,23 +580,22 @@ private fun AppDistributionExtension.configure( private data class OutputProviders( val versionName: Provider, val versionCode: Provider, - val outputFileName: Provider, + val apkOutputFileName: Provider, ) private data class AppCenterDistributionTaskParams( val config: AppCenterDistributionConfig, val buildVariant: BuildVariant, val changelogFileProvider: Provider, - val buildVariantOutputFileProvider: Provider, + val apkOutputFileProvider: Provider, val tagBuildProvider: Provider, val outputConfig: OutputConfig, ) - private data class PlayTaskParams( val config: PlayConfig, val buildVariant: BuildVariant, - val buildVariantOutputFileProvider: Provider, + val bundleOutputFileProvider: Provider, val tagBuildProvider: Provider, val outputConfig: OutputConfig, ) @@ -614,7 +609,7 @@ private fun mapToVersionCode(tagBuildFile: RegularFile): Int { } } -private fun mapToOutputFileName( +private fun mapToOutputApkFileName( tagBuildFile: RegularFile, outputFileName: String, baseFileName: String?, @@ -626,15 +621,8 @@ private fun mapToOutputFileName( val versionName = tagBuild.buildVariant val versionCode = tagBuild.buildNumber "$baseFileName-$versionName-vc$versionCode-$formattedDate.apk" - } else if (file.exists() && outputFileName.endsWith(".aab")) { - val tagBuild = fromJson(file) - val versionName = tagBuild.buildVariant - val versionCode = tagBuild.buildNumber - "$baseFileName-$versionName-vc$versionCode-$formattedDate.aab" } else if (!file.exists() && outputFileName.endsWith(".apk")) { "$baseFileName-$formattedDate.apk" - } else if (!file.exists() && outputFileName.endsWith(".aab")) { - "$baseFileName-$formattedDate.aab" } else { createDefaultOutputFileName(baseFileName, outputFileName) } @@ -660,20 +648,13 @@ private fun mapToVersionName( } } -private fun Project.mapToOutputFile( +private fun Project.mapToOutputApkFile( buildVariant: BuildVariant, fileName: String, ): Provider { - return if (fileName.endsWith("aab")) { - project.tasks.withType(PackageBundleTask::class.java) - .firstOrNull { it.variantName == buildVariant.name } - ?.bundleFile - ?: throw GradleException("no output for variant ${buildVariant.name}") - } else { - project.tasks.withType(PackageAndroidArtifact::class.java) - .firstOrNull { it.variantName == buildVariant.name } - ?.outputDirectory - ?.map { directory -> directory.file(fileName) } - ?: throw GradleException("no output for variant ${buildVariant.name}") - } + return project.tasks.withType(PackageAndroidArtifact::class.java) + .firstOrNull { it.variantName == buildVariant.name } + ?.outputDirectory + ?.map { directory -> directory.file(fileName) } + ?: throw GradleException("no output for variant ${buildVariant.name}") } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt index 5c53e64..4c74087 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt @@ -10,7 +10,6 @@ import ru.kode.android.build.publish.plugin.extension.config.OutputConfig import ru.kode.android.build.publish.plugin.extension.config.PlayConfig import ru.kode.android.build.publish.plugin.extension.config.SlackConfig import ru.kode.android.build.publish.plugin.extension.config.TelegramConfig -import ru.kode.android.build.publish.plugin.task.play.PlayDistributionTask import javax.inject.Inject const val EXTENSION_NAME = "buildPublish" diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt index c18da4c..e56f55c 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/PlayConfig.kt @@ -2,7 +2,6 @@ package ru.kode.android.build.publish.plugin.extension.config import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt index ba83483..d1e2654 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayDistributionTask.kt @@ -5,7 +5,6 @@ import org.gradle.api.GradleException import org.gradle.api.file.RegularFileProperty import org.gradle.api.plugins.BasePlugin import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.Optional @@ -14,13 +13,14 @@ import org.gradle.api.tasks.options.Option import org.gradle.workers.WorkQueue import org.gradle.workers.WorkerExecutor import ru.kode.android.build.publish.plugin.enity.mapper.fromJson -import ru.kode.android.build.publish.plugin.extension.config.MAX_REQUEST_COUNT -import ru.kode.android.build.publish.plugin.extension.config.MAX_REQUEST_DELAY_MS -import ru.kode.android.build.publish.plugin.task.appcenter.work.AppCenterUploadWork import ru.kode.android.build.publish.plugin.task.play.work.PlayUploadWork -import ru.kode.android.build.publish.plugin.util.capitalized import javax.inject.Inject +/** + * Task to publish app at given release track in google play console with set priority + * Contains basic functionality, for the extensions reference to original implementation: + * https://github.com/Triple-T/gradle-play-publisher + */ abstract class PlayDistributionTask @Inject constructor( @@ -56,7 +56,6 @@ abstract class PlayDistributionTask ) abstract val appId: Property - @get:Input @get:Option( option = "trackId", diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt index 5e654bd..5acafd1 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/PlayPublisher.kt @@ -36,7 +36,10 @@ interface PlayPublisher { * More docs are available * [here](https://developers.google.com/android-publisher/api-ref/edits/commit). */ - fun commitEdit(id: String, sendChangesForReview: Boolean = true): CommitResponse + fun commitEdit( + id: String, + sendChangesForReview: Boolean = true, + ): CommitResponse /** * Validates an edit with the given [id]. diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt index cf62305..f4315cc 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/AndroidPublisher.kt @@ -19,30 +19,34 @@ import org.apache.http.impl.client.BasicCredentialsProvider import org.apache.http.impl.client.ProxyAuthenticationStrategy import java.io.FileInputStream import java.io.InputStream +import java.lang.IllegalStateException import java.security.KeyStore import java.util.concurrent.TimeUnit +@Suppress("TooGenericExceptionCaught") // Exception is rethrown with proper message internal fun createPublisher(credentials: InputStream): AndroidPublisher { val transport = buildTransport() - val credential = try { - GoogleCredentials.fromStream(credentials) { transport } - } catch (e: Exception) { - throw Exception( - "Credential parsing may have failed. " + - "Ensure credential files supplied in the DSL contain valid JSON " + - "and/or the ANDROID_PUBLISHER_CREDENTIALS envvar contains valid JSON " + - "(not a file path).", e) - }.createScoped(listOf(AndroidPublisherScopes.ANDROIDPUBLISHER)) + val credential = + try { + GoogleCredentials.fromStream(credentials) { transport } + } catch (e: Exception) { + throw IllegalStateException( + "Credential parsing may have failed. " + + "Ensure credential files supplied in the DSL contain valid JSON " + + "and/or the ANDROID_PUBLISHER_CREDENTIALS envvar contains valid JSON " + + "(not a file path).", + e, + ) + }.createScoped(listOf(AndroidPublisherScopes.ANDROIDPUBLISHER)) return AndroidPublisher.Builder( transport, GsonFactory.getDefaultInstance(), - AndroidPublisherAdapter(credential) + AndroidPublisherAdapter(credential), ).setApplicationName("PLUGIN_NAME").build() } -internal infix fun GoogleJsonResponseException.has(error: String) = - details?.errors.orEmpty().any { it.reason == error } +internal infix fun GoogleJsonResponseException.has(error: String) = details?.errors.orEmpty().any { it.reason == error } private fun buildTransport(): HttpTransport { val trustStore: String? = System.getProperty("javax.net.ssl.trustStore", null) @@ -72,12 +76,13 @@ private fun createHttpTransport(): HttpTransport { val credentials = BasicCredentialsProvider() credentials.setCredentials( AuthScope(proxyHost, proxyPort), - UsernamePasswordCredentials(proxyUser, proxyPassword) + UsernamePasswordCredentials(proxyUser, proxyPassword), ) - val httpClient = ApacheHttpTransport.newDefaultHttpClientBuilder() - .setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE) - .setDefaultCredentialsProvider(credentials) - .build() + val httpClient = + ApacheHttpTransport.newDefaultHttpClientBuilder() + .setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE) + .setDefaultCredentialsProvider(credentials) + .build() return ApacheHttpTransport(httpClient) } } @@ -88,15 +93,16 @@ private class AndroidPublisherAdapter( credential: GoogleCredentials, ) : HttpCredentialsAdapter(credential) { override fun initialize(request: HttpRequest) { - val backOffHandler = HttpBackOffUnsuccessfulResponseHandler( - ExponentialBackOff.Builder() - .setMaxElapsedTimeMillis(TimeUnit.MINUTES.toMillis(3).toInt()) - .build() - ) + val backOffHandler = + HttpBackOffUnsuccessfulResponseHandler( + ExponentialBackOff.Builder() + .setMaxElapsedTimeMillis(TimeUnit.MINUTES.toMillis(3).toInt()) + .build(), + ) super.initialize( request.setReadTimeout(0) - .setUnsuccessfulResponseHandler(backOffHandler) + .setUnsuccessfulResponseHandler(backOffHandler), ) } } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt index 6b9ceae..6fba296 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/DefaultPlayPublisher.kt @@ -7,13 +7,10 @@ import com.google.api.client.http.FileContent import com.google.api.client.json.gson.GsonFactory import com.google.api.services.androidpublisher.AndroidPublisher import com.google.api.services.androidpublisher.model.Apk -import com.google.api.services.androidpublisher.model.AppDetails import com.google.api.services.androidpublisher.model.Bundle import com.google.api.services.androidpublisher.model.DeobfuscationFilesUploadResponse import com.google.api.services.androidpublisher.model.ExpansionFile -import com.google.api.services.androidpublisher.model.Image import com.google.api.services.androidpublisher.model.InAppProduct -import com.google.api.services.androidpublisher.model.Listing import com.google.api.services.androidpublisher.model.Track import java.io.File import kotlin.math.roundToInt @@ -38,7 +35,10 @@ internal class DefaultPlayPublisher( } } - override fun commitEdit(id: String, sendChangesForReview: Boolean): CommitResponse { + override fun commitEdit( + id: String, + sendChangesForReview: Boolean, + ): CommitResponse { return try { publisher.edits().commit(appId, id) .setChangesNotSentForReview(!sendChangesForReview) @@ -53,37 +53,10 @@ internal class DefaultPlayPublisher( publisher.edits().validate(appId, id).execute() } - override fun getAppDetails(editId: String): AppDetails { - return publisher.edits().details().get(appId, editId).execute() - } - - override fun getListings(editId: String): List { - return publisher.edits().listings().list(appId, editId).execute()?.listings.orEmpty() - } - - override fun getImages(editId: String, locale: String, type: String): List { - val response = publisher.edits().images().list(appId, editId, locale, type).execute() - return response?.images.orEmpty() - } - - override fun updateDetails(editId: String, details: AppDetails) { - publisher.edits().details().update(appId, editId, details).execute() - } - - override fun updateListing(editId: String, locale: String, listing: Listing) { - publisher.edits().listings().update(appId, editId, locale, listing).execute() - } - - override fun deleteImages(editId: String, locale: String, type: String) { - publisher.edits().images().deleteall(appId, editId, locale, type).execute() - } - - override fun uploadImage(editId: String, locale: String, type: String, image: File) { - val content = FileContent(MIME_TYPE_IMAGE, image) - publisher.edits().images().upload(appId, editId, locale, type, content).execute() - } - - override fun getTrack(editId: String, track: String): Track { + override fun getTrack( + editId: String, + track: String, + ): Track { return try { publisher.edits().tracks().get(appId, editId, track).execute() } catch (e: GoogleJsonResponseException) { @@ -99,28 +72,44 @@ internal class DefaultPlayPublisher( return publisher.edits().tracks().list(appId, editId).execute()?.tracks.orEmpty() } - override fun updateTrack(editId: String, track: Track) { - println("Updating ${track.releases.map { it.status }.distinct()} release " + - "($appId:${track.releases.flatMap { it.versionCodes.orEmpty() }}) " + - "in track '${track.track}'") + override fun updateTrack( + editId: String, + track: Track, + ) { + println( + "Updating ${track.releases.map { it.status }.distinct()} release " + + "($appId:${track.releases.flatMap { it.versionCodes.orEmpty() }}) " + + "in track '${track.track}'", + ) publisher.edits().tracks().update(appId, editId, track.track, track).execute() } - override fun uploadBundle(editId: String, bundleFile: File): Bundle { + override fun uploadBundle( + editId: String, + bundleFile: File, + ): Bundle { val content = FileContent(MIME_TYPE_STREAM, bundleFile) return publisher.edits().bundles().upload(appId, editId, content) .trackUploadProgress("App Bundle", bundleFile) .execute() } - override fun uploadApk(editId: String, apkFile: File): Apk { + override fun uploadApk( + editId: String, + apkFile: File, + ): Apk { val content = FileContent(MIME_TYPE_APK, apkFile) return publisher.edits().apks().upload(appId, editId, content) .trackUploadProgress("APK", apkFile) .execute() } - override fun attachObb(editId: String, type: String, appVersion: Int, obbVersion: Int) { + override fun attachObb( + editId: String, + type: String, + appVersion: Int, + obbVersion: Int, + ) { val obb = ExpansionFile().also { it.referencesVersion = obbVersion } publisher.edits().expansionfiles() .update(appId, editId, appVersion, type, obb) @@ -134,11 +123,12 @@ internal class DefaultPlayPublisher( type: String, ): DeobfuscationFilesUploadResponse { val mapping = FileContent(MIME_TYPE_STREAM, file) - val humanFileName = when (type) { - "proguard" -> "mapping" - "nativeCode" -> "native debug symbols" - else -> type - } + val humanFileName = + when (type) { + "proguard" -> "mapping" + "nativeCode" -> "native debug symbols" + else -> type + } return publisher.edits().deobfuscationfiles() .upload(appId, editId, versionCode, type, mapping) .trackUploadProgress("$humanFileName file", file) @@ -146,27 +136,30 @@ internal class DefaultPlayPublisher( } override fun uploadInternalSharingBundle(bundleFile: File): UploadInternalSharingArtifactResponse { - val bundle = publisher.internalappsharingartifacts() - .uploadbundle(appId, FileContent(MIME_TYPE_STREAM, bundleFile)) - .trackUploadProgress("App Bundle", bundleFile) - .execute() + val bundle = + publisher.internalappsharingartifacts() + .uploadbundle(appId, FileContent(MIME_TYPE_STREAM, bundleFile)) + .trackUploadProgress("App Bundle", bundleFile) + .execute() return UploadInternalSharingArtifactResponse(bundle.toPrettyString(), bundle.downloadUrl) } override fun uploadInternalSharingApk(apkFile: File): UploadInternalSharingArtifactResponse { - val apk = publisher.internalappsharingartifacts() - .uploadapk(appId, FileContent(MIME_TYPE_APK, apkFile)) - .trackUploadProgress("APK", apkFile) - .execute() + val apk = + publisher.internalappsharingartifacts() + .uploadapk(appId, FileContent(MIME_TYPE_APK, apkFile)) + .trackUploadProgress("APK", apkFile) + .execute() return UploadInternalSharingArtifactResponse(apk.toPrettyString(), apk.downloadUrl) } override fun getInAppProducts(): List { - fun AndroidPublisher.Inappproducts.List.withToken(token: String?) = apply { - this.token = token - } + fun AndroidPublisher.Inappproducts.List.withToken(token: String?) = + apply { + this.token = token + } val products = mutableListOf() @@ -205,11 +198,12 @@ internal class DefaultPlayPublisher( return UpdateProductResponse(false) } - private fun readProductFile(product: File) = product.inputStream().use { - GsonFactory.getDefaultInstance() - .createJsonParser(it) - .parse(InAppProduct::class.java) - } + private fun readProductFile(product: File) = + product.inputStream().use { + GsonFactory.getDefaultInstance() + .createJsonParser(it) + .parse(InAppProduct::class.java) + } private fun > R.trackUploadProgress( thing: String, diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt index 2690433..ec40730 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/InternalPlayPublisher.kt @@ -1,11 +1,8 @@ package ru.kode.android.build.publish.plugin.task.play.publisher import com.google.api.services.androidpublisher.model.Apk -import com.google.api.services.androidpublisher.model.AppDetails import com.google.api.services.androidpublisher.model.Bundle import com.google.api.services.androidpublisher.model.DeobfuscationFilesUploadResponse -import com.google.api.services.androidpublisher.model.Image -import com.google.api.services.androidpublisher.model.Listing import com.google.api.services.androidpublisher.model.Track import ru.kode.android.build.publish.plugin.task.play.PlayPublisher import java.io.File @@ -14,33 +11,36 @@ import java.io.IOException internal interface InternalPlayPublisher : PlayPublisher { val appId: String - fun getAppDetails(editId: String): AppDetails - - fun getListings(editId: String): List- - fun getImages(editId: String, locale: String, type: String): List - - fun updateDetails(editId: String, details: AppDetails) - - fun updateListing(editId: String, locale: String, listing: Listing) - - fun deleteImages(editId: String, locale: String, type: String) - - fun uploadImage(editId: String, locale: String, type: String, image: File) - - fun getTrack(editId: String, track: String): Track + fun getTrack( + editId: String, + track: String, + ): Track fun listTracks(editId: String): List - fun updateTrack(editId: String, track: Track) + fun updateTrack( + editId: String, + track: Track, + ) @Throws(IOException::class) - fun uploadBundle(editId: String, bundleFile: File): Bundle + fun uploadBundle( + editId: String, + bundleFile: File, + ): Bundle @Throws(IOException::class) - fun uploadApk(editId: String, apkFile: File): Apk + fun uploadApk( + editId: String, + apkFile: File, + ): Apk - fun attachObb(editId: String, type: String, appVersion: Int, obbVersion: Int) + fun attachObb( + editId: String, + type: String, + appVersion: Int, + obbVersion: Int, + ) fun uploadDeobfuscationFile( editId: String, diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt index 4b34a59..d364615 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/PublisherModels.kt @@ -6,9 +6,10 @@ package ru.kode.android.build.publish.plugin.task.play.publisher * More docs are available * [here](https://developers.google.com/android-publisher/api-ref/edits/tracks). */ +@Suppress("EnumNaming") // Google API Model enum class ReleaseStatus( - /** The API name of the status. */ - val publishedName: String, + /** The API name of the status. */ + val publishedName: String, ) { /** The release is live. */ COMPLETED("completed"), @@ -20,7 +21,7 @@ enum class ReleaseStatus( HALTED("halted"), /** The release is still being rolled out. */ - IN_PROGRESS("inProgress") + IN_PROGRESS("inProgress"), } /** @@ -29,9 +30,10 @@ enum class ReleaseStatus( * More docs are available * [here](https://github.com/Triple-T/gradle-play-publisher#handling-version-conflicts). */ +@Suppress("EnumNaming") // Google API Model enum class ResolutionStrategy( - /** The API name of the strategy. */ - val publishedName: String, + /** The API name of the strategy. */ + val publishedName: String, ) { /** Conflicts should be automagically resolved. */ AUTO("auto"), @@ -47,5 +49,5 @@ enum class ResolutionStrategy( FAIL("fail"), /** Keep going and pretend like nothing happened. */ - IGNORE("ignore") + IGNORE("ignore"), } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt index 34d4cdb..28a6bb1 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/publisher/Responses.kt @@ -4,66 +4,65 @@ import com.google.api.client.googleapis.json.GoogleJsonResponseException /** Response for an app details request. */ data class GppAppDetails internal constructor( - /** The default language. */ - val defaultLocale: String?, - /** Developer contact email. */ - val contactEmail: String?, - /** Developer contact phone. */ - val contactPhone: String?, - /** Developer contact website. */ - val contactWebsite: String?, + /** The default language. */ + val defaultLocale: String?, + /** Developer contact email. */ + val contactEmail: String?, + /** Developer contact phone. */ + val contactPhone: String?, + /** Developer contact website. */ + val contactWebsite: String?, ) /** Response for an app listing request. */ data class GppListing internal constructor( - /** The listing's language. */ - val locale: String, - /** The app description. */ - val fullDescription: String?, - /** The app tagline. */ - val shortDescription: String?, - /** The app title. */ - val title: String?, - /** The app promo url. */ - val video: String?, + /** The listing's language. */ + val locale: String, + /** The app description. */ + val fullDescription: String?, + /** The app tagline. */ + val shortDescription: String?, + /** The app title. */ + val title: String?, + /** The app promo url. */ + val video: String?, ) /** Response for an app graphic request. */ data class GppImage internal constructor( - /** The image's download URL. */ - val url: String, - /** The image's SHA256 hash. */ - val sha256: String, + /** The image's download URL. */ + val url: String, + /** The image's SHA256 hash. */ + val sha256: String, ) /** Response for a track release note request. */ data class ReleaseNote internal constructor( - /** The release note's track. */ - val track: String, - /** The release note's language. */ - val locale: String, - /** The release note. */ - val contents: String, + /** The release note's track. */ + val track: String, + /** The release note's language. */ + val locale: String, + /** The release note. */ + val contents: String, ) /** Response for an edit request. */ sealed class EditResponse { /** Response for a successful edit request. */ data class Success internal constructor( - /** The id of the edit in question. */ - val id: String, + /** The id of the edit in question. */ + val id: String, ) : EditResponse() /** Response for an unsuccessful edit request. */ data class Failure internal constructor( - private val e: GoogleJsonResponseException, + private val e: GoogleJsonResponseException, ) : EditResponse() { /** @return true if the app wasn't found in the Play Console, false otherwise */ fun isNewApp(): Boolean = e has "applicationNotFound" /** @return true if the provided edit is invalid for any reason, false otherwise */ - fun isInvalidEdit(): Boolean = - e has "editAlreadyCommitted" || e has "editNotFound" || e has "editExpired" + fun isInvalidEdit(): Boolean = e has "editAlreadyCommitted" || e has "editNotFound" || e has "editExpired" /** @return true if the user doesn't have permission to access this app, false otherwise */ fun isUnauthorized(): Boolean = e.statusCode == 401 @@ -83,11 +82,12 @@ sealed class CommitResponse { /** Response for an unsuccessful commit request. */ data class Failure internal constructor( - private val e: GoogleJsonResponseException, + private val e: GoogleJsonResponseException, ) : CommitResponse() { /** @return true if the changes cannot be sent for review, false otherwise */ fun failedToSendForReview(): Boolean = - e has "badRequest" && e.details.message.orEmpty().contains("changesNotSentForReview") + e has "badRequest" && + e.details.message.orEmpty().contains("changesNotSentForReview") /** Cleanly rethrows the error. */ fun rethrow(suppressed: Failure? = null): Nothing { @@ -102,23 +102,22 @@ sealed class CommitResponse { /** Response for an internal sharing artifact upload. */ data class UploadInternalSharingArtifactResponse internal constructor( - /** The response's full JSON payload. */ - val json: String, - - /** The download URL of the uploaded artifact. */ - val downloadUrl: String, + /** The response's full JSON payload. */ + val json: String, + /** The download URL of the uploaded artifact. */ + val downloadUrl: String, ) /** Response for a product request. */ data class GppProduct internal constructor( - /** The product ID. */ - val sku: String, - /** The response's full JSON payload. */ - val json: String, + /** The product ID. */ + val sku: String, + /** The response's full JSON payload. */ + val json: String, ) /** Response for a product update request. */ data class UpdateProductResponse internal constructor( - /** @return true if the product doesn't exist and needs to be created, false otherwise. */ - val needsCreating: Boolean, + /** @return true if the product doesn't exist and needs to be created, false otherwise. */ + val needsCreating: Boolean, ) diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt index 29cafe2..2572e78 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/DefaultEditManager.kt @@ -1,14 +1,8 @@ package ru.kode.android.build.publish.plugin.task.play.track import com.google.api.client.googleapis.json.GoogleJsonResponseException -import com.google.api.services.androidpublisher.model.AppDetails -import com.google.api.services.androidpublisher.model.Listing import org.slf4j.LoggerFactory -import ru.kode.android.build.publish.plugin.task.play.publisher.GppAppDetails -import ru.kode.android.build.publish.plugin.task.play.publisher.GppImage -import ru.kode.android.build.publish.plugin.task.play.publisher.GppListing import ru.kode.android.build.publish.plugin.task.play.publisher.InternalPlayPublisher -import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseNote import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseStatus import ru.kode.android.build.publish.plugin.task.play.publisher.ResolutionStrategy import ru.kode.android.build.publish.plugin.task.play.publisher.has @@ -19,88 +13,6 @@ internal class DefaultEditManager( private val tracks: TrackManager, private val editId: String, ) : EditManager { - override fun getAppDetails(): GppAppDetails { - val details = publisher.getAppDetails(editId) - return GppAppDetails( - details.defaultLanguage, - details.contactEmail, - details.contactPhone, - details.contactWebsite - ) - } - - override fun getListings(): List { - return publisher.getListings(editId).map { - GppListing( - it.language, - it.fullDescription, - it.shortDescription, - it.title, - it.video - ) - } - } - - override fun getImages(locale: String, type: String): List { - return publisher.getImages(editId, locale, type).map { - GppImage(it.url + HIGH_RES_IMAGE_REQUEST, it.sha256) - } - } - - override fun findMaxAppVersionCode(): Long { - return tracks.findHighestTrack()?.releases.orEmpty() - .flatMap { it.versionCodes.orEmpty() } - .maxOrNull() ?: 1 - } - - override fun findLeastStableTrackName(): String? { - return tracks.findHighestTrack()?.track - } - - override fun getReleaseNotes(): List { - return tracks.getReleaseNotes().map { (track, notes) -> - notes.map { ReleaseNote(track, it.language, it.text) } - }.flatten() - } - - override fun publishAppDetails( - defaultLocale: String?, - contactEmail: String?, - contactPhone: String?, - contactWebsite: String?, - ) { - publisher.updateDetails(editId, AppDetails().apply { - this.defaultLanguage = defaultLocale - this.contactEmail = contactEmail - this.contactPhone = contactPhone - this.contactWebsite = contactWebsite - }) - } - - override fun publishListing( - locale: String, - title: String?, - shortDescription: String?, - fullDescription: String?, - video: String?, - ) { - publisher.updateListing(editId, locale, Listing().apply { - this.title = title - this.shortDescription = shortDescription - this.fullDescription = fullDescription - this.video = video - }) - } - - override fun publishImages(locale: String, type: String, images: List) { - publisher.deleteImages(editId, locale, type) - for (image in images) { - println("Uploading $locale listing graphic for type '$type': ${image.name}") - // These can't be uploaded in parallel because order matters - publisher.uploadImage(editId, locale, type, image) - } - } - override fun promoteRelease( promoteTrackName: String, fromTrackName: String, @@ -112,31 +24,34 @@ internal class DefaultEditManager( retainableArtifacts: List?, versionCode: Long?, ) { - tracks.promote(TrackManager.PromoteConfig( - promoteTrackName, - fromTrackName, - versionCode, - TrackManager.BaseConfig( - releaseStatus, - userFraction, - updatePriority, - releaseNotes, - retainableArtifacts, - releaseName - ) - )) + tracks.promote( + TrackManager.PromoteConfig( + promoteTrackName, + fromTrackName, + versionCode, + TrackManager.BaseConfig( + releaseStatus, + userFraction, + updatePriority, + releaseNotes, + retainableArtifacts, + releaseName, + ), + ), + ) } override fun uploadBundle( bundleFile: File, strategy: ResolutionStrategy, ): Long? { - val bundle = try { - publisher.uploadBundle(editId, bundleFile) - } catch (e: GoogleJsonResponseException) { - handleUploadFailures(e, strategy, bundleFile) - return null - } + val bundle = + try { + publisher.uploadBundle(editId, bundleFile) + } catch (e: GoogleJsonResponseException) { + handleUploadFailures(e, strategy, bundleFile) + return null + } return bundle.versionCode.toLong() } @@ -154,68 +69,62 @@ internal class DefaultEditManager( ) { if (versionCodes.isEmpty()) return - tracks.update(TrackManager.UpdateConfig( - trackName, - versionCodes, - didPreviousBuildSkipCommit, - TrackManager.BaseConfig( - releaseStatus, - userFraction, - updatePriority, - releaseNotes, - retainableArtifacts, - releaseName - ) - )) - } - - private fun uploadMappingFile(versionCode: Int, mappingFile: File?) { - if (mappingFile != null && mappingFile.length() > 0) { - publisher.uploadDeobfuscationFile(editId, mappingFile, versionCode, "proguard") - } - } - - private fun Int.attachObb(type: String, versionCode: Int) { - println("Attaching $type OBB ($this) to APK $versionCode") - publisher.attachObb(editId, type, versionCode, this) + tracks.update( + TrackManager.UpdateConfig( + trackName, + versionCodes, + didPreviousBuildSkipCommit, + TrackManager.BaseConfig( + releaseStatus, + userFraction, + updatePriority, + releaseNotes, + retainableArtifacts, + releaseName, + ), + ), + ) } + @Suppress("ComplexCondition") // API response code handling private fun handleUploadFailures( e: GoogleJsonResponseException, strategy: ResolutionStrategy, artifact: File, - ): Nothing? = if ( - e has "apkNotificationMessageKeyUpgradeVersionConflict" || - e has "apkUpgradeVersionConflict" || - e has "apkNoUpgradePath" || - e has "forbidden" && e.details.message.orEmpty().let { m -> - // Bundle message: APK specifies a version code that has already been used. - // APK message: Cannot update a published APK. - m.contains("version code", ignoreCase = true) || m.contains("Cannot update", ignoreCase = true) + ): Nothing? = + if ( + e has "apkNotificationMessageKeyUpgradeVersionConflict" || + e has "apkUpgradeVersionConflict" || + e has "apkNoUpgradePath" || + e has "forbidden" && + e.details.message.orEmpty().let { m -> + // Bundle message: APK specifies a version code that has already been used. + // APK message: Cannot update a published APK. + m.contains("version code", ignoreCase = true) || m.contains("Cannot update", ignoreCase = true) + } + ) { + when (strategy) { + ResolutionStrategy.AUTO, ResolutionStrategy.AUTO_OFFSET -> throw IllegalStateException( + "Concurrent uploads for app ${publisher.appId} (version code " + + "already used). Make sure to synchronously upload your APKs such " + + "that they don't conflict. If this problem persists, delete your " + + "drafts in the Play Console's artifact library.", + e, + ) + + ResolutionStrategy.FAIL -> throw IllegalStateException( + "Version code is too low or has already been used for app " + + "${publisher.appId}.", + e, + ) + + ResolutionStrategy.IGNORE -> + LoggerFactory.getLogger(EditManager::class.java).warn( + "Ignoring artifact ($artifact)", + ) + } + null + } else { + throw e } - ) { - when (strategy) { - ResolutionStrategy.AUTO, ResolutionStrategy.AUTO_OFFSET -> throw IllegalStateException( - "Concurrent uploads for app ${publisher.appId} (version code " + - "already used). Make sure to synchronously upload your APKs such " + - "that they don't conflict. If this problem persists, delete your " + - "drafts in the Play Console's artifact library.", - e - ) - - ResolutionStrategy.FAIL -> throw IllegalStateException( - "Version code is too low or has already been used for app " + - "${publisher.appId}.", - e - ) - - ResolutionStrategy.IGNORE -> LoggerFactory.getLogger(EditManager::class.java).warn( - "Ignoring artifact ($artifact)") - } - null - } else { - throw e - } } - -private const val HIGH_RES_IMAGE_REQUEST = "=h16383" // Max res: 2^14 - 1 diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt index 5ba181d..bf4476a 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/EditManager.kt @@ -1,48 +1,20 @@ package ru.kode.android.build.publish.plugin.task.play.track -import ru.kode.android.build.publish.plugin.task.play.PlayPublisher -import ru.kode.android.build.publish.plugin.task.play.publisher.GppAppDetails -import ru.kode.android.build.publish.plugin.task.play.publisher.GppImage -import ru.kode.android.build.publish.plugin.task.play.publisher.GppListing -import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseNote import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseStatus import ru.kode.android.build.publish.plugin.task.play.publisher.ResolutionStrategy import java.io.File -import java.util.ServiceLoader interface EditManager { - fun getAppDetails(): GppAppDetails - fun getListings(): List - fun getImages(locale: String, type: String): List - fun findMaxAppVersionCode(): Long - fun findLeastStableTrackName(): String? - fun getReleaseNotes(): List - fun publishAppDetails( - defaultLocale: String?, - contactEmail: String?, - contactPhone: String?, - contactWebsite: String?, - ) - - fun publishListing( - locale: String, - title: String?, - shortDescription: String?, - fullDescription: String?, - video: String?, - ) - - fun publishImages(locale: String, type: String, images: List) fun promoteRelease( promoteTrackName: String, fromTrackName: String, releaseStatus: ReleaseStatus?, releaseName: String?, - releaseNotes: Map?, + releaseNotes: Map?, userFraction: Double?, updatePriority: Int?, retainableArtifacts: List?, - versionCode: Long? + versionCode: Long?, ) fun uploadBundle( @@ -56,21 +28,9 @@ interface EditManager { trackName: String, releaseStatus: ReleaseStatus?, releaseName: String?, - releaseNotes: Map?, + releaseNotes: Map?, userFraction: Double?, updatePriority: Int?, retainableArtifacts: List?, ) - - interface Factory { - fun create(publisher: PlayPublisher, editId: String): EditManager - } - - companion object { - operator fun invoke( - publisher: PlayPublisher, - editId: String, - ): EditManager = ServiceLoader.load(Factory::class.java).last() - .create(publisher, editId) - } } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt index fa614da..2384aef 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/track/TrackManager.kt @@ -7,10 +7,6 @@ import ru.kode.android.build.publish.plugin.task.play.publisher.InternalPlayPubl import ru.kode.android.build.publish.plugin.task.play.publisher.ReleaseStatus internal interface TrackManager { - fun findHighestTrack(): Track? - - fun getReleaseNotes(): Map> - fun update(config: UpdateConfig) fun promote(config: PromoteConfig) @@ -43,35 +39,15 @@ internal class DefaultTrackManager( private val publisher: InternalPlayPublisher, private val editId: String, ) : TrackManager { - override fun findHighestTrack(): Track? { - return publisher.listTracks(editId).maxByOrNull { - it.releases.orEmpty().flatMap { it.versionCodes.orEmpty() }.maxOrNull() ?: 0 - } - } - - override fun getReleaseNotes(): Map> { - val releaseNotes = mutableMapOf>() - - val tracks = publisher.listTracks(editId) - for (track in tracks) { - val notes = track.releases?.maxByOrNull { - it.versionCodes?.maxOrNull() ?: Long.MIN_VALUE - }?.releaseNotes.orEmpty() - - releaseNotes[track.track] = notes - } - - return releaseNotes - } - override fun update(config: TrackManager.UpdateConfig) { - val track = if (config.didPreviousBuildSkipCommit) { - createTrackForSkippedCommit(config) - } else if (config.base.releaseStatus.orDefault().isRollout()) { - createTrackForRollout(config) - } else { - createDefaultTrack(config) - } + val track = + if (config.didPreviousBuildSkipCommit) { + createTrackForSkippedCommit(config) + } else if (config.base.releaseStatus.orDefault().isRollout()) { + createTrackForRollout(config) + } else { + createDefaultTrack(config) + } publisher.updateTrack(editId, track) } @@ -89,38 +65,44 @@ internal class DefaultTrackManager( // Only keep the unique statuses from the highest version code since duplicate statuses are // not allowed. This is how we deal with an update from inProgress -> completed. We update // all the tracks to completed, then get rid of the one that used to be inProgress. - track.releases = track.releases.sortedByDescending { - it.versionCodes?.maxOrNull() - }.distinctBy { - it.status - } + track.releases = + track.releases.sortedByDescending { + it.versionCodes?.maxOrNull() + }.distinctBy { + it.status + } println("Promoting release from track '${track.track}'") track.track = config.promoteTrackName publisher.updateTrack(editId, track) } + @Suppress("NestedBlockDepth") // TODO refactor and simplify TrackManager private fun createTrackForSkippedCommit(config: TrackManager.UpdateConfig): Track { val track = publisher.getTrack(editId, config.trackName) if (track.releases.isNullOrEmpty()) { track.releases = listOf(TrackRelease().mergeChanges(config.versionCodes, config.base)) } else { - val hasReleaseToBeUpdated = track.releases.firstOrNull { - it.status == config.base.releaseStatus.orDefault().publishedName - } != null + val hasReleaseToBeUpdated = + track.releases.firstOrNull { + it.status == config.base.releaseStatus.orDefault().publishedName + } != null if (hasReleaseToBeUpdated) { for (release in track.releases) { if (release.status == config.base.releaseStatus.orDefault().publishedName) { release.mergeChanges( - release.versionCodes.orEmpty() + config.versionCodes, config.base) + release.versionCodes.orEmpty() + config.versionCodes, + config.base, + ) } } } else { - val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply { - maybeCopyChangelogFromPreviousRelease(config.trackName) - } + val release = + TrackRelease().mergeChanges(config.versionCodes, config.base).apply { + maybeCopyChangelogFromPreviousRelease(config.trackName) + } track.releases = track.releases + release } } @@ -132,27 +114,32 @@ internal class DefaultTrackManager( val track = publisher.getTrack(editId, config.trackName) val keep = track.releases.orEmpty().filterNot { it.isRollout() } - val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply { - maybeCopyChangelogFromPreviousRelease(config.trackName) - } + val release = + TrackRelease().mergeChanges(config.versionCodes, config.base).apply { + maybeCopyChangelogFromPreviousRelease(config.trackName) + } track.releases = keep + release return track } - private fun createDefaultTrack(config: TrackManager.UpdateConfig) = Track().apply { - track = config.trackName - val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply { - maybeCopyChangelogFromPreviousRelease(config.trackName) + private fun createDefaultTrack(config: TrackManager.UpdateConfig) = + Track().apply { + track = config.trackName + val release = + TrackRelease() + .mergeChanges(config.versionCodes, config.base) + .apply { maybeCopyChangelogFromPreviousRelease(config.trackName) } + releases = listOf(release) } - releases = listOf(release) - } private fun TrackRelease.maybeCopyChangelogFromPreviousRelease(trackName: String) { if (!releaseNotes.isNullOrEmpty()) return - val previousRelease = publisher.getTrack(editId, trackName) - .releases.orEmpty() + val previousRelease = + publisher.getTrack(editId, trackName) + .releases + .orEmpty() .maxByOrNull { it.versionCodes.orEmpty().maxOrNull() ?: 1 } releaseNotes = previousRelease?.releaseNotes } @@ -169,12 +156,18 @@ internal class DefaultTrackManager( updateUpdatePriority(config.updatePriority) } - private fun TrackRelease.updateVersionCodes(versionCodes: List?, retainableArtifacts: List?) { + private fun TrackRelease.updateVersionCodes( + versionCodes: List?, + retainableArtifacts: List?, + ) { val newVersions = versionCodes ?: this.versionCodes.orEmpty() this.versionCodes = newVersions + retainableArtifacts.orEmpty() } - private fun TrackRelease.updateStatus(releaseStatus: ReleaseStatus?, hasUserFraction: Boolean) { + private fun TrackRelease.updateStatus( + releaseStatus: ReleaseStatus?, + hasUserFraction: Boolean, + ) { if (releaseStatus != null) { status = releaseStatus.publishedName } else if (hasUserFraction) { @@ -189,25 +182,27 @@ internal class DefaultTrackManager( } private fun TrackRelease.updateReleaseNotes(rawReleaseNotes: Map?) { - val releaseNotes = rawReleaseNotes.orEmpty().map { (locale, notes) -> - LocalizedText().apply { - language = locale - text = notes + val releaseNotes = + rawReleaseNotes.orEmpty().map { (locale, notes) -> + LocalizedText().apply { + language = locale + text = notes + } } - } val existingReleaseNotes = this.releaseNotes.orEmpty() - this.releaseNotes = if (existingReleaseNotes.isEmpty()) { - releaseNotes - } else { - val merged = releaseNotes.toMutableList() + this.releaseNotes = + if (existingReleaseNotes.isEmpty()) { + releaseNotes + } else { + val merged = releaseNotes.toMutableList() - for (existing in existingReleaseNotes) { - if (merged.none { it.language == existing.language }) merged += existing - } + for (existing in existingReleaseNotes) { + if (merged.none { it.language == existing.language }) merged += existing + } - merged - } + merged + } } private fun TrackRelease.updateUserFraction(userFraction: Double?) { @@ -226,12 +221,11 @@ internal class DefaultTrackManager( } } - private fun ReleaseStatus.isRollout() = - this == ReleaseStatus.IN_PROGRESS || this == ReleaseStatus.HALTED + private fun ReleaseStatus.isRollout() = this == ReleaseStatus.IN_PROGRESS || this == ReleaseStatus.HALTED private fun TrackRelease.isRollout() = - status == ReleaseStatus.IN_PROGRESS.publishedName || - status == ReleaseStatus.HALTED.publishedName + status == ReleaseStatus.IN_PROGRESS.publishedName || + status == ReleaseStatus.HALTED.publishedName private fun ReleaseStatus?.orDefault() = this ?: DEFAULT_RELEASE_STATUS diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt index 30dfd84..f2a2bae 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/play/work/PlayUploadWork.kt @@ -3,7 +3,6 @@ package ru.kode.android.build.publish.plugin.task.play.work import org.gradle.api.file.RegularFileProperty import org.gradle.api.logging.Logging import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty import org.gradle.workers.WorkAction import org.gradle.workers.WorkParameters import ru.kode.android.build.publish.plugin.task.play.publisher.DefaultPlayPublisher @@ -28,24 +27,32 @@ abstract class PlayUploadWork : WorkAction { private val logger = Logging.getLogger(this::class.java) override fun execute() { - val track = parameters.trackId.get() val priority = parameters.updatePriority.orNull ?: 0 val releaseName = parameters.releaseName.get() val file = parameters.outputFile.asFile.get() - val publisher = DefaultPlayPublisher( - publisher = createPublisher(parameters.apiToken.asFile.get().inputStream()), - appId = parameters.appId.get() - ) + val publisher = + DefaultPlayPublisher( + publisher = createPublisher(parameters.apiToken.asFile.get().inputStream()), + appId = parameters.appId.get(), + ) logger.info("Step 1/3: Requesting track edit...") - val editId = when (val result = publisher.insertEdit()) { - is EditResponse.Success -> result.id - is EditResponse.Failure -> null - } + val editId = + when (val result = publisher.insertEdit()) { + is EditResponse.Success -> result.id + is EditResponse.Failure -> { + if (result.isNewApp()) { + logger.error("Error response: app does not exist") + } else { + logger.error("Error response: $result") + } + null + } + } if (editId == null) { - logger.error("Failed to fetch edit id to upload bundle. Check your credentials") + logger.error("Failed to fetch edit id to upload bundle") return } @@ -57,25 +64,25 @@ abstract class PlayUploadWork : WorkAction { val versionCode = editManager.uploadBundle(file, ResolutionStrategy.IGNORE) if (versionCode == null) { - logger.error("Failed to upload bundle. Check your credentials") + logger.error("Failed to upload bundle") return } logger.info("Step 3/3: Pushing $releaseName to $track at P=$priority V=$versionCode") trackManager.update( - config = TrackManager.UpdateConfig( - trackName = track, - versionCodes = listOf(versionCode), - didPreviousBuildSkipCommit = false, - TrackManager.BaseConfig( - userFraction = parameters.versionCode.orNull ?: 0.1, - updatePriority = priority, - releaseName = parameters.releaseName.get() - ) - ) + config = + TrackManager.UpdateConfig( + trackName = track, + versionCodes = listOf(versionCode), + didPreviousBuildSkipCommit = false, + TrackManager.BaseConfig( + userFraction = parameters.versionCode.orNull ?: 0.1, + updatePriority = priority, + releaseName = parameters.releaseName.get(), + ), + ), ) logger.info("Step 3/3: Bundle upload successful") } - }