Repository navigation
feat(google-services-plugin): inject Firebase App Check debug token in debug builds - #481
Conversation
rlazo
left a comment
There was a problem hiding this comment.
Thanks for the PR! Overall the approach looks solid (lazy providers, task inputs declared, release builds skipped). A few comments inline around cache behavior, debug detection, and config-file resolution.
| it.applicationId.set(packageName) | ||
| it.gmpAppId.set(project.layout.buildDirectory.file("gmpAppId/${variant.name}.txt")) | ||
| it.debugBuild.set(isDebug) | ||
| it.appCheckConfigFiles.set(getAppCheckConfigFiles(project)) |
There was a problem hiding this comment.
[MEDIUM] appCheckConfigFiles is wired as a task input for every variant, including release.
Since the task is @CacheableTask, declaring ~/.config/configstore/firebase-appcheck.json as an @InputFiles for release variants means (a) any token refresh by the Firebase CLI invalidates up-to-date checks for release processXGoogleServices tasks, and (b) release build-cache keys now differ per developer (file content hash) even though the file has zero effect on release output, killing remote cache hits. Only wire the files when the variant is debug.
| it.appCheckConfigFiles.set(getAppCheckConfigFiles(project)) | |
| if (isDebug) { | |
| it.appCheckConfigFiles.set(getAppCheckConfigFiles(project)) | |
| } |
There was a problem hiding this comment.
Done in 653b280 — gated appCheckConfigFiles registration on if (isDebug) so release task inputs and cache keys are unaffected.
| val isDebug = | ||
| buildType.equals("debug", ignoreCase = true) || | ||
| variant.name.endsWith("Debug", ignoreCase = true) |
There was a problem hiding this comment.
[MEDIUM] Debug detection relies on naming heuristics rather than the build type's isDebuggable flag.
A custom build type such as staging { initWith(debug) } (debuggable, commonly used for App Check testing) is skipped, while a non-debuggable build type that merely ends in Debug is matched and gets a token packaged. For application modules the DSL is finalized by onVariants, so the real flag is available; the name heuristic could remain as a fallback for module types whose BuildType DSL lacks isDebuggable.
| val isDebug = | |
| buildType.equals("debug", ignoreCase = true) || | |
| variant.name.endsWith("Debug", ignoreCase = true) | |
| val isDebug = | |
| project.extensions | |
| .findByType(ApplicationExtension::class.java) | |
| ?.buildTypes | |
| ?.findByName(buildType) | |
| ?.isDebuggable | |
| ?: (buildType.equals("debug", ignoreCase = true) || | |
| variant.name.endsWith("Debug", ignoreCase = true)) |
There was a problem hiding this comment.
Done in 653b280 — now checks ApplicationExtension.buildTypes.findByName(buildType)?.isDebuggable first with the name heuristic as a fallback for non-application modules.
| project.providers.gradleProperty(APPCHECK_CONFIG_FILE_PROPERTY).orNull | ||
| ?: project.providers.systemProperty(APPCHECK_CONFIG_FILE_PROPERTY).orNull | ||
| if (!overridePath.isNullOrBlank()) { | ||
| return listOf(File(overridePath)) |
There was a problem hiding this comment.
[MEDIUM] A relative -Pfirebase.appcheck.configFile path is resolved against the daemon's working directory.
File(overridePath) with a relative path resolves against the JVM CWD, which for a Gradle daemon is not the project directory, so e.g. -Pfirebase.appcheck.configFile=.secrets/appcheck.json silently finds nothing.
| return listOf(File(overridePath)) | |
| return listOf(project.file(overridePath)) |
There was a problem hiding this comment.
Done in 653b280 — switched to project.file(overridePath).
| val userHome = project.providers.systemProperty("user.home").orNull | ||
| if (!userHome.isNullOrBlank()) { | ||
| files.add(File(userHome, ".config/configstore/$APPCHECK_CONFIG_FILE_NAME")) | ||
| } | ||
| val xdgConfigHome = project.providers.environmentVariable("XDG_CONFIG_HOME").orNull | ||
| if (!xdgConfigHome.isNullOrBlank()) { | ||
| val xdgFile = File(xdgConfigHome, "configstore/$APPCHECK_CONFIG_FILE_NAME") | ||
| if (!files.contains(xdgFile)) { | ||
| files.add(xdgFile) | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] ~/.config is searched before $XDG_CONFIG_HOME, inverting the Firebase CLI's precedence.
The CLI's configstore uses XDG_CONFIG_HOME instead of ~/.config when it is set. Because the loop in resolveAppCheckDebugToken returns on the first match, a stale ~/.config/configstore/firebase-appcheck.json will shadow the live file the CLI is actually writing to. Add the XDG location first.
| val userHome = project.providers.systemProperty("user.home").orNull | |
| if (!userHome.isNullOrBlank()) { | |
| files.add(File(userHome, ".config/configstore/$APPCHECK_CONFIG_FILE_NAME")) | |
| } | |
| val xdgConfigHome = project.providers.environmentVariable("XDG_CONFIG_HOME").orNull | |
| if (!xdgConfigHome.isNullOrBlank()) { | |
| val xdgFile = File(xdgConfigHome, "configstore/$APPCHECK_CONFIG_FILE_NAME") | |
| if (!files.contains(xdgFile)) { | |
| files.add(xdgFile) | |
| } | |
| } | |
| val xdgConfigHome = project.providers.environmentVariable("XDG_CONFIG_HOME").orNull | |
| if (!xdgConfigHome.isNullOrBlank()) { | |
| files.add(File(xdgConfigHome, "configstore/$APPCHECK_CONFIG_FILE_NAME")) | |
| } | |
| val userHome = project.providers.systemProperty("user.home").orNull | |
| if (!userHome.isNullOrBlank()) { | |
| val homeFile = File(userHome, ".config/configstore/$APPCHECK_CONFIG_FILE_NAME") | |
| if (!files.contains(homeFile)) { | |
| files.add(homeFile) | |
| } | |
| } |
There was a problem hiding this comment.
Actually I think this is intended, isn't it @AustinBenoit ?
There was a problem hiding this comment.
Clarification on this one (updated in def72a0 with an inline comment): rather than using default configstore path resolution (where XDG_CONFIG_HOME replaces ~/.config), the CLI implementation for this feature (go/firebase-api-sdk-cli-seamless-debug-tokens) explicitly passes $HOME/.config/configstore/firebase-appcheck.json as the canonical primary configPath and mirrors writes to $XDG_CONFIG_HOME/configstore/firebase-appcheck.json when XDG_CONFIG_HOME is set. That way, if a developer sets XDG_CONFIG_HOME only in their interactive shell (e.g. .zshrc) and not in their GUI Android Studio / Gradle daemon environment (or later unsets XDG_CONFIG_HOME), ~/.config/configstore/firebase-appcheck.json is always guaranteed to be the up-to-date primary file.
| continue | ||
| } | ||
| try { | ||
| val root = JsonParser().parse(configFile.bufferedReader(Charsets.UTF_8)) |
There was a problem hiding this comment.
[MEDIUM] The BufferedReader opened on the config file is never closed.
JsonParser.parse(Reader) does not close the reader, so the file handle leaks until GC (and on Windows can keep the configstore file locked while the CLI tries to rewrite it).
| val root = JsonParser().parse(configFile.bufferedReader(Charsets.UTF_8)) | |
| val root = configFile.bufferedReader(Charsets.UTF_8).use { JsonParser().parse(it) } |
There was a problem hiding this comment.
Done in 653b280 — wrapped bufferedReader in .use { ... }.
|
|
||
| @get:OutputFile abstract val gmpAppId: RegularFileProperty | ||
|
|
||
| @get:Optional @get:Input abstract val debugBuild: Property<Boolean> |
There was a problem hiding this comment.
[MEDIUM] Debug tokens are written into the outputs of a @CacheableTask, so they get pushed to any configured remote build cache.
The generated values.xml containing the per-developer secret becomes a build-cache artifact. The cache key includes the config file hash so cross-developer hits are unlikely, but the token is still persisted in shared infrastructure the CLI explicitly tells users not to commit. Consider disabling caching of this task when a token is actually injected, e.g. in the plugin's task registration:
it.outputs.doNotCacheIf("Firebase App Check debug token is injected") { task ->
(task as GoogleServicesTask).debugBuild.getOrElse(false) &&
task.appCheckConfigFiles.getOrElse(emptyList()).any { f -> f.isFile }
}There was a problem hiding this comment.
Done in 653b280 — added outputs.doNotCacheIf when debugBuild is true and a local config file is present.
| val tokenPrimitive = tokensObj.getAsJsonPrimitive(googleAppId) ?: continue | ||
| if (tokenPrimitive.isString) { | ||
| val token = tokenPrimitive.asString.trim() | ||
| if (token.isNotEmpty() && !token.any { it == '<' || it == '>' || it == '&' || it == '"' }) { |
There was a problem hiding this comment.
[LOW] XML sanitization misses characters that AAPT treats specially in string resources.
<, >, &, " are rejected, but a token starting with @ or ? is interpreted as a resource reference, and an unescaped ' fails aapt2 compilation. Either escape the value properly or extend the reject list, e.g. token.none { it in "<>&\"'" } && !token.startsWith("@") && !token.startsWith("?").
There was a problem hiding this comment.
Done in 653b280 — extended the reject check to exclude ' and leading @ / ?, and added unit test coverage in GoogleServicesPluginTest.
…ebug detection, and config resolution
Summary
Implements the
com.google.gms.google-servicesGradle plugin build-time injection for Firebase App Check Seamless Debug Tokens (go/firebase-api-sdk-cli-seamless-debug-tokens):buildType == "debug"or variant name ending with"Debug"),GoogleServicesTaskchecks~/.config/configstore/firebase-appcheck.json(and$XDG_CONFIG_HOME/configstore/firebase-appcheck.json, or-Pfirebase.appcheck.configFileoverride) for a debug token matching the variant'smobilesdk_app_id.<string name="firebase_app_check_debug_token" translatable="false">...</string>into the generatedvalues/values.xmlresource file consumed byfirebase-appcheck-debugat runtime.ProviderFactory(project.providers) and@PathSensitive(PathSensitivity.RELATIVE) @InputFilesfor full Gradle Configuration Cache and incremental build compatibility.Testing
GoogleServicesPluginTest:appCheck debug token injected in debug build and omitted in release buildmalformed appCheck config file is ignored gracefully