Skip to content

Commit 40198e4

Browse files
runningcodeclaude
andauthored
feat(distribution): Add install-groups support (#1070)
* feat(distribution): Add install-groups support Add support for install groups in the distribution extension to control update visibility between builds. Install groups allow separation of distribution channels (e.g., "internal", "beta", "production") so that only builds with matching install groups can see updates for each other. Changes: - Add installGroups property to DistributionExtension - Wire installGroups through SentryUploadAppArtifactTask to generate --install-group CLI arguments for sentry-cli - Add installGroups to GenerateDistributionPropertiesTask to write io.sentry.distribution.install-groups-override property - Add comprehensive tests for all new functionality The feature is forward-compatible with sentry-cli 3.1.0 (ignores unknown flags) and will automatically activate when sentry-cli is upgraded to 3.2.0+. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: Update CHANGELOG for install-groups feature Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: Move install-groups entry to Unreleased section Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: Simplify code and remove unnecessary comments - Simplify installGroups documentation in DistributionExtension - Remove redundant inline comments from implementation - Remove verbose test comments Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent bd235c5 commit 40198e4

8 files changed

Lines changed: 144 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add install groups support for Build Distribution to control update visibility between distribution channels ([#1070](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1070))
8+
59
### Dependencies
610

711
- Bump CLI from v3.1.0 to v3.2.0 ([#1068](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1068))

plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ fun Variant.configureUploadAppTasks(
481481
sentryAuthToken = extension.authToken,
482482
sentryUrl = extension.url,
483483
sentryProperties = sentryProps,
484+
installGroups = extension.distribution.installGroups,
484485
taskSuffix = name.capitalized,
485486
buildVariant = buildVariant,
486487
)

plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/DistributionExtension.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,13 @@ constructor(objects: ObjectFactory, providerFactory: ProviderFactory) {
3737
/** Auth token used for distribution operations. */
3838
val authToken: Property<String> =
3939
objects.property(String::class.java).convention(System.getenv("SENTRY_DISTRIBUTION_AUTH_TOKEN"))
40+
41+
/**
42+
* Install groups control which builds can see updates for each other.
43+
*
44+
* Only builds with matching install groups will be offered updates. This is useful for managing
45+
* separate distribution channels (e.g., "internal", "beta", "production").
46+
*/
47+
val installGroups: SetProperty<String> =
48+
objects.setProperty(String::class.java).convention(emptySet())
4049
}

plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/GenerateDistributionPropertiesTask.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.gradle.api.file.RegularFile
1313
import org.gradle.api.file.RegularFileProperty
1414
import org.gradle.api.provider.Property
1515
import org.gradle.api.provider.Provider
16+
import org.gradle.api.provider.SetProperty
1617
import org.gradle.api.tasks.CacheableTask
1718
import org.gradle.api.tasks.Input
1819
import org.gradle.api.tasks.InputFile
@@ -47,6 +48,8 @@ abstract class GenerateDistributionPropertiesTask : PropertiesFileOutputTask() {
4748

4849
@get:Input abstract val buildConfiguration: Property<String>
4950

51+
@get:Input @get:Optional abstract val installGroups: SetProperty<String>
52+
5053
@TaskAction
5154
fun generateProperties() {
5255
outputFile.get().asFile.writer().use { writer ->
@@ -56,6 +59,9 @@ abstract class GenerateDistributionPropertiesTask : PropertiesFileOutputTask() {
5659
writer.appendLine("$DISTRIBUTION_AUTH_TOKEN_PROPERTY=$it")
5760
}
5861
writer.appendLine("$BUILD_CONFIGURATION_PROPERTY=${buildConfiguration.get()}")
62+
installGroups.orNull
63+
?.takeIf { it.isNotEmpty() }
64+
?.let { writer.appendLine("$INSTALL_GROUPS_PROPERTY=${it.joinToString(",")}") }
5965
}
6066
}
6167

@@ -65,6 +71,7 @@ abstract class GenerateDistributionPropertiesTask : PropertiesFileOutputTask() {
6571
const val PROJECT_SLUG_PROPERTY = "io.sentry.distribution.project-slug"
6672
const val DISTRIBUTION_AUTH_TOKEN_PROPERTY = "io.sentry.distribution.auth-token"
6773
const val BUILD_CONFIGURATION_PROPERTY = "io.sentry.distribution.build-configuration"
74+
const val INSTALL_GROUPS_PROPERTY = "io.sentry.distribution.install-groups-override"
6875

6976
fun register(
7077
project: Project,
@@ -122,6 +129,8 @@ abstract class GenerateDistributionPropertiesTask : PropertiesFileOutputTask() {
122129
task.distributionAuthToken.set(extension.distribution.authToken)
123130

124131
task.buildConfiguration.set(buildConfiguration)
132+
133+
task.installGroups.set(extension.distribution.installGroups)
125134
}
126135
}
127136
}

plugin-build/src/main/kotlin/io/sentry/android/gradle/tasks/SentryUploadAppArtifactTask.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import org.gradle.api.file.RegularFile
1212
import org.gradle.api.model.ObjectFactory
1313
import org.gradle.api.provider.Property
1414
import org.gradle.api.provider.Provider
15+
import org.gradle.api.provider.SetProperty
1516
import org.gradle.api.tasks.Input
1617
import org.gradle.api.tasks.InputDirectory
1718
import org.gradle.api.tasks.InputFile
@@ -44,6 +45,7 @@ abstract class SentryUploadAppArtifactTask @Inject constructor(objectFactory: Ob
4445
@get:Input @get:Optional abstract val vcsBaseRef: Property<String>
4546
@get:Input @get:Optional abstract val vcsPrNumber: Property<Int>
4647
@get:Input @get:Optional abstract val buildConfiguration: Property<String>
48+
@get:Input @get:Optional abstract val installGroups: SetProperty<String>
4749

4850
override fun getArguments(args: MutableList<String>) {
4951
args.add("build")
@@ -59,6 +61,9 @@ abstract class SentryUploadAppArtifactTask @Inject constructor(objectFactory: Ob
5961
vcsBaseRef.orNull?.let { args.addAll(listOf("--base-ref", it)) }
6062
vcsPrNumber.orNull?.let { args.addAll(listOf("--pr-number", it.toString())) }
6163
buildConfiguration.orNull?.let { args.addAll(listOf("--build-configuration", it)) }
64+
installGroups.orNull
65+
?.takeIf { it.isNotEmpty() }
66+
?.forEach { group -> args.addAll(listOf("--install-group", group)) }
6267

6368
val bundleFile = bundle.orNull?.asFile
6469
if (bundleFile != null) {
@@ -102,6 +107,7 @@ abstract class SentryUploadAppArtifactTask @Inject constructor(objectFactory: Ob
102107
sentryProject: Provider<String>,
103108
sentryAuthToken: Property<String>,
104109
sentryUrl: Property<String>,
110+
installGroups: SetProperty<String>,
105111
taskSuffix: String = "",
106112
buildVariant: String = "",
107113
): Pair<TaskProvider<SentryUploadAppArtifactTask>, TaskProvider<SentryUploadAppArtifactTask>> {
@@ -130,6 +136,7 @@ abstract class SentryUploadAppArtifactTask @Inject constructor(objectFactory: Ob
130136
task.buildConfiguration.set(
131137
extension.sizeAnalysis.buildConfiguration.orElse(project.provider { buildVariant })
132138
)
139+
task.installGroups.set(installGroups)
133140
sentryTelemetryProvider?.let { task.sentryTelemetryService.set(it) }
134141
task.asSentryCliExec()
135142
task.withSentryTelemetry(extension, sentryTelemetryProvider)
@@ -159,6 +166,7 @@ abstract class SentryUploadAppArtifactTask @Inject constructor(objectFactory: Ob
159166
task.buildConfiguration.set(
160167
extension.sizeAnalysis.buildConfiguration.orElse(project.provider { buildVariant })
161168
)
169+
task.installGroups.set(installGroups)
162170
sentryTelemetryProvider?.let { task.sentryTelemetryService.set(it) }
163171
task.asSentryCliExec()
164172
task.withSentryTelemetry(extension, sentryTelemetryProvider)

plugin-build/src/test/kotlin/io/sentry/android/gradle/extensions/DistributionExtensionTest.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,22 @@ class DistributionExtensionTest {
6262

6363
assertEquals("test-token", extension.authToken.get())
6464
}
65+
66+
@Test
67+
fun `installGroups is empty by default`() {
68+
val project = ProjectBuilder.builder().build()
69+
val extension = project.objects.newInstance(DistributionExtension::class.java)
70+
71+
assertTrue(extension.installGroups.get().isEmpty())
72+
}
73+
74+
@Test
75+
fun `installGroups can be configured with group names`() {
76+
val project = ProjectBuilder.builder().build()
77+
val extension = project.objects.newInstance(DistributionExtension::class.java)
78+
79+
extension.installGroups.set(setOf("internal", "beta"))
80+
81+
assertEquals(setOf("internal", "beta"), extension.installGroups.get())
82+
}
6583
}

plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/GenerateDistributionPropertiesTaskTest.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.sentry.android.gradle.tasks
33
import io.sentry.android.gradle.extensions.SentryPluginExtension
44
import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask.Companion.BUILD_CONFIGURATION_PROPERTY
55
import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask.Companion.DISTRIBUTION_AUTH_TOKEN_PROPERTY
6+
import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask.Companion.INSTALL_GROUPS_PROPERTY
67
import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask.Companion.ORG_SLUG_PROPERTY
78
import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask.Companion.PROJECT_SLUG_PROPERTY
89
import io.sentry.android.gradle.util.PropertiesUtil
@@ -275,6 +276,65 @@ class GenerateDistributionPropertiesTaskTest {
275276
assertEquals("debug", props.getProperty(BUILD_CONFIGURATION_PROPERTY))
276277
}
277278

279+
@Test
280+
fun `installGroups is written to properties file as comma-separated list`() {
281+
val project = createProject()
282+
val extension = project.extensions.findByName("sentry") as SentryPluginExtension
283+
extension.distribution.installGroups.set(setOf("internal", "beta", "alpha"))
284+
285+
val task: TaskProvider<GenerateDistributionPropertiesTask> =
286+
GenerateDistributionPropertiesTask.register(
287+
project,
288+
extension,
289+
null,
290+
project.layout.buildDirectory.dir("dummy/folder/"),
291+
"test",
292+
"debug",
293+
)
294+
295+
val outputDir = File(project.buildDir, "dummy/folder/")
296+
outputDir.mkdirs()
297+
298+
task.get().generateProperties()
299+
300+
val expectedFile = File(project.buildDir, "dummy/folder/sentry-distribution.properties")
301+
assertTrue(expectedFile.exists())
302+
303+
val props = PropertiesUtil.load(expectedFile)
304+
305+
val installGroupsValue = props.getProperty(INSTALL_GROUPS_PROPERTY)
306+
kotlin.test.assertNotNull(installGroupsValue)
307+
val groups = installGroupsValue.split(",").toSet()
308+
assertEquals(setOf("internal", "beta", "alpha"), groups)
309+
}
310+
311+
@Test
312+
fun `empty installGroups does not write property to file`() {
313+
val project = createProject()
314+
val extension = project.extensions.findByName("sentry") as SentryPluginExtension
315+
extension.distribution.installGroups.set(emptySet())
316+
317+
val task: TaskProvider<GenerateDistributionPropertiesTask> =
318+
GenerateDistributionPropertiesTask.register(
319+
project,
320+
extension,
321+
null,
322+
project.layout.buildDirectory.dir("dummy/folder/"),
323+
"test",
324+
"debug",
325+
)
326+
327+
val outputDir = File(project.buildDir, "dummy/folder/")
328+
outputDir.mkdirs()
329+
330+
task.get().generateProperties()
331+
332+
val expectedFile = File(project.buildDir, "dummy/folder/sentry-distribution.properties")
333+
val props = PropertiesUtil.load(expectedFile)
334+
335+
assertNull(props.getProperty(INSTALL_GROUPS_PROPERTY))
336+
}
337+
278338
private fun createProject(): Project {
279339
with(ProjectBuilder.builder().build()) {
280340
plugins.apply("io.sentry.android.gradle")

plugin-build/src/test/kotlin/io/sentry/android/gradle/tasks/SentryUploadAppArtifactTaskTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,41 @@ class SentryUploadAppArtifactTaskTest {
270270
assertThat(args).containsAtLeast("--build-configuration", "debugRelease").inOrder()
271271
}
272272

273+
@Test
274+
fun `installGroups parameters are passed to CLI correctly`() {
275+
val project = createProject()
276+
val apkDir = project.apkDirProvider(dummyApkName)
277+
val task: TaskProvider<SentryUploadAppArtifactTask> =
278+
project.tasks.register("testUploadAppArtifact", SentryUploadAppArtifactTask::class.java) {
279+
it.cliExecutable.set("sentry-cli")
280+
it.apk.set(apkDir)
281+
it.installGroups.set(setOf("internal", "beta", "alpha"))
282+
}
283+
284+
val args = task.get().computeCommandLineArgs()
285+
286+
assertThat(args).contains("--install-group")
287+
assertThat(args).containsAtLeast("--install-group", "internal")
288+
assertThat(args).containsAtLeast("--install-group", "beta")
289+
assertThat(args).containsAtLeast("--install-group", "alpha")
290+
}
291+
292+
@Test
293+
fun `empty installGroups does not add CLI args`() {
294+
val project = createProject()
295+
val apkDir = project.apkDirProvider(dummyApkName)
296+
val task: TaskProvider<SentryUploadAppArtifactTask> =
297+
project.tasks.register("testUploadAppArtifact", SentryUploadAppArtifactTask::class.java) {
298+
it.cliExecutable.set("sentry-cli")
299+
it.apk.set(apkDir)
300+
it.installGroups.set(emptySet())
301+
}
302+
303+
val args = task.get().computeCommandLineArgs()
304+
305+
assertThat(args).doesNotContain("--install-group")
306+
}
307+
273308
private fun createProject(): Project {
274309
with(ProjectBuilder.builder().build()) {
275310
plugins.apply("com.android.application")

0 commit comments

Comments
 (0)