From 6ffd9378be47f9a4747db4218ce4872ebd04f065 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Fri, 20 Feb 2026 15:03:26 -0500 Subject: [PATCH 1/5] Move task operations out of onVariants to fix AGP Artifacts API conflicts --- packages/core/sentry.gradle | 56 +++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 3ab9225241..13b1685dcf 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -65,13 +65,43 @@ tasks.register("cleanupTemporarySentryJsonConfiguration") { plugins.withId('com.android.application') { def androidComponents = extensions.getByName("androidComponents") + // Collect only variant metadata in onVariants - do NOT access tasks here. + // Calling tasks.matching { }.each { } inside onVariants forces task realization + // during AGP's variant configuration phase, which disrupts the Artifacts API + // transform chain used by other plugins (e.g. those calling + // variant.artifacts.use(...).toTransform(SingleArtifact.APK)). The result is + // that those plugins' APK output ends up in build/intermediates/ instead of + // build/outputs/, causing downstream tooling to fail to locate the final APK. + def releaseVariants = [] androidComponents.onVariants(androidComponents.selector().all()) { v -> if (!v.name.toLowerCase().contains("debug")) { + def outputs = [] + v.outputs.each { output -> + outputs << [ + baseName: output.baseName, + versionCode: output.versionCode.getOrElse(0), + versionName: output.versionName.getOrElse(''), + appId: v.applicationId.get(), + ] + } + releaseVariants << [name: v.name, outputs: outputs] + } + } + + // All task-level operations must happen in afterEvaluate, not inside onVariants. + // By the time afterEvaluate runs, all plugins have registered their onVariants + // callbacks and the AGP Artifacts API transform chain is fully established. + project.afterEvaluate { + releaseVariants.each { variantData -> // separately we then hook into the bundle task of react native to inject // sourcemap generation parameters. In case for whatever reason no release // was found for the asset folder we just bail. - def bundleTasks = tasks.findAll { task -> (task.name.startsWith("createBundle") || task.name.startsWith("bundle")) && task.name.endsWith("JsAndAssets") && !task.name.contains("Debug") && task.enabled } - bundleTasks.each { bundleTask -> + tasks.matching { task -> + (task.name.startsWith("createBundle") || task.name.startsWith("bundle")) && + task.name.endsWith("JsAndAssets") && + !task.name.contains("Debug") && + task.enabled + }.each { bundleTask -> def shouldCleanUp def sourcemapOutput def bundleOutput @@ -94,7 +124,7 @@ plugins.withId('com.android.application') { // .findAll{!['class', 'active'].contains(it.key)} // .join('\n') - def currentVariants = extractCurrentVariants(bundleTask, v) + def currentVariants = extractCurrentVariants(bundleTask, variantData) if (currentVariants == null) return def previousCliTask = null @@ -497,8 +527,12 @@ def forceSourceMapOutputFromBundleTask(bundleTask) { return [shouldCleanUp, bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand] } -/** compose array with one item - current build flavor name */ -static extractCurrentVariants(bundleTask, variant) { +/** compose array with one item - current build flavor name. + * variantData is a pre-collected plain map of the form: + * [name: String, outputs: [[baseName, versionCode, versionName, appId], ...]] + * collected during the onVariants callback to avoid accessing tasks there. + */ +static extractCurrentVariants(bundleTask, variantData) { // examples: bundleLocalReleaseJsAndAssets, createBundleYellowDebugJsAndAssets def pattern = Pattern.compile("(?:create)?(?:B|b)undle([A-Z][A-Za-z0-9_]+)JsAndAssets") @@ -511,14 +545,14 @@ static extractCurrentVariants(bundleTask, variant) { } def currentVariants = null - if (variant.name.equalsIgnoreCase(currentRelease)) { + if (variantData.name.equalsIgnoreCase(currentRelease)) { currentVariants = [:] - def variantName = variant.name - variant.outputs.each { output -> - def defaultVersionCode = output.versionCode.getOrElse(0) + def variantName = variantData.name + variantData.outputs.each { output -> + def defaultVersionCode = output.versionCode def versionCode = System.getenv('SENTRY_DIST') ?: defaultVersionCode - def appId = variant.applicationId.get() - def versionName = output.versionName.getOrElse('') // may be empty if not set + def appId = output.appId + def versionName = output.versionName // may be empty if not set def defaultReleaseName = "${appId}@${versionName}+${versionCode}" def releaseName = System.getenv('SENTRY_RELEASE') ?: defaultReleaseName From 42727924f658cf8c4b0222d6fa524c92c4449c7e Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 23 Feb 2026 10:37:50 -0500 Subject: [PATCH 2/5] Moving the enabled check into the .each block Co-authored-by: Antonis Lilis --- packages/core/sentry.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 13b1685dcf..10f4186946 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -100,8 +100,8 @@ plugins.withId('com.android.application') { (task.name.startsWith("createBundle") || task.name.startsWith("bundle")) && task.name.endsWith("JsAndAssets") && !task.name.contains("Debug") && - task.enabled }.each { bundleTask -> + if (!bundleTask.enabled) return def shouldCleanUp def sourcemapOutput def bundleOutput From 24f3f78b288cd968e9ddd75c01c1a82e0fcdf487 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 23 Feb 2026 10:41:35 -0500 Subject: [PATCH 3/5] Add warn if releaseVariants is not populated Co-authored-by: Antonis Lilis --- packages/core/sentry.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 10f4186946..5b97f59fdb 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -92,6 +92,9 @@ plugins.withId('com.android.application') { // By the time afterEvaluate runs, all plugins have registered their onVariants // callbacks and the AGP Artifacts API transform chain is fully established. project.afterEvaluate { + if (releaseVariants.isEmpty()) { + project.logger.warn("[sentry] No release variants collected, onVariants may have run after afterEvaluate. Sourcemap upload tasks will not be registered.") + } releaseVariants.each { variantData -> // separately we then hook into the bundle task of react native to inject // sourcemap generation parameters. In case for whatever reason no release From b0722591c966179421c38057d5a22f72a03fac4d Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 23 Feb 2026 11:45:17 -0500 Subject: [PATCH 4/5] remove unnecessary data conversion --- packages/core/sentry.gradle | 41 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 5b97f59fdb..5b39c49e7b 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -65,8 +65,8 @@ tasks.register("cleanupTemporarySentryJsonConfiguration") { plugins.withId('com.android.application') { def androidComponents = extensions.getByName("androidComponents") - // Collect only variant metadata in onVariants - do NOT access tasks here. - // Calling tasks.matching { }.each { } inside onVariants forces task realization + // Collect ApplicationVariant objects in onVariants - do NOT access tasks here. + // Calling tasks.findAll or tasks.matching{}.each{} inside onVariants forces task realization // during AGP's variant configuration phase, which disrupts the Artifacts API // transform chain used by other plugins (e.g. those calling // variant.artifacts.use(...).toTransform(SingleArtifact.APK)). The result is @@ -75,16 +75,7 @@ plugins.withId('com.android.application') { def releaseVariants = [] androidComponents.onVariants(androidComponents.selector().all()) { v -> if (!v.name.toLowerCase().contains("debug")) { - def outputs = [] - v.outputs.each { output -> - outputs << [ - baseName: output.baseName, - versionCode: output.versionCode.getOrElse(0), - versionName: output.versionName.getOrElse(''), - appId: v.applicationId.get(), - ] - } - releaseVariants << [name: v.name, outputs: outputs] + releaseVariants << v } } @@ -95,14 +86,14 @@ plugins.withId('com.android.application') { if (releaseVariants.isEmpty()) { project.logger.warn("[sentry] No release variants collected, onVariants may have run after afterEvaluate. Sourcemap upload tasks will not be registered.") } - releaseVariants.each { variantData -> + releaseVariants.each { v -> // separately we then hook into the bundle task of react native to inject // sourcemap generation parameters. In case for whatever reason no release // was found for the asset folder we just bail. tasks.matching { task -> (task.name.startsWith("createBundle") || task.name.startsWith("bundle")) && task.name.endsWith("JsAndAssets") && - !task.name.contains("Debug") && + !task.name.contains("Debug") }.each { bundleTask -> if (!bundleTask.enabled) return def shouldCleanUp @@ -127,7 +118,7 @@ plugins.withId('com.android.application') { // .findAll{!['class', 'active'].contains(it.key)} // .join('\n') - def currentVariants = extractCurrentVariants(bundleTask, variantData) + def currentVariants = extractCurrentVariants(bundleTask, v) if (currentVariants == null) return def previousCliTask = null @@ -530,12 +521,8 @@ def forceSourceMapOutputFromBundleTask(bundleTask) { return [shouldCleanUp, bundleOutput, sourcemapOutput, packagerSourcemapOutput, bundleCommand] } -/** compose array with one item - current build flavor name. - * variantData is a pre-collected plain map of the form: - * [name: String, outputs: [[baseName, versionCode, versionName, appId], ...]] - * collected during the onVariants callback to avoid accessing tasks there. - */ -static extractCurrentVariants(bundleTask, variantData) { +/** compose array with one item - current build flavor name */ +static extractCurrentVariants(bundleTask, variant) { // examples: bundleLocalReleaseJsAndAssets, createBundleYellowDebugJsAndAssets def pattern = Pattern.compile("(?:create)?(?:B|b)undle([A-Z][A-Za-z0-9_]+)JsAndAssets") @@ -548,14 +535,14 @@ static extractCurrentVariants(bundleTask, variantData) { } def currentVariants = null - if (variantData.name.equalsIgnoreCase(currentRelease)) { + if (variant.name.equalsIgnoreCase(currentRelease)) { currentVariants = [:] - def variantName = variantData.name - variantData.outputs.each { output -> - def defaultVersionCode = output.versionCode + def variantName = variant.name + variant.outputs.each { output -> + def defaultVersionCode = output.versionCode.getOrElse(0) def versionCode = System.getenv('SENTRY_DIST') ?: defaultVersionCode - def appId = output.appId - def versionName = output.versionName // may be empty if not set + def appId = variant.applicationId.get() + def versionName = output.versionName.getOrElse('') // may be empty if not set def defaultReleaseName = "${appId}@${versionName}+${versionCode}" def releaseName = System.getenv('SENTRY_RELEASE') ?: defaultReleaseName From ee94db5b01a2f99d5d92d08f32d506dc23066774 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 23 Feb 2026 12:45:31 -0500 Subject: [PATCH 5/5] Add null safety and consistent lazy evaluation for task matching --- packages/core/sentry.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 5b39c49e7b..cdb70e7d85 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -119,7 +119,7 @@ plugins.withId('com.android.application') { // .join('\n') def currentVariants = extractCurrentVariants(bundleTask, v) - if (currentVariants == null) return + if (currentVariants == null || currentVariants.isEmpty()) return def previousCliTask = null def applicationVariant = null @@ -328,9 +328,10 @@ plugins.withId('com.android.application') { previousCliTask.configure { finalizedBy cliCleanUpTask } def packageTasks = tasks.matching { - task -> ("package${applicationVariant}".equalsIgnoreCase(task.name) || "package${applicationVariant}Bundle".equalsIgnoreCase(task.name)) && task.enabled + task -> ("package${applicationVariant}".equalsIgnoreCase(task.name) || "package${applicationVariant}Bundle".equalsIgnoreCase(task.name)) } packageTasks.configureEach { packageTask -> + if (!packageTask.enabled) return packageTask.dependsOn modulesTask packageTask.finalizedBy modulesCleanUpTask }