Skip to content

Commit 6228e3a

Browse files
authored
AAB Publisher - Gradle plugin for publishing app bundle to Google Play (#469)
1 parent d19d31c commit 6228e3a

File tree

13 files changed

+245
-29
lines changed

13 files changed

+245
-29
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
A Gradle plugin for publishing the Android App Bundle for a single build variant to Google Play (Internal Testing track).
2+
3+
It uses [gradle-play-publisher](https://github.com/Triple-T/gradle-play-publisher)'s `android-publisher` library internally to interact with the [Android Publisher API](https://developers.google.com/android-publisher).
4+
5+
**The plugin is fully compatible with AGP 9's new DSL.**
6+
7+
## Usage
8+
9+
Apply the plugin in your app module's `build.gradle.kts` and configure the variant to publish and the service account credentials:
10+
11+
```kotlin
12+
plugins {
13+
id("io.github.reactivecircus.aab-publisher")
14+
}
15+
16+
aabPublisher {
17+
// The build variant to publish
18+
variant.set("prodRelease")
19+
// Path to the Google Play Service Account JSON credentials file
20+
serviceAccountCredentials.set(file("play-services-account.json"))
21+
}
22+
```
23+
24+
To publish the bundle to Google Play:
25+
26+
```bash
27+
./gradlew publishBundleToGooglePlay
28+
```
29+
30+
The task will upload the Android App Bundle (AAB) file for the configured build variant to Google Play's Internal Testing track.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import dev.detekt.gradle.Detekt
2+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3+
4+
plugins {
5+
`java-gradle-plugin`
6+
alias(libs.plugins.kotlin.jvm)
7+
alias(libs.plugins.lint)
8+
alias(libs.plugins.detekt)
9+
}
10+
11+
gradlePlugin {
12+
plugins {
13+
register("aabPublisher") {
14+
id = "io.github.reactivecircus.aab-publisher"
15+
implementationClass = "io.github.reactivecircus.aabpublisher.AabPublisherGradlePlugin"
16+
}
17+
}
18+
}
19+
20+
kotlin {
21+
compilerOptions {
22+
jvmTarget = JvmTarget.JVM_17
23+
}
24+
explicitApi()
25+
}
26+
27+
java {
28+
sourceCompatibility = JavaVersion.VERSION_17
29+
targetCompatibility = JavaVersion.VERSION_17
30+
}
31+
32+
detekt {
33+
source.setFrom(file("src/"))
34+
config.setFrom(file("$rootDir/../detekt.yml"))
35+
buildUponDefaultConfig = true
36+
parallel = true
37+
}
38+
39+
tasks.withType<Detekt>().configureEach {
40+
jvmTarget = JvmTarget.JVM_17.target
41+
reports {
42+
checkstyle.required.set(false)
43+
sarif.required.set(false)
44+
markdown.required.set(false)
45+
}
46+
}
47+
48+
dependencies {
49+
// enable Ktlint formatting
50+
detektPlugins(libs.plugin.detektKtlintWrapper)
51+
52+
compileOnly(libs.plugin.agp)
53+
implementation(libs.androidPublisher)
54+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.github.reactivecircus.aabpublisher
2+
3+
import org.gradle.api.file.RegularFileProperty
4+
import org.gradle.api.provider.Property
5+
6+
public abstract class AabPublisherExtension {
7+
public abstract val variant: Property<String>
8+
public abstract val serviceAccountCredentials: RegularFileProperty
9+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.github.reactivecircus.aabpublisher
2+
3+
import com.android.build.api.artifact.SingleArtifact
4+
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
5+
import org.gradle.api.Plugin
6+
import org.gradle.api.Project
7+
8+
public class AabPublisherGradlePlugin : Plugin<Project> {
9+
override fun apply(target: Project): Unit = with(target) {
10+
val aabPublisherExtension = target.extensions.create("aabPublisher", AabPublisherExtension::class.java)
11+
var androidAppPluginApplied = false
12+
pluginManager.withPlugin("com.android.application") {
13+
androidAppPluginApplied = true
14+
extensions.configure(ApplicationAndroidComponentsExtension::class.java) { extension ->
15+
extension.onVariants { variant ->
16+
if (!variant.name.equals(aabPublisherExtension.variant.get(), ignoreCase = true)) return@onVariants
17+
tasks.register(
18+
"publishBundleToGooglePlay",
19+
PublishBundleToGooglePlay::class.java,
20+
) {
21+
it.group = "AAB Publisher"
22+
it.description = getTaskDescription(variant.name)
23+
it.bundle.set(variant.artifacts.get(SingleArtifact.BUNDLE))
24+
it.serviceAccountCredentials.set(aabPublisherExtension.serviceAccountCredentials)
25+
it.applicationId.set(variant.applicationId)
26+
}
27+
}
28+
}
29+
}
30+
afterEvaluate {
31+
check(androidAppPluginApplied) {
32+
"AAB Publisher requires the `com.android.application` plugin to be applied to the same project," +
33+
" it's missing in $displayName."
34+
}
35+
}
36+
}
37+
38+
private fun getTaskDescription(variantName: String): String =
39+
"Publishes the Android App Bundle to Google Play (internal testing) for the $variantName variant."
40+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package io.github.reactivecircus.aabpublisher
2+
3+
import com.github.triplet.gradle.androidpublisher.CommitResponse
4+
import com.github.triplet.gradle.androidpublisher.EditManager
5+
import com.github.triplet.gradle.androidpublisher.EditResponse
6+
import com.github.triplet.gradle.androidpublisher.PlayPublisher
7+
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
8+
import com.github.triplet.gradle.androidpublisher.ResolutionStrategy
9+
import org.gradle.api.DefaultTask
10+
import org.gradle.api.file.RegularFileProperty
11+
import org.gradle.api.provider.Property
12+
import org.gradle.api.tasks.Input
13+
import org.gradle.api.tasks.InputFile
14+
import org.gradle.api.tasks.PathSensitive
15+
import org.gradle.api.tasks.PathSensitivity
16+
import org.gradle.api.tasks.TaskAction
17+
import org.gradle.work.DisableCachingByDefault
18+
import org.gradle.work.NormalizeLineEndings
19+
20+
@DisableCachingByDefault
21+
internal abstract class PublishBundleToGooglePlay : DefaultTask() {
22+
@get:InputFile
23+
@get:PathSensitive(PathSensitivity.RELATIVE)
24+
@get:NormalizeLineEndings
25+
abstract val bundle: RegularFileProperty
26+
27+
@get:InputFile
28+
@get:PathSensitive(PathSensitivity.RELATIVE)
29+
@get:NormalizeLineEndings
30+
abstract val serviceAccountCredentials: RegularFileProperty
31+
32+
@get:Input
33+
abstract val applicationId: Property<String>
34+
35+
@TaskAction
36+
fun execute() {
37+
val bundleFile = bundle.asFile.get()
38+
val credentialsFile = serviceAccountCredentials.asFile.get()
39+
val appId = applicationId.get()
40+
41+
logger.lifecycle("Publishing {} to Google Play (internal testing).", bundleFile.name)
42+
43+
val publisher = credentialsFile.inputStream().use { stream ->
44+
PlayPublisher(stream, appId)
45+
}
46+
47+
val editId = when (val response = publisher.insertEdit()) {
48+
is EditResponse.Success -> response.id
49+
is EditResponse.Failure -> response.rethrow()
50+
}
51+
52+
val editManager = EditManager(publisher, editId)
53+
val versionCode = editManager.uploadBundle(bundleFile, ResolutionStrategy.FAIL)
54+
if (versionCode != null) {
55+
logger.lifecycle("Uploaded bundle with version code: {}", versionCode)
56+
57+
// publish to internal testing track and makes it active
58+
editManager.publishArtifacts(
59+
versionCodes = listOf(versionCode),
60+
didPreviousBuildSkipCommit = false,
61+
trackName = "internal",
62+
releaseStatus = ReleaseStatus.COMPLETED,
63+
releaseName = null,
64+
releaseNotes = null,
65+
userFraction = null,
66+
updatePriority = null,
67+
retainableArtifacts = null,
68+
)
69+
}
70+
71+
when (val response = publisher.commitEdit(editId)) {
72+
is CommitResponse.Success -> {
73+
logger.lifecycle("Successfully published app bundle to Google Play (internal testing).")
74+
}
75+
76+
is CommitResponse.Failure -> {
77+
if (response.failedToSendForReview()) {
78+
val retryResponse = publisher.commitEdit(editId, sendChangesForReview = false)
79+
if (retryResponse is CommitResponse.Failure) {
80+
retryResponse.rethrow(response)
81+
}
82+
logger.lifecycle(
83+
"Successfully published bundle to Google Play (internal testing)." +
84+
" Changes were not sent for review.",
85+
)
86+
} else {
87+
response.rethrow()
88+
}
89+
}
90+
}
91+
}
92+
}

build-logic/kstreamlined-build-plugin/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ dependencies {
4949
// TODO: remove once https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 is fixed
5050
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
5151

52+
implementation(project(":aab-publisher"))
5253
implementation(project(":chameleon:chameleon-gradle-plugin"))
5354
implementation(project(":licentia:licentia-gradle-plugin"))
5455
implementation(project(":v2p"))
@@ -77,7 +78,6 @@ dependencies {
7778
implementation(libs.plugin.firebasePerf)
7879
implementation(libs.plugin.crashlytics)
7980
implementation(libs.plugin.appDistribution)
80-
implementation(libs.plugin.playPublisher)
8181
implementation(libs.plugin.skie)
8282
implementation(libs.plugin.sqldelight)
8383
implementation(libs.plugin.baselineprofile)

build-logic/kstreamlined-build-plugin/src/main/kotlin/io/github/reactivecircus/kstreamlined/gradle/AndroidAppExtension.kt

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import com.android.build.api.variant.ApplicationAndroidComponentsExtension
1111
import com.android.build.api.variant.BuildConfigField
1212
import com.android.build.api.variant.ResValue
1313
import com.android.build.api.variant.Variant
14-
import com.github.triplet.gradle.play.PlayPublisherExtension
1514
import com.google.firebase.appdistribution.gradle.AppDistributionExtension
1615
import com.google.firebase.appdistribution.gradle.tasks.UploadDistributionTask
1716
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
1817
import com.google.firebase.perf.plugin.FirebasePerfExtension
1918
import com.google.gms.googleservices.GoogleServicesPlugin
19+
import io.github.reactivecircus.aabpublisher.AabPublisherExtension
2020
import io.github.reactivecircus.appversioning.AppVersioningExtension
2121
import io.github.reactivecircus.kstreamlined.gradle.internal.configureAndroidApplicationExtension
2222
import io.github.reactivecircus.kstreamlined.gradle.internal.configureAndroidApplicationVariants
@@ -30,7 +30,6 @@ import io.github.reactivecircus.kstreamlined.gradle.internal.configureTest
3030
import io.github.reactivecircus.kstreamlined.gradle.internal.libs
3131
import isCiBuild
3232
import org.gradle.api.Action
33-
import org.gradle.api.NamedDomainObjectContainer
3433
import org.gradle.api.Project
3534
import org.gradle.api.model.ObjectFactory
3635
import org.gradle.api.plugins.BasePluginExtension
@@ -370,7 +369,7 @@ internal abstract class AndroidAppExtensionImpl @Inject constructor(
370369

371370
versioningConfig?.let(::configureAppVersioning)
372371

373-
playPublishingServiceAccountCredentials?.let(::configurePlayPublishing)
372+
playPublishingServiceAccountCredentials?.let(::configureAabPublisher)
374373

375374
if (googleServicesEnabled) {
376375
configureGoogleServices()
@@ -533,20 +532,11 @@ internal abstract class AndroidAppExtensionImpl @Inject constructor(
533532
}
534533
}
535534

536-
private fun configurePlayPublishing(serviceAccountCredentials: File) = with(project) {
537-
pluginManager.apply("com.github.triplet.play")
538-
extensions.configure(PlayPublisherExtension::class.java) {
539-
it.enabled.set(false) // only enable for prodRelease variant
535+
private fun configureAabPublisher(serviceAccountCredentials: File) = with(project) {
536+
pluginManager.apply("io.github.reactivecircus.aab-publisher")
537+
extensions.configure(AabPublisherExtension::class.java) {
538+
it.variant.set("prodRelease") // publish prodRelease variant
540539
it.serviceAccountCredentials.set(serviceAccountCredentials)
541-
it.defaultToAppBundles.set(true)
542-
}
543-
extensions.configure(ApplicationExtension::class.java) { extension ->
544-
(extension as ExtensionAware).extensions
545-
.configure<NamedDomainObjectContainer<PlayPublisherExtension>>("playConfigs") { container ->
546-
container.register(ProductFlavors.Prod) {
547-
it.enabled.set(true)
548-
}
549-
}
550540
}
551541
}
552542

build-logic/licentia/licentia-gradle-plugin/src/main/kotlin/io/github/reactivecircus/licentia/gradle/GenerateLicensesInfoSource.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ internal abstract class GenerateLicensesInfoSource : DefaultTask() {
2626
@get:OutputDirectory
2727
abstract val outputDir: DirectoryProperty
2828

29-
init {
30-
group = "Licentia"
31-
description = "Generates `LicencesInfo` implementation from Licensee plugin's Json report."
32-
}
33-
3429
@TaskAction
3530
fun execute() {
3631
LicensesInfoGenerator.buildFileSpec(

build-logic/licentia/licentia-gradle-plugin/src/main/kotlin/io/github/reactivecircus/licentia/gradle/LicentiaGradlePlugin.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public class LicentiaGradlePlugin : Plugin<Project> {
2525
"generateLicensesInfoSource${variant.name.capitalized()}",
2626
GenerateLicensesInfoSource::class.java,
2727
) { task ->
28+
task.group = "Licentia"
29+
task.description = getTaskDescription(variant.name)
2830
task.packageName.set(variant.namespace.map { "$it.licentia" })
2931
task.artifactsJsonFile.set(
3032
tasks.withType(LicenseeTask::class.java)
@@ -59,4 +61,7 @@ public class LicentiaGradlePlugin : Plugin<Project> {
5961
}
6062
}
6163
}
64+
65+
private fun getTaskDescription(variantName: String): String =
66+
"Generates `LicencesInfo` implementation from Licensee plugin's Json report for the $variantName variant."
6267
}

build-logic/settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ plugins {
5353

5454
rootProject.name = "build-logic"
5555
include(":kstreamlined-build-plugin")
56+
include(":aab-publisher")
5657
include(":chameleon:chameleon-compiler-plugin")
5758
include(":chameleon:chameleon-gradle-plugin")
5859
include(":chameleon:chameleon-runtime")

0 commit comments

Comments
 (0)