Use public Sentry org/project, env-injected token - #5708
Conversation
|
Claude finished @mokagio's task in 3m 48s —— View job Code review
The direction is sound and the change is well scoped. Two inline comments on Findings1. A blank
2. The guard doesn't cover the source-bundle upload — inline ·
3. Confirm the hardcoded slugs against what's in
Verified
On the stated gotchaNothing in this repo exports Security / performanceNo concerns. Moving the token from an on-disk Review only — no code changes made or pushed. |
There was a problem hiding this comment.
Pull request overview
This PR makes Sentry Gradle configuration less dependent on secret.properties by hardcoding the public Sentry org/project slugs in build scripts and sourcing the Sentry auth token from an environment variable (for CI-only ProGuard mapping uploads).
Changes:
- Hardcode Sentry
projectNamefor app, automotive, and wear modules to public slugs. - Stop exporting Sentry org/project/token values from
secret.propertiesviadependencies.gradle.kts. - Update common Sentry Gradle configuration to read
SENTRY_AUTH_TOKENfrom the environment and fail the mapping upload task when the token is missing.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
build.gradle.kts |
Moves Sentry auth token to env-var provider, hardcodes org, and adds a guard to fail upload tasks when token is missing. |
dependencies.gradle.kts |
Removes Sentry org/project/token keys from the secret.properties-backed Gradle properties export. |
app/build.gradle.kts |
Hardcodes Sentry project slug for the main Android app module. |
automotive/build.gradle.kts |
Hardcodes Sentry project slug for the Automotive module. |
wear/build.gradle.kts |
Hardcodes Sentry project slug for the Wear module. |
| // 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) { |
There was a problem hiding this comment.
Provider.isPresent is true for an env var that is set but empty. That's the most likely way this fails in practice — a secret-injection step that resolves to nothing exports SENTRY_AUTH_TOKEN="" rather than leaving it unset. In that case the guard passes, authToken = "" reaches the plugin, and you get an opaque Sentry auth error mid-upload instead of this message.
The code being replaced deliberately used isNullOrBlank() for exactly this reason. Suggest keeping that semantics:
| if (!sentryAuthToken.isPresent) { | |
| if (sentryAuthToken.orNull.isNullOrBlank()) { |
Worth mirroring on line 480 too, so a blank value never reaches the extension:
authToken = sentryAuthToken.orNull?.takeIf { it.isNotBlank() }There was a problem hiding this comment.
Good catch, adopted in 9d74335 — with one adjustment.
Rather than blank-checking at both sites, I normalise once at the source so the bound value and the guard can't drift:
val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.takeIf { it.isNotBlank() }authToken = sentryAuthToken and if (sentryAuthToken == null) then read the same thing by construction.
One correction to the premise: the code being replaced here didn't use isNullOrBlank() for the token — it was project.findProperty("sentryAuthToken")?.toString(), unchecked. The isNullOrBlank() you're thinking of was in app/build.gradle.kts, guarding a warning println about sentryAndroidProject, and this PR removes it. Doesn't change the conclusion — an empty env var reading as "" rather than absent is exactly the failure mode worth defending against.
Posted by Claude Code (Opus 5) on behalf of @mokagio with approval.
|
|
||
| // 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 { |
There was a problem hiding this comment.
Two small things about the predicate:
-
includeSourceContextis toggled by the same flag but its upload task (sentryUploadSourceBundle*) isn't matched here. It needs the auth token just as much, and task ordering isn't guaranteed, so a token-less CI run may well fail on the source bundle first — with the plugin's error rather than this one. Broadening the match keeps the friendly message authoritative:tasks.matching { it.name.startsWith("uploadSentryProguardMappings") || it.name.startsWith("sentryUploadSourceBundle") }.configureEach { ... } -
Matching on a name prefix couples the guard to a plugin-internal naming convention. If a future
io.sentry.android.gradlebump renames the task, this silently stops matching and the guard quietly disappears — the exact failure it exists to prevent.tasks.withType<SentryUploadProguardMappingsTask>()would break the build at compile time instead. Your call on whether the extra import is worth it; if you keep the string match, a comment pointing at the plugin version it was verified against (6.16.0) would help the next person doing the upgrade.
There was a problem hiding this comment.
Both points valid — fixed in 9d74335 by going one level up the type hierarchy instead of broadening the string match:
tasks.withType<SentryCliExecTask>().configureEach { ... }Checking the 6.16.0 plugin jar: SentryCliExecTask is the public base of every task that shells out to sentry-cli with the auth token — SentryUploadProguardMappingsTask, UploadSourceBundleTask, and the native-symbols/app-artifact tasks. So one type covers both cases you raised, and a future rename is a compile error rather than a silently vanishing guard.
Your first point checks out: sentryUploadSourceBundle* is registered by UploadSourceBundleTask and gated by the same includeSourceContext flag, so the old prefix genuinely missed it.
Two consequences worth noting:
- The message is now "upload debug files to Sentry" rather than "upload ProGuard mappings", since the net is wider. The
-PskipSentryProguardMappingUploadescape hatch stays accurate — in this repo that one flag gates bothincludeProguardMappingandincludeSourceContext. - The extra tasks this now catches are inert here: nothing sets
uploadNativeSymbolsor app-artifact upload, so those tasks are never registered.
Note that CI cannot exercise this guard: every PR-triggered build passes -PskipSentryProguardMappingUpload=true (assemble-release-apk.sh, and the prototype lane), so the upload tasks are never registered and doFirst never runs. The first real exercise is build_bundle on a beta/release build, which passes no skip property. What CI does confirm is that the build scripts compile, so the typed match resolves against the plugin.
Posted by Claude Code (Opus 5) on behalf of @mokagio with approval.
|
|
|
| App Name | ⌚ Wear | |
| Build Type | DebugProd | |
| Build Number | 9445 | |
| Version | 8.18-rc-1 | |
| Application ID | au.com.shiftyjelly.pocketcasts | |
| Commit | e6a053a | |
| Direct Download | pocketcasts-wear-prototype-build-pr5708-e6a053a.apk | |
| Installation URL | 66449fg0usvn8 |
📲 You can test the changes from this Pull Request in 🚗 Automotive by scanning the QR code below to install the corresponding build.
| App Name | 🚗 Automotive | |
| Build Type | DebugProd | |
| Build Number | 9445 | |
| Version | 8.18-rc-1 | |
| Application ID | au.com.shiftyjelly.pocketcasts | |
| Commit | e6a053a | |
| Direct Download | pocketcasts-automotive-prototype-build-pr5708-e6a053a.apk | |
| Installation URL | 66449fg0usvn8 |
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 <noreply@anthropic.com>
|
Claude finished @mokagio's task in 3m 22s —— View job Follow-up review — commit
|
|
Claude finished @mokagio's task in 1m 18s —— View job Follow-up review — commit
|
|
Claude finished @mokagio's task in 3m 8s —— View job Code review — ready-for-review pass (
|
|
Version |
| extensions.getByType(SentryPluginExtension::class.java).apply { | ||
| authToken = project.findProperty("sentryAuthToken")?.toString() | ||
| org = project.findProperty("sentryOrg")?.toString() | ||
| authToken = sentryAuthToken |
There was a problem hiding this comment.
Not sure why this assignment is needed if you already have it above?
You could also keep the fact that it exists or not like val hasSentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").map { it.isNotBlank() }.orElse(false) or similar.
There was a problem hiding this comment.
Adopted the Provider form in 5dad760, but kept the assignment — it's load-bearing.
Checking the 6.16.0 plugin jar: SentryPluginExtension.authToken is objects.property(String::class.java).convention(null as String?). It has no environment default, so without authToken = … the extension property stays absent. SentryCliExecTask then goes the other way — it exports SENTRY_AUTH_TOKEN into the sentry-cli process from that property. And the extension value isn't only used for that: SentryOrgValueSource, SentryTelemetryService, and the source-bundle/native-symbol registration read it too.
So sentry-cli picking the token up "for free" would rely on it happening to be in the Gradle daemon's inherited environment, which is stale whenever the daemon outlives the shell that started it. The explicit assignment makes it deterministic.
Your second point stands on its own though, so I took it:
val sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").filter { it.isNotBlank() }
authToken = sentryAuthToken
// …
if (!sentryAuthToken.isPresent) { throw GradleException(…) }Same single source for the bound value and the guard, but lazy now — a build that never configures Sentry never resolves the variable. I used filter { it.isNotBlank() } rather than map { … }.orElse(false) so the one provider serves both roles; a blank value stays absent instead of reaching the plugin as "".
Verified against the three env states (unset / set-but-empty / set), with configuration cache on: the extension property reads <absent>, <absent>, dummy-token, and the guard throws, throws, passes.
Posted by Claude Code (Opus 5) on behalf of @mokagio with approval.
There was a problem hiding this comment.
Correcting myself — I've reverted the provider refactor in 3ad15ee, so this is back to the String? val you commented on.
The refactor was a local improvement that made this repo the odd one out. The same migration already merged elsewhere with the eager read:
- WordPress-Android#23189 —
WordPress/build.gradle:71,def sentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull?.trim() ?: null, bound withauthToken = sentryAuthTokenand guarded withif (sentryAuthToken == null)onSentryCliExecTask. Same shape as here. - simplenote-android#1850 —
Simplenote/build.gradle:77,authToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").orNull(no guard).
The point of this migration is one pattern across the fleet, so consistency wins over the marginally nicer form here.
On the assignment itself: it is needed. In the 6.16.0 plugin jar, SentryPluginExtension.authToken is objects.property(String::class.java).convention(null as String?) — no environment default. SentryCliExecTask goes the other way, exporting SENTRY_AUTH_TOKEN into the sentry-cli process from that property, and SentryOrgValueSource / SentryTelemetryService read it too. Without the assignment we'd be relying on the token happening to sit in the Gradle daemon's inherited environment, which is stale whenever the daemon outlives the shell that started it.
Why takeIf { it.isNotBlank() } here rather than WordPress-Android's ?.trim() ?: null: that form relies on Groovy truth, where "" ?: null is null. In Kotlin the elvis only fires on null, so a set-but-empty variable would sail through as "". Verified both against the four env states — unset, empty, whitespace, real — and they agree.
Posted by Claude Code (Opus 5) on behalf of @mokagio with approval.
There was a problem hiding this comment.
@iangmaia real Gio here, not a meat proxy.
You could also keep the fact that it exists or not like val hasSentryAuthToken = providers.environmentVariable("SENTRY_AUTH_TOKEN").map { it.isNotBlank() }.orElse(false) or similar.
I updated the implementation in 829a341 and aligned it with the other apps that already do this.
Not sure why this assignment is needed if you already have it above?
My understanding is that the val sentryAuthToken = assigns the token from the env if any making it available to the other steps. The authToken = sentryAuthToken is what passes it to the Sentry plugin itself.
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 <noreply@anthropic.com>
|
Claude finished @mokagio's task in 3m 1s —— View job Follow-up review — commit
|
|
Claude finished @mokagio's task in 2m 12s —— View job Follow-up review — commit
|
`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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-authored-by: Gio Lodi <giovanni.lodi42@gmail.com>
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 <noreply@anthropic.com>
This reverts 5dad760, 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
3ad15ee to
829a341
Compare
|
Claude finished @mokagio's task in 2m 21s —— View job Follow-up review — commit
|
SENTRY_AUTH_TOKEN |
before (takeIf { isNotBlank() }) |
after (trim().ifEmpty { null }) |
|---|---|---|
| unset | null |
null |
"" |
null |
null |
" " |
null |
null |
"sntrys_…\n" |
"sntrys_…\n" → opaque CLI auth error |
"sntrys_…" ✅ |
"sntrys_…" |
unchanged | unchanged |
So the only changed row is the one the commit message targets, and the guard's behaviour on the three empty-ish rows is preserved. A trailing newline is a realistic shape for an injected secret ($(cat secret), a heredoc, some secret-manager CLIs), so this is a real case rather than a theoretical one — and it was the worst one, because it's the only input that passed the guard and then failed deep in the upload, which is precisely what the guard exists to prevent.
?.ifEmpty { null } type-checks, for the record. It resolves to the generic <C, R> C.ifEmpty(() -> R): R where C : CharSequence, C : R — with C = String and the lambda returning Nothing?, R widens to String?, satisfying C : R. Result is String?, same as before, so authToken = sentryAuthToken binds identically. ?.ifBlank { null } would collapse the two calls into one, but after trim() the two forms are indistinguishable and the current spelling reads closer to the sibling repos' ?.trim(). Not worth changing.
The rationale in the commit message about Groovy truth is right. ?.trim() ?: null works in WordPress-Android because Groovy's elvis treats "" as falsy; ported literally to Kotlin the elvis fires only on null, so a set-but-empty variable would bind "". This keeps the fleet's semantics while adapting the spelling to the language — the correct way to be consistent.
One assumption worth naming (not a blocker)
The trim only reaches sentry-cli if the plugin overrides the child process's SENTRY_AUTH_TOKEN from the extension property rather than letting the exec task inherit the agent's raw value. Gradle's Exec environment starts as a copy of the parent env, so a plugin that sets the key wins over inheritance and the trimmed value is what the CLI sees. If instead the plugin only used the property for its own purposes (org resolution, telemetry) and relied on inheritance for the CLI, the untrimmed env var would still reach sentry-cli and this fix would be inert for the newline case — harmless, but not the improvement claimed.
You verified in an earlier round that SentryCliExecTask exports the variable from the property, which is the override case, so this is very likely already settled. Flagging it only because it's the single link that makes this commit do anything, and it's not visible from the diff.
The plugin is now 6.18.0, not 6.16.0
gradle/libs.versions.toml:54 reads sentry-plugin = "6.18.0". That didn't come from this PR — origin/main already has it via the 8.19 release merge (2dc458d) — but it means the jar inspections cited in this PR's discussion (SentryCliExecTask being the public base of the token-using tasks; authToken being objects.property(String).convention(null)) were made against a version the tree no longer resolves. Both facts are very likely stable across a patch bump, and the import io.sentry.android.gradle.tasks.SentryCliExecTask is a compile-time reference, so a rename or removal is a build-script compile failure rather than a silently vanishing guard.
Worth knowing that GitHub Actions doesn't settle that here — the only non-Claude check on this PR is Validate Gradle Wrapper. Buildkite compiling the build scripts is the actual confirmation. Also note authToken's convention(null) is the fact that makes the authToken = sentryAuthToken assignment load-bearing (per your reply to @iangmaia); if a future bump gave it an environment convention, the assignment would become redundant rather than wrong.
Re-verified on 829a341
- Single source preserved.
authToken(:481) and the guard (:499) still read the sameval, so they can't drift — the fix from the first review round survives its third rewrite, which is the property that matters most here. - Guard scope unchanged.
tasks.withType<SentryCliExecTask>().configureEach(:497) coverssentryUploadSourceBundle*alongside the ProGuard mappings;configureEachkeeps it lazy, and thedoFirstlambda captures only aString?local, so nothing from the script instance is dragged into the serialised task graph. - No stragglers. Grepping the tree for
sentryOrg/sentryAuthToken/sentry*Projectreturns only the fourSENTRY_AUTH_TOKENlines inbuild.gradle.kts(:478,:481,:499,:501). No.buildkite/script, Fastlane lane,gradle.propertiesdefault or doc references the removed keys. - DSNs untouched, per the stated tradeoff —
dependencies.gradle.ktsstill resolvessentryAndroidDsn/-Automotive/-Wear/-Tvfromsecret.properties, per-app fallback intact. - Escape hatch real and in use —
.buildkite/commands/assemble-release-apk.sh:19andfastlane/Fastfile:647both pass-PskipSentryProguardMappingUpload=true. - Style. Line
:478is 109 chars, under the 120 limit. The only over-length lines inbuild.gradle.ktsremain the pre-existingbuildConfigFieldcalls at:287/:289. - Config cache. Unchanged from
3ad15ee: the token resolves at configuration time and lands in.gradle/configuration-cache/as a literal, which matters only if that directory is ever archived or shared across jobs. The trim doesn't affect input tracking —providers.environmentVariablestill registers the read.
Sandbox limits, unchanged: Gradle can't execute and there are no resolved dependencies on disk, so I can't run spotlessCheck or inspect the 6.18.0 plugin jar. The length and grep checks above are manual reads, not the gates themselves.
Still open (operational, unchanged from prior rounds)
- Diff
org = "a8c"and the three project slugs character-for-character against the currentmobile-secretsvalues before the deletion follow-up lands. A typo surfaces only as a 404 at the upload step of a real release build, and afterwards the old values are gone. SENTRY_AUTH_TOKENon the agents stays load-bearing and unexercised by PR CI — every PR-triggered build passes the skip flag, so no upload task is registered and the guard never runs. First real exercise isbuild_bundleon a beta/release build;CI=true ./gradlew :app:assembleRelease --dry-runwould confirm whichSentryCliExecTasksubtypes get scheduled. Keep the skip flag handy for that first run: the failure mode is now a hard failure after the bundle is built, rather than aprintlnand a release shipped without mappings.mobile-secretsmay still hold an orphanedandroid/pocket-casts/sentry.properties— never referenced by.configure, so worth sweeping in the same follow-up.
Review only — no code changes made or pushed.
· Branch: ainfra-2790-remove-sentryproperties-in-favor-of-public-values-and-env
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 <noreply@anthropic.com>
|
Claude finished @mokagio's task in 1m 12s —— View job Follow-up review — commit
|
SENTRY_AUTH_TOKEN |
sentryAuthToken |
old message | new message |
|---|---|---|---|
| unset | null |
accurate | accurate |
"" |
null |
"is not set" — misleading | accurate |
" " |
null |
"is not set" — misleading | accurate |
The middle two rows are the realistic CI failures — a secret-injection step that resolves to nothing exports the variable empty rather than leaving it unset. Someone debugging that would have run echo $SENTRY_AUTH_TOKEN, seen a line of output, and concluded the message was wrong about their environment. Now it points at the actual cause.
It also partly compensates for what eaac1b3 removed: with the explanatory comment above line 478 gone, this message is the only place in the repo that records blank-is-treated-as-unset. Not a substitute for a code comment — a reader of ?.trim()?.ifEmpty { null } won't see the string 23 lines below — but the semantics are at least written down somewhere now.
Line 501 lands at 116 chars, under the 120 limit; the only over-length lines in the file remain the pre-existing buildConfigField calls at :287/:289.
Re-verified on f981a32
- Single source intact.
authToken(build.gradle.kts:481) and the guard (:499) still read the samevalfrom:478, so they can't drift. That's the fix from the first review round, now surviving four rewrites of the surrounding expression. - Guard scope unchanged.
tasks.withType<SentryCliExecTask>().configureEach(:497) coverssentryUploadSourceBundle*alongside the ProGuard mappings;configureEachkeeps it lazy, and thedoFirstlambda captures only aString?local. - No stragglers. Grepping the tree for
sentryOrg/sentryAuthToken/sentry*Projectreturns only the fourSENTRY_AUTH_TOKENlines inbuild.gradle.kts(:478,:481,:499,:501). - Escape hatch named in the message is real and in use —
.buildkite/commands/assemble-release-apk.sh:19andfastlane/Fastfile:647both pass-PskipSentryProguardMappingUpload=true. Worth noting the message names the ProGuard-specific flag while the guard now covers source-bundle uploads too; that's still correct in this repo, since that one flag gates bothincludeProguardMappingandincludeSourceContext(:486–:487). - DSNs untouched, per the stated tradeoff —
dependencies.gradle.ktsstill resolvessentryAndroidDsn/-Automotive/-Wear/-Tvfromsecret.properties. - Diff is still 6 files, +19/−24, matching the description.
- Plugin is
6.18.0(gradle/libs.versions.toml:54), not the6.16.0the jar inspections in this thread were made against — that came frommainvia the 8.19 merge, not from this PR. Both facts cited (SentryCliExecTaskas the public base;authTokenhaving anullconvention) are very likely stable across a patch bump, and theimportmakes a rename a script-compile error rather than a silent no-op.
Sandbox limits, unchanged: Gradle can't execute and there are no resolved dependencies on disk, so I can't run spotlessCheck or inspect the 6.18.0 plugin jar. The length and grep checks above are manual reads, not the gates themselves. Note the only non-Claude check on this PR is Validate Gradle Wrapper — Buildkite compiling the build scripts is the real confirmation that the typed match resolves.
Still open (operational, unchanged from prior rounds)
- Diff
org = "a8c"and the three project slugs character-for-character against the currentmobile-secretsvalues before the deletion follow-up lands. A typo surfaces only as a 404 at the upload step of a real release build, and afterwards the old values are gone. SENTRY_AUTH_TOKENon the agents stays load-bearing and unexercised by PR CI — every PR-triggered build passes the skip flag, so no upload task is registered and this message can't be reached. First real exercise isbuild_bundleon a beta/release build;CI=true ./gradlew :app:assembleRelease --dry-runwould confirm whichSentryCliExecTasksubtypes get scheduled. Keep the skip flag handy for that first run — the failure mode is now a hard failure after the bundle is built, rather than aprintlnand a release shipped without mappings.mobile-secretsmay still hold an orphanedandroid/pocket-casts/sentry.properties— never referenced by.configure, so worth sweeping in the same follow-up that deletes thesecret.propertieskeys.
Review only — no code changes made or pushed.
· Branch: ainfra-2790-remove-sentryproperties-in-favor-of-public-values-and-env

See https://linear.app/a8c/issue/AINFRA-2790. Also, wordpress-mobile/WordPress-Android#23189 for a similar implementation.
sentry.properties..propertiesto env var, so that it can be kept off any machine that doesn't need it (i.e., ideally CI only)Notice the pinned
mobile-secretscheckout has not changed, that's because the updates here only stop reading properties fromsecrets.properties, they don't need new ones. So, I thought I'd keep the diff smaller.AI-generated details
Rationale
sentryOrg, the per-variantsentry*Projectslugs, andsentryAuthTokenaren't secrets: the org/project slugs are Sentry-public identifiers, and the auth token is only needed by CI to upload ProGuard mappings. Moving them out ofsecret.propertiesshrinks whatmobile-secretsneeds to hold for this repo, matching the pattern already shipped in simplenote-android#1850 and in review in dayone-android.Intentional tradeoff
Unlike those sibling repos, the Sentry DSN stays in
secret.propertiesunchanged. Pocket Casts Android is open source and hasn't otherwise adopteda8c-secretsyet, so per the paaHJt-ahO-p2 RFC the DSN can't become a public repo constant the way it did there.Gotchas
SENTRY_AUTH_TOKENis already available as an env var on this repo's Buildkite agents (it needed no pipeline changes for dayone-android). Watching the release-build CI job's mapping upload step to confirm.mobile-secretschange (not included here, applied separately): deletesentryOrg,sentryAuthToken,sentryAndroidProject,sentryAutomotiveProject,sentryWearProject,sentryTvProjectfrom thepocket-casts-androidsecret.propertiesentry. The DSN keys stay.How to test
./gradlew help(or any task) evaluates all Gradle scripts cleanly;./gradlew spotlessCheckpasses.