From c51c173a4ef8671326b4fa718fac57336ccd247c Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Fri, 7 Aug 2026 14:20:22 +1000 Subject: [PATCH 1/8] Use public Sentry org/project, env-injected token `sentryOrg`, the per-variant `sentry*Project` slugs, and `sentryAuthToken` aren't secrets: the org/project slugs are Sentry-public identifiers, and the auth token is CI-only. Moving them out of `secret.properties` shrinks what mobile-secrets needs to hold for this repo. The Sentry DSNs stay in `secret.properties` unchanged. Pocket Casts Android is open source and hasn't otherwise adopted a8c-secrets yet, so per the "Public vs. Secrets x Internal vs. External" RFC the DSN can't become a public repo constant the way it did in simplenote-android/dayone-android. The mobile-secrets deletion of the six keys above happens separately. Part of AINFRA-2790. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Sonnet 5 --- app/build.gradle.kts | 6 +----- automotive/build.gradle.kts | 6 +----- build.gradle.kts | 19 +++++++++++++++++-- dependencies.gradle.kts | 6 ------ wear/build.gradle.kts | 6 +----- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 599d9e2e6c0..0c59e490b5e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } sentry { - projectName = project.findProperty("sentryAndroidProject")?.toString() + projectName = "pocket-casts-android" } android { @@ -48,10 +48,6 @@ android { named("release") { manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher" - - if (project.findProperty("sentryAndroidProject")?.toString().isNullOrBlank()) { - println("WARNING: Sentry configuration not found. The ProGuard mapping files won't be uploaded.") - } } } diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 273572e5fcb..2793c2500cc 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -9,7 +9,7 @@ plugins { } sentry { - projectName = project.findProperty("sentryAutomotiveProject")?.toString() + projectName = "pocket-casts-automotive" } android { @@ -38,10 +38,6 @@ android { named("release") { manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher" - - if (project.findProperty("sentryAutomotiveProject")?.toString().isNullOrBlank()) { - println("WARNING: Sentry configuration not found. The ProGuard mapping files won't be uploaded.") - } } } diff --git a/build.gradle.kts b/build.gradle.kts index 608aa995262..a20234145df 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -474,9 +474,11 @@ subprojects { } fun Project.applyCommonSentryConfiguration() { + val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN") + extensions.getByType(SentryPluginExtension::class.java).apply { - authToken = project.findProperty("sentryAuthToken")?.toString() - org = project.findProperty("sentryOrg")?.toString() + authToken = sentryAuthToken.orNull + org = "a8c" val shouldUploadDebugFiles = System.getenv()["CI"].toBoolean() && !project.properties["skipSentryProguardMappingUpload"]?.toString().toBoolean() @@ -490,6 +492,19 @@ fun Project.applyCommonSentryConfiguration() { includeDependenciesReport = false ignoredBuildTypes = setOf("debug", "debugProd", "prototype") } + + // The upload task only exists when includeProguardMapping is on, so this fires exactly when a + // mapping upload is expected — fail there rather than ship a release we can't deobfuscate. + tasks.matching { it.name.startsWith("uploadSentryProguardMappings") }.configureEach { + doFirst { + if (!sentryAuthToken.isPresent) { + throw GradleException( + "SENTRY_AUTH_TOKEN is not set. Export it to upload ProGuard mappings, " + + "or pass -PskipSentryProguardMappingUpload=true to skip the upload.", + ) + } + } + } } tasks.register("aggregatedLintRelease") { diff --git a/dependencies.gradle.kts b/dependencies.gradle.kts index 8229c2d2c37..b1b56164a72 100644 --- a/dependencies.gradle.kts +++ b/dependencies.gradle.kts @@ -120,11 +120,5 @@ project.apply { set("encryptionKey", secretProperties.getProperty("encryption_key", "")) set("appSecret", secretProperties.getProperty("app_secret", "")) set("metaAppId", secretProperties.getProperty("metaAppId", "")) - set("sentryAuthToken", secretProperties.getProperty("sentryAuthToken", "")) - set("sentryOrg", secretProperties.getProperty("sentryOrg", "")) - set("sentryAndroidProject", secretProperties.getProperty("sentryAndroidProject", "")) - set("sentryAutomotiveProject", secretProperties.getProperty("sentryAutomotiveProject", "")) - set("sentryWearProject", secretProperties.getProperty("sentryWearProject", "")) - set("sentryTvProject", secretProperties.getProperty("sentryTvProject", "")) } } diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index b00d75695b7..1af31aa6739 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -11,7 +11,7 @@ plugins { } sentry { - projectName = project.findProperty("sentryWearProject")?.toString() + projectName = "pocket-casts-wear" } android { @@ -34,10 +34,6 @@ android { named("release") { manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher" - - if (project.findProperty("sentryWearProject")?.toString().isNullOrBlank()) { - println("WARNING: Sentry configuration not found. The ProGuard mapping files won't be uploaded.") - } } } From e11c0ea75e5dc671bedae4538be56ba16f310acb Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Aug 2026 15:39:41 +1000 Subject: [PATCH 2/8] Guard all sentry-cli tasks on a non-blank token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A secret-injection step that resolves to nothing exports an empty `SENTRY_AUTH_TOKEN` rather than leaving it unset, and `Provider.isPresent` is `true` for that. The guard passed, a blank token reached the plugin, and sentry-cli failed mid-upload with an opaque auth error instead of the actionable message. Normalising once at the source keeps the bound `authToken` and the guard from disagreeing. `includeSourceContext` rides the same flag as `includeProguardMapping` here, so `sentryUploadSourceBundle*` needs the token just as much, but the old `uploadSentryProguardMappings` name prefix never matched it — and task ordering isn't guaranteed, so a token-less CI run could fail there first. `SentryCliExecTask` is the base of every task that shells out to sentry-cli with the token, so matching on the type covers both and fails at compile time if a future plugin bump renames a task. This diverges from the name-prefix match in the sibling wpandroid and dayone-android changes. dayone-android never enables source context, so it isn't exposed; wpandroid does and still is. Both points came from review feedback on #5708. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Opus 5 --- build.gradle.kts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a20234145df..6129d158054 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ import com.google.devtools.ksp.gradle.KspExtension import com.google.devtools.ksp.gradle.KspGradleSubplugin import io.sentry.android.gradle.extensions.InstrumentationFeature import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.android.gradle.tasks.SentryCliExecTask import java.util.EnumSet import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions @@ -474,10 +475,13 @@ subprojects { } fun Project.applyCommonSentryConfiguration() { - val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN") + // A secret-injection step that resolves to nothing exports an empty SENTRY_AUTH_TOKEN rather + // than leaving it unset, so treat blank as missing — otherwise the guard below passes and + // sentry-cli fails mid-upload with an opaque auth error. + val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.takeIf { it.isNotBlank() } extensions.getByType(SentryPluginExtension::class.java).apply { - authToken = sentryAuthToken.orNull + authToken = sentryAuthToken org = "a8c" val shouldUploadDebugFiles = System.getenv()["CI"].toBoolean() && @@ -493,13 +497,15 @@ fun Project.applyCommonSentryConfiguration() { ignoredBuildTypes = setOf("debug", "debugProd", "prototype") } - // The upload task only exists when includeProguardMapping is on, so this fires exactly when a - // mapping upload is expected — fail there rather than ship a release we can't deobfuscate. - tasks.matching { it.name.startsWith("uploadSentryProguardMappings") }.configureEach { + // SentryCliExecTask is the base of every task that shells out to sentry-cli with the auth token + // (ProGuard mappings and source bundles here). Those tasks only exist when the upload flags are + // on, so this fires exactly when an upload is expected — fail there rather than ship a release + // we can't deobfuscate. + tasks.withType().configureEach { doFirst { - if (!sentryAuthToken.isPresent) { + if (sentryAuthToken == null) { throw GradleException( - "SENTRY_AUTH_TOKEN is not set. Export it to upload ProGuard mappings, " + + "SENTRY_AUTH_TOKEN is not set. Export it to upload debug files to Sentry, " + "or pass -PskipSentryProguardMappingUpload=true to skip the upload.", ) } From 0f7d6d9d29223412937e00ae6a61d7552f2fddc8 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Aug 2026 16:36:00 +1000 Subject: [PATCH 3/8] Remove verbose AI comments Co-authored-by: Gio Lodi --- build.gradle.kts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6129d158054..94a2c8d755f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -475,9 +475,6 @@ subprojects { } fun Project.applyCommonSentryConfiguration() { - // A secret-injection step that resolves to nothing exports an empty SENTRY_AUTH_TOKEN rather - // than leaving it unset, so treat blank as missing — otherwise the guard below passes and - // sentry-cli fails mid-upload with an opaque auth error. val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.takeIf { it.isNotBlank() } extensions.getByType(SentryPluginExtension::class.java).apply { @@ -497,10 +494,6 @@ fun Project.applyCommonSentryConfiguration() { ignoredBuildTypes = setOf("debug", "debugProd", "prototype") } - // SentryCliExecTask is the base of every task that shells out to sentry-cli with the auth token - // (ProGuard mappings and source bundles here). Those tasks only exist when the upload flags are - // on, so this fires exactly when an upload is expected — fail there rather than ship a release - // we can't deobfuscate. tasks.withType().configureEach { doFirst { if (sentryAuthToken == null) { From 527d95bbe60922bcb3092ed2daf2f4c10990deef Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 11 Aug 2026 17:31:19 +1000 Subject: [PATCH 4/8] Delete unused `sentry.properties.enc` --- .configure-files/sentry.properties.enc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .configure-files/sentry.properties.enc diff --git a/.configure-files/sentry.properties.enc b/.configure-files/sentry.properties.enc deleted file mode 100644 index 82b98bce1b1..00000000000 --- a/.configure-files/sentry.properties.enc +++ /dev/null @@ -1 +0,0 @@ -UgUt>|tϝmHLGX#kA@r{xjjKKhVh8P8^c4EcC~LX wýPIBZK>ʿi^1Q2{{DM \ No newline at end of file From 9193ab20e0b958dfb6d780898c69c3ec88737734 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 18 Aug 2026 20:08:36 +1000 Subject: [PATCH 5/8] Keep the Sentry auth token lazy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading `SENTRY_AUTH_TOKEN` through the Provider API instead of resolving it eagerly with `.orNull` means a build that never configures Sentry never resolves it, and the extension property and the task guard are the same provider rather than two reads that could drift. Per @iangmaia's review on #5708. The plugin does not default `authToken` from the environment — its convention is `null`, and the extension value also feeds telemetry and the org lookup, not just the `sentry-cli` environment — so the assignment itself stays. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 5 --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 94a2c8d755f..7aaeac03d0e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -475,7 +475,7 @@ subprojects { } fun Project.applyCommonSentryConfiguration() { - val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.takeIf { it.isNotBlank() } + val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").filter { it.isNotBlank() } extensions.getByType(SentryPluginExtension::class.java).apply { authToken = sentryAuthToken @@ -496,7 +496,7 @@ fun Project.applyCommonSentryConfiguration() { tasks.withType().configureEach { doFirst { - if (sentryAuthToken == null) { + if (!sentryAuthToken.isPresent) { throw GradleException( "SENTRY_AUTH_TOKEN is not set. Export it to upload debug files to Sentry, " + "or pass -PskipSentryProguardMappingUpload=true to skip the upload.", From d4c2b86e0e8ad145619b69070e47fb337d791992 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 18 Aug 2026 20:23:55 +1000 Subject: [PATCH 6/8] Revert "Keep the Sentry auth token lazy" This reverts 5dad76003, restoring the eager read. The lazy provider was a local improvement that made this repo the odd one out. simplenote-android#1850 and WordPress-Android#23189 both shipped the eager `.orNull` read bound to a `String?` val, and this migration's value is one pattern across the fleet, not the best pattern per repo. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 5 --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7aaeac03d0e..94a2c8d755f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -475,7 +475,7 @@ subprojects { } fun Project.applyCommonSentryConfiguration() { - val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").filter { it.isNotBlank() } + val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.takeIf { it.isNotBlank() } extensions.getByType(SentryPluginExtension::class.java).apply { authToken = sentryAuthToken @@ -496,7 +496,7 @@ fun Project.applyCommonSentryConfiguration() { tasks.withType().configureEach { doFirst { - if (!sentryAuthToken.isPresent) { + if (sentryAuthToken == null) { throw GradleException( "SENTRY_AUTH_TOKEN is not set. Export it to upload debug files to Sentry, " + "or pass -PskipSentryProguardMappingUpload=true to skip the upload.", From 829a341ed694bec4d551c60feaa8ebcbd77f08b5 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 18 Aug 2026 20:37:47 +1000 Subject: [PATCH 7/8] Trim the Sentry auth token before using it A token injected with a trailing newline passed `isNotBlank()` and reached `sentry-cli` verbatim, failing with the opaque auth error the guard exists to pre-empt. Aligns with WordPress-Android#23189 and DayOne-Android#8230, which both trim. Their `?.trim() ?: null` does not port directly: it relies on Groovy truth, where `"" ?: null` is `null`. Kotlin's elvis fires only on `null`, so the literal translation would let a set-but-empty variable through as `""`. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 5 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 94a2c8d755f..7dc164fa430 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -475,7 +475,7 @@ subprojects { } fun Project.applyCommonSentryConfiguration() { - val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.takeIf { it.isNotBlank() } + val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.trim()?.ifEmpty { null } extensions.getByType(SentryPluginExtension::class.java).apply { authToken = sentryAuthToken From f981a329933df3267adc771c795207f94d0e0179 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 18 Aug 2026 20:42:54 +1000 Subject: [PATCH 8/8] Say "or is blank" in the auth token guard The guard rejects a set-but-empty variable as well as an unset one, but the message named only the unset case, sending anyone who hit it looking for a variable that is in fact there. Matches DayOne-Android#8230 and WordPress-Android#23189. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 5 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7dc164fa430..6ca14232a22 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -498,7 +498,7 @@ fun Project.applyCommonSentryConfiguration() { doFirst { if (sentryAuthToken == null) { throw GradleException( - "SENTRY_AUTH_TOKEN is not set. Export it to upload debug files to Sentry, " + + "SENTRY_AUTH_TOKEN is not set (or is blank). Export it to upload debug files to Sentry, " + "or pass -PskipSentryProguardMappingUpload=true to skip the upload.", ) }