From dc236ea5e6720a36a564ffcc2324f17fbc1ac21b Mon Sep 17 00:00:00 2001 From: Fabien Demangeat Date: Fri, 17 Jul 2026 15:32:20 +0100 Subject: [PATCH 1/4] fix(android): make Sentry Gradle tasks Configuration Cache compatible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `copySentryJsonConfiguration` and the `*_SentryUpload` tasks no longer read `project` state at execution time — `onlyIf` predicates resolving closures from `project.extra`, plus `project.rootDir`, `project.copy`, `project.logger`, and `Project.file` inside task actions. Environment reads are captured at configuration time, file copies use an injected `FileSystemOperations`, and task actions use the task's own `logger`. No behaviour change. This fixes the hard `Could not evaluate onlyIf predicate` failure when `org.gradle.configuration-cache=true` (Gradle 9 recommends it by default). Verified end-to-end on a React Native 0.83.9 app with Gradle 9.0.0: the equivalent Groovy `sentry.gradle` (shipped in 8.10.0) went from BUILD FAILED (`Could not evaluate onlyIf predicate for task ':app:copySentryJsonConfiguration'`) to BUILD SUCCESSFUL with config cache enabled after this change. See #6466. Interim step ahead of the SAGP migration (getsentry/sentry-android-gradle-plugin#796). Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Code --- CHANGELOG.md | 6 ++++++ packages/core/sentry.gradle.kts | 36 ++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13d8fd9eba..e2d8f74bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ ## Unreleased +### Fixes + +- Make `copySentryJsonConfiguration` and the `*_SentryUpload` Gradle tasks compatible with the Gradle Configuration Cache ([#6466](https://github.com/getsentry/sentry-react-native/issues/6466)) + + These tasks previously read `project` state at execution time — `onlyIf` predicates resolving closures from `project.extra`, plus `project.rootDir`, `project.copy`, `project.logger`, and `Project.file` inside task actions — which fails the build with `Could not evaluate onlyIf predicate` when `org.gradle.configuration-cache=true` (Gradle 9 defaults to recommending it). Environment reads are now captured at configuration time, file copies use an injected `FileSystemOperations`, and task actions use the task's own `logger`. No behaviour change. Interim step ahead of the full SAGP migration (getsentry/sentry-android-gradle-plugin#796). + ### Dependencies - Bump JavaScript SDK from v10.65.0 to v10.67.0 ([#6471](https://github.com/getsentry/sentry-react-native/pull/6471), [#6494](https://github.com/getsentry/sentry-react-native/pull/6494)) diff --git a/packages/core/sentry.gradle.kts b/packages/core/sentry.gradle.kts index 1f3c6d06f4..c418e7168e 100644 --- a/packages/core/sentry.gradle.kts +++ b/packages/core/sentry.gradle.kts @@ -78,6 +78,11 @@ interface InjectedExecOps { val execOps: org.gradle.process.ExecOperations } +interface InjectedFsOps { + @get:Inject + val fs: org.gradle.api.file.FileSystemOperations +} + extra["shouldCopySentryOptionsFile"] = object : groovy.lang.Closure(this) { fun doCall(): Boolean = System.getenv("SENTRY_COPY_OPTIONS_FILE") != "false" @@ -100,17 +105,24 @@ val config: Map = val configFile = "sentry.options.json" val androidAssetsDir = File("$rootDir/app/src/main/assets") +// Values captured at configuration time so task onlyIf specs and actions do not read +// `project` state at execution time (required for Gradle Configuration Cache compatibility). +val copyOptionsFileEnabled = System.getenv("SENTRY_COPY_OPTIONS_FILE") != "false" +val sentryAutoUploadGeneralEnabled = System.getenv("SENTRY_DISABLE_AUTO_UPLOAD") != "true" +val rootDirFile = project.rootDir + tasks.register("copySentryJsonConfiguration") { - onlyIf { shouldCopySentryOptionsFile() } + onlyIf { copyOptionsFileEnabled } + val injectedFs = project.objects.newInstance(InjectedFsOps::class.java) doLast { - val appRoot = project.rootDir.parentFile ?: project.rootDir + val appRoot = rootDirFile.parentFile ?: rootDirFile val sentryOptionsFile = File(appRoot, configFile) if (sentryOptionsFile.exists()) { if (!androidAssetsDir.exists()) { androidAssetsDir.mkdirs() } - copy { + injectedFs.fs.copy { from(sentryOptionsFile) into(androidAssetsDir) rename { configFile } @@ -156,7 +168,7 @@ tasks.register("copySentryJsonConfiguration") { } tasks.register("cleanupTemporarySentryJsonConfiguration") { - onlyIf { shouldCopySentryOptionsFile() } + onlyIf { copyOptionsFileEnabled } doLast { val sentryOptionsFile = File(androidAssetsDir, configFile) if (sentryOptionsFile.exists()) { @@ -207,7 +219,7 @@ fun resolveSentryCliPackagePath(reactRoot: File): String { "--print", "require.resolve('@sentry/cli/package.json', { paths: [require.resolve('@sentry/react-native/package.json')] })", ), - ).directory(rootDir) + ).directory(reactRoot) .start() val output = process.inputStream @@ -558,7 +570,7 @@ fun processVariant(v: Any) { val cliTask = tasks.register(nameCliTask) { - onlyIf { shouldSentryAutoUploadGeneral() } + onlyIf { sentryAutoUploadGeneralEnabled } description = "upload debug symbols to sentry" group = "sentry.io" @@ -591,9 +603,9 @@ fun processVariant(v: Any) { .start() val processOutput = process.inputStream.bufferedReader().readText() process.waitFor() - project.logger.lifecycle("Check generated source map for Debug ID: $processOutput") + logger.lifecycle("Check generated source map for Debug ID: $processOutput") - project.logger.lifecycle("Sentry Source Maps upload will include the release name and dist.") + logger.lifecycle("Sentry Source Maps upload will include the release name and dist.") extraArgs.addAll(listOf("--release", releaseName, "--dist", versionCode.toString())) } @@ -608,7 +620,7 @@ fun processVariant(v: Any) { if (flavorAware) { propertiesFile = "$reactRoot/android/sentry-$variant.properties" - project.logger.info("For $variant using: $propertiesFile") + logger.info("For $variant using: $propertiesFile") } else { environment("SENTRY_PROPERTIES", propertiesFile) } @@ -623,7 +635,7 @@ fun processVariant(v: Any) { "Create it, or disable 'flavorAware' in project.ext.sentryCli.", ) } - project.logger.info("file not found '$propertiesFile' for '$variant'") + logger.info("file not found '$propertiesFile' for '$variant'") } val sentryUrl = sentryProps.getProperty("defaults.url") @@ -685,9 +697,9 @@ fun processVariant(v: Any) { } else { args } - project.logger.lifecycle("Sentry-CLI arguments: $loggedArgs") + logger.lifecycle("Sentry-CLI arguments: $loggedArgs") val osCompatibility = if (Os.isFamily(Os.FAMILY_WINDOWS)) listOf("cmd", "/c", "node") else emptyList() - if (System.getenv("SENTRY_DOTENV_PATH") == null && file("$reactRoot/.env.sentry-build-plugin").exists()) { + if (System.getenv("SENTRY_DOTENV_PATH") == null && File("$reactRoot/.env.sentry-build-plugin").exists()) { environment("SENTRY_DOTENV_PATH", "$reactRoot/.env.sentry-build-plugin") } commandLine(osCompatibility + args) From 77a478337c10a488b8ab53e10c9a08f44b67fa78 Mon Sep 17 00:00:00 2001 From: Fabien Demangeat Date: Fri, 17 Jul 2026 15:54:26 +0100 Subject: [PATCH 2/4] fix(android): consult overridable auto-upload closure at config time Addresses review feedback: capturing `SENTRY_DISABLE_AUTO_UPLOAD` via `System.getenv` at apply-time ignored a `project.ext.shouldSentryAutoUploadGeneral` override set right after `apply from` (e.g. the Expo `disableAutoUpload` config plugin, which injects `project.ext.shouldSentryAutoUploadGeneral = { -> return false }`). Uploads would then run even when the user disabled them. Now capture `shouldSentryAutoUploadGeneral()` inside the `onVariants` callback, which runs after the app `build.gradle` has evaluated, so the override is in effect. Likewise capture `shouldCopySentryOptionsFile()` via the closure rather than re-reading the env var, so `project.ext` overrides of it keep working. Both `onlyIf` specs still reference a captured boolean, so the tasks remain Configuration Cache compatible, and the original override behavior is restored. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Code --- packages/core/sentry.gradle.kts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/core/sentry.gradle.kts b/packages/core/sentry.gradle.kts index c418e7168e..8b5c1d60fd 100644 --- a/packages/core/sentry.gradle.kts +++ b/packages/core/sentry.gradle.kts @@ -107,8 +107,9 @@ val androidAssetsDir = File("$rootDir/app/src/main/assets") // Values captured at configuration time so task onlyIf specs and actions do not read // `project` state at execution time (required for Gradle Configuration Cache compatibility). -val copyOptionsFileEnabled = System.getenv("SENTRY_COPY_OPTIONS_FILE") != "false" -val sentryAutoUploadGeneralEnabled = System.getenv("SENTRY_DISABLE_AUTO_UPLOAD") != "true" +// The overridable `shouldCopySentryOptionsFile()` closure is still consulted (not the env var +// directly) so setups that reassign `project.ext.shouldCopySentryOptionsFile` keep working. +val copyOptionsFileEnabled = shouldCopySentryOptionsFile() val rootDirFile = project.rootDir tasks.register("copySentryJsonConfiguration") { @@ -503,6 +504,12 @@ fun processVariant(v: Any) { val vName = v.javaClass.getMethod("getName").invoke(v) as String if (vName.contains("debug", ignoreCase = true)) return + // Captured here (inside the onVariants callback, which runs after the app build.gradle has + // evaluated) rather than at apply-time, so a `project.ext.shouldSentryAutoUploadGeneral` + // override set right after `apply from` (e.g. the Expo `disableAutoUpload` plugin) is honored. + // Referencing this captured boolean in `onlyIf` keeps the task Configuration Cache compatible. + val sentryAutoUploadGeneralEnabled = shouldSentryAutoUploadGeneral() + val variantCapitalized = Character.toUpperCase(vName[0]).toString() + vName.substring(1) val sentryBundleTaskName = listOf( From 32d2d3efaf82bfeed513214b24da3d7f8d6cdc9e Mon Sep 17 00:00:00 2001 From: Fabien Demangeat Date: Fri, 17 Jul 2026 17:44:18 +0100 Subject: [PATCH 3/4] fix(android): defer copy-options capture + hoist CLI path resolution (CC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two more review findings plus a failure surfaced by a full bundle run under the configuration cache: 1. `copyOptionsFileEnabled` is now a `Property` set in `afterEvaluate` (after the app build.gradle evaluates), so a `project.ext.shouldCopySentryOptionsFile` override placed after `apply from` is honored — mirroring the deferral already applied for `shouldSentryAutoUploadGeneral`. 2. `resolveSentryCliPackagePath(reactRoot)` is now resolved at configuration time and captured, instead of being called inside the `*_SentryUpload` `doLast` exec closure. A `:app:createBundle...JsAndAssets` run with `configuration-cache=true` showed the previous revision still failed at execution ("Could not find method resolveSentryCliPackagePath() ... on DefaultExecAction_Decorated"): a serialized task action cannot resolve top-level script methods. Verified on a fresh configuration-cache store (React Native 0.83.9, Gradle 9.0.0): copySentryJsonConfiguration, _SentryUpload, and _SentryCollectModules execute with no sentry.gradle hard failure. Only the pre-existing warn-level config-time `require.resolve` external processes (resolveSentryReactNativeSDKPath / resolveSentryCliPackagePath) remain, which are out of scope for this PR. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Code --- packages/core/sentry.gradle.kts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/core/sentry.gradle.kts b/packages/core/sentry.gradle.kts index 8b5c1d60fd..331af5d300 100644 --- a/packages/core/sentry.gradle.kts +++ b/packages/core/sentry.gradle.kts @@ -107,13 +107,14 @@ val androidAssetsDir = File("$rootDir/app/src/main/assets") // Values captured at configuration time so task onlyIf specs and actions do not read // `project` state at execution time (required for Gradle Configuration Cache compatibility). -// The overridable `shouldCopySentryOptionsFile()` closure is still consulted (not the env var -// directly) so setups that reassign `project.ext.shouldCopySentryOptionsFile` keep working. -val copyOptionsFileEnabled = shouldCopySentryOptionsFile() +// `copyOptionsFileEnabled` is a Property populated in `afterEvaluate` (below) rather than at +// apply-time, so a `project.ext.shouldCopySentryOptionsFile` override placed after `apply from` +// is still honored. Referencing the Property in `onlyIf` keeps the tasks Config Cache compatible. +val copyOptionsFileEnabled = objects.property(Boolean::class.java) val rootDirFile = project.rootDir tasks.register("copySentryJsonConfiguration") { - onlyIf { copyOptionsFileEnabled } + onlyIf { copyOptionsFileEnabled.get() } val injectedFs = project.objects.newInstance(InjectedFsOps::class.java) doLast { val appRoot = rootDirFile.parentFile ?: rootDirFile @@ -169,7 +170,7 @@ tasks.register("copySentryJsonConfiguration") { } tasks.register("cleanupTemporarySentryJsonConfiguration") { - onlyIf { copyOptionsFileEnabled } + onlyIf { copyOptionsFileEnabled.get() } doLast { val sentryOptionsFile = File(androidAssetsDir, configFile) if (sentryOptionsFile.exists()) { @@ -582,6 +583,10 @@ fun processVariant(v: Any) { group = "sentry.io" val sentryPackage = resolveSentryReactNativeSDKPath(reactRoot) + // Resolved at configuration time and captured: calling this script method from + // inside the doLast exec closure would fail under the Configuration Cache (the + // serialized task action can't resolve top-level script methods). + val cliPackage = resolveSentryCliPackagePath(reactRoot) val copyDebugIdScript = config["copyDebugIdScript"] ?.toString() @@ -663,7 +668,6 @@ fun processVariant(v: Any) { } } - val cliPackage = resolveSentryCliPackagePath(reactRoot) var cliExecutable = sentryProps.getProperty("cli.executable") ?: "$cliPackage/bin/sentry-cli" if (Os.isFamily(Os.FAMILY_WINDOWS)) { @@ -797,6 +801,10 @@ fun processVariant(v: Any) { } project.afterEvaluate { + // Resolve the (overridable) closure now, after the app build.gradle has evaluated, so an + // override placed after `apply from` is honored. The Property is read by the copy/cleanup + // tasks' onlyIf at execution time without touching `project` (Configuration Cache safe). + copyOptionsFileEnabled.set(shouldCopySentryOptionsFile()) tasks.named("preBuild").configure { dependsOn("copySentryJsonConfiguration") } From 9c4bfe21f08937e76af560f72d0cfafd08bf3a9f Mon Sep 17 00:00:00 2001 From: Fabien Demangeat Date: Tue, 21 Jul 2026 13:24:26 +0100 Subject: [PATCH 4/4] =?UTF-8?q?fix(android):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20CC=20property=20default,=20dir=20consistency,=20cha?= =?UTF-8?q?ngelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `copyOptionsFileEnabled` now has `.convention(true)`, preserving the documented default (copy enabled) so `onlyIf { .get() }` can't throw IllegalStateException if `afterEvaluate` never runs. - `resolveSentryCliPackagePath` reverted to `.directory(rootDir)` to match `resolveSentryReactNativeSDKPath`; both run at configuration time now, so `rootDir` is Configuration Cache safe and consistent. - CHANGELOG entry links the PR (#6469) instead of the issue. Re-verified on a fresh configuration-cache store (RN 0.83.9, Gradle 9.0.0): copySentryJsonConfiguration, _SentryUpload, and _SentryCollectModules all execute with no sentry.gradle failure. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Code --- CHANGELOG.md | 2 +- packages/core/sentry.gradle.kts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2d8f74bac..deed15e8f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Make `copySentryJsonConfiguration` and the `*_SentryUpload` Gradle tasks compatible with the Gradle Configuration Cache ([#6466](https://github.com/getsentry/sentry-react-native/issues/6466)) +- Make `copySentryJsonConfiguration` and the `*_SentryUpload` Gradle tasks compatible with the Gradle Configuration Cache ([#6469](https://github.com/getsentry/sentry-react-native/pull/6469)) These tasks previously read `project` state at execution time — `onlyIf` predicates resolving closures from `project.extra`, plus `project.rootDir`, `project.copy`, `project.logger`, and `Project.file` inside task actions — which fails the build with `Could not evaluate onlyIf predicate` when `org.gradle.configuration-cache=true` (Gradle 9 defaults to recommending it). Environment reads are now captured at configuration time, file copies use an injected `FileSystemOperations`, and task actions use the task's own `logger`. No behaviour change. Interim step ahead of the full SAGP migration (getsentry/sentry-android-gradle-plugin#796). diff --git a/packages/core/sentry.gradle.kts b/packages/core/sentry.gradle.kts index 331af5d300..1ab9e54543 100644 --- a/packages/core/sentry.gradle.kts +++ b/packages/core/sentry.gradle.kts @@ -110,7 +110,8 @@ val androidAssetsDir = File("$rootDir/app/src/main/assets") // `copyOptionsFileEnabled` is a Property populated in `afterEvaluate` (below) rather than at // apply-time, so a `project.ext.shouldCopySentryOptionsFile` override placed after `apply from` // is still honored. Referencing the Property in `onlyIf` keeps the tasks Config Cache compatible. -val copyOptionsFileEnabled = objects.property(Boolean::class.java) +// The convention preserves the documented default (copy enabled) if `afterEvaluate` never runs. +val copyOptionsFileEnabled = objects.property(Boolean::class.java).convention(true) val rootDirFile = project.rootDir tasks.register("copySentryJsonConfiguration") { @@ -221,7 +222,7 @@ fun resolveSentryCliPackagePath(reactRoot: File): String { "--print", "require.resolve('@sentry/cli/package.json', { paths: [require.resolve('@sentry/react-native/package.json')] })", ), - ).directory(reactRoot) + ).directory(rootDir) .start() val output = process.inputStream