Conversation
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for linking and clearing a Firebase Web App associated with a Cloud Run service to enable SDK auto-initialization. It adds --app and --clear-app options to the update command, automatically links web apps during initialization, and injects the configuration into environment variables. Feedback on these changes includes wrapping getOrCreateWebApp in a try-catch block to prevent crashes from permission errors, skipping redundant redeployments when the app ID is already linked, using hasOwnProperty instead of the in operator to avoid prototype pollution, and replacing numeric length checks in object spreads to prevent evaluating to { ...0 }.
| let updateApp = Boolean(newAppId || clearApp); | ||
| if (clearApp && !existing.annotations?.[FIREBASE_APP_ANNOTATION]) { | ||
| logBullet(`Service ${clc.bold(serviceId)} does not have a linked Firebase Web App.`); | ||
| updateApp = false; | ||
| } |
There was a problem hiding this comment.
If the user provides a --app <appId> that is already linked to the service, we will still trigger a full redeployment (which involves a slow build and deploy process). We should check if the new app ID is already set on the service and skip the update if so.
| let updateApp = Boolean(newAppId || clearApp); | |
| if (clearApp && !existing.annotations?.[FIREBASE_APP_ANNOTATION]) { | |
| logBullet(`Service ${clc.bold(serviceId)} does not have a linked Firebase Web App.`); | |
| updateApp = false; | |
| } | |
| let updateApp = Boolean(newAppId || clearApp); | |
| if (newAppId && newAppId === existing.annotations?.[FIREBASE_APP_ANNOTATION]) { | |
| logBullet("Service " + clc.bold(serviceId) + " is already linked to Firebase Web App " + newAppId + "."); | |
| updateApp = false; | |
| } else if (clearApp && !existing.annotations?.[FIREBASE_APP_ANNOTATION]) { | |
| logBullet("Service " + clc.bold(serviceId) + " does not have a linked Firebase Web App."); | |
| updateApp = false; | |
| } |
| baseImage, | ||
| appId, | ||
| ...(autoInitEnv?.FIREBASE_CONFIG && { firebaseConfig: autoInitEnv.FIREBASE_CONFIG }), | ||
| ...(Object.keys(buildEnv).length && { buildEnv }), |
There was a problem hiding this comment.
Using && with a numeric length inside an object spread can evaluate to 0, resulting in { ...0 }. It is cleaner and more type-safe to use a ternary operator or a boolean check.
| ...(Object.keys(buildEnv).length && { buildEnv }), | |
| ...(Object.keys(buildEnv).length > 0 ? { buildEnv } : {}), |
b574dd5 to
25b3c53
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces the ability to link and clear a Firebase Web App for Cloud Run services, enabling Firebase SDK auto-initialization. It adds --app and --clear-app options to the run:services:update command, updates firebase init run to automatically associate a Web App, and injects the corresponding FIREBASE_CONFIG and FIREBASE_WEBAPP_CONFIG environment variables during preparation and deployment. The review feedback highlights two important improvements: first, wrapping the getOrCreateWebApp call in firebase init run with a try-catch block to prevent crashes on network or permission failures; second, throwing a FirebaseError if a newly requested Web App lookup fails during deployment, ensuring the service is not left in a partially configured state.
# Conflicts: # src/deploy/run/deploy.ts
…t for SDK autoinit
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds support for linking and clearing a Firebase Web App on Cloud Run services to enable SDK auto-initialization, updating the CLI commands, deployment pipeline, and initialization flows accordingly. The feedback recommends moving the resolution of the default service account and project number inside the try-catch block to prevent deployment crashes, correcting the arguments passed to getProjectNumber, and utilizing the getError utility instead of manual type checks to safely handle unknown error types.
| const serviceAccount = | ||
| existing?.template?.serviceAccount || | ||
| (await getDefaultServiceAccount(await getProjectNumber(context))); | ||
| try { |
There was a problem hiding this comment.
The resolution of the default service account and project number is currently performed outside of the try-catch block. If getProjectNumber or getDefaultServiceAccount fails (e.g., due to missing permissions or APIs not being enabled), the entire deployment will crash. Moving this logic inside the try block ensures that such failures are handled gracefully.
Additionally, getProjectNumber in firebase-tools typically expects an options object containing a project property (e.g., { project: string }). Passing context directly (which only has projectId) is highly likely to fail in production, which is currently masked in unit tests because getProjectNumber is stubbed.
let serviceAccount = existing?.template?.serviceAccount;
try {
if (!serviceAccount) {
serviceAccount = await getDefaultServiceAccount(
await getProjectNumber({ project: context.projectId }),
);
}| @@ -1,17 +1,27 @@ | |||
| import { FirebaseError } from "../../error"; | |||
| import { getAutoinitEnvVars } from "../../apphosting/utils"; | |||
| import { FirebaseError, getErrStatus } from "../../error"; | |||
| if (requireValidApp) { | ||
| throw new FirebaseError( | ||
| `Unable to lookup details for Firebase Web App ${appId} on service ${serviceId}.`, | ||
| { original: err instanceof Error ? err : undefined }, |
There was a problem hiding this comment.
Use the getError utility instead of a manual instanceof Error ternary check. This ensures that any non-standard error objects (such as raw API response objects) are safely converted to standard Error instances while preserving their error messages.
| { original: err instanceof Error ? err : undefined }, | |
| { original: getError(err) }, |
| /** Runtime FIREBASE_CONFIG JSON for the linked Firebase Web App. */ | ||
| firebaseConfig?: string; |
There was a problem hiding this comment.
Confirm exactly why we need to pass in firebaseConfig here when we are also passing in appId. Also if we are passing in the fireabaseConfig, why are we not passing the adminConfig?
| if (svc.firebaseConfig) { | ||
| const env = (container.env || []).filter((e) => e.name !== "FIREBASE_CONFIG"); | ||
| container.env = [...env, { name: "FIREBASE_CONFIG", value: svc.firebaseConfig }]; | ||
| } else if (context.appId === null && container.env) { | ||
| container.env = container.env.filter((e) => e.name !== "FIREBASE_CONFIG"); | ||
| if (!container.env.length) { | ||
| delete container.env; | ||
| } | ||
| } |
There was a problem hiding this comment.
Again, confirm why we allow overriding FIREBASE_CONFIG but not the admin sdk envvars.
| }); | ||
| }); | ||
|
|
||
| it("preserves secret-backed FIREBASE_CONFIG on the container without overwriting it", async () => { |
There was a problem hiding this comment.
investigate more why FIREBASE_CONFIG would be a secret. If so, shouldn't we always make it a secret?
| it("warns and continues without autoinit if getAppConfig fails for an existing annotation", async () => { | ||
| getServiceStub.resolves({ | ||
| ...existing, | ||
| annotations: { [FIREBASE_APP_ANNOTATION]: "1:1:web:a" }, | ||
| }); | ||
| getAppConfigStub.rejects(new Error("boom")); | ||
| const svc = await prepareOne(); | ||
| expect(svc.appId).to.equal("1:1:web:a"); | ||
| expect(svc.firebaseConfig).to.be.undefined; | ||
| expect(svc.buildEnv).to.be.undefined; | ||
| expect(hasRolesStub).not.to.have.been.called; | ||
| }); | ||
|
|
||
| it("throws if getAppConfig fails when explicitly setting a new appId", async () => { | ||
| getAppConfigStub.rejects(new Error("boom")); | ||
| await expect(prepareOne({}, { appId: "bad-app" })).to.be.rejectedWith( | ||
| "Unable to lookup details for Firebase Web App bad-app on service s.", | ||
| ); | ||
| expect(hasRolesStub).not.to.have.been.called; | ||
| }); | ||
| }); |
There was a problem hiding this comment.
nope, if autoinit fails, we should fail clearly and NOT do a half-correct deployment.
| async function resolveAutoInitEnv( | ||
| serviceId: string, | ||
| appId: string | undefined, | ||
| existing: runv2.Service | undefined, | ||
| requireValidApp: boolean, | ||
| ): Promise<Record<string, string> | undefined> { | ||
| if (!appId) { | ||
| return undefined; | ||
| } | ||
| try { | ||
| const webappConfig = (await managementApps.getAppConfig( | ||
| appId, | ||
| managementApps.AppPlatform.WEB, | ||
| )) as WebConfig; | ||
| const autoinitVars = getAutoinitEnvVars(webappConfig); | ||
| if (appId === existing?.annotations?.[FIREBASE_APP_ANNOTATION]) { | ||
| for (const env of mainContainer(existing?.template)?.env || []) { | ||
| if (Object.prototype.hasOwnProperty.call(autoinitVars, env.name)) { | ||
| if ("value" in env && env.value !== undefined) { | ||
| autoinitVars[env.name] = env.value; | ||
| } else { | ||
| delete autoinitVars[env.name]; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return Object.keys(autoinitVars).length ? autoinitVars : undefined; | ||
| } catch (err: unknown) { | ||
| if (requireValidApp) { | ||
| throw new FirebaseError( | ||
| `Unable to lookup details for Firebase Web App ${appId} on service ${serviceId}.`, | ||
| { original: err instanceof Error ? err : undefined }, | ||
| ); | ||
| } | ||
| logLabeledWarning( | ||
| "run", | ||
| `Unable to lookup details for Firebase Web App ${appId} on service ${serviceId}. Firebase SDK autoinit will not be available.`, | ||
| ); | ||
| return undefined; | ||
| } | ||
| } |
There was a problem hiding this comment.
This code is pretty hard to read, overly indented, and has complex nested if/else logic.
| if (getErrStatus(err) === 403) { | ||
| logLabeledWarning( | ||
| "run", | ||
| `Failed to grant ${ADMIN_SDK_ROLE} to ${serviceAccount}. Make sure you have the resourcemanager.projects.setIamPolicy permission.`, |
There was a problem hiding this comment.
add extra text "or as an admin to grant this role."
Description
Adds Firebase Web App linking and SDK Auto-Initialization support (
firebase.google.com/app-idservice annotation):firebase init run.--app <appId>and--clear-appflags tofirebase run:services:update <serviceId>.prepare/deploy, fetches the linked Web App's config viamanagementApps.getAppConfig, passesFIREBASE_CONFIGandFIREBASE_WEBAPP_CONFIGinto the build environment (respecting user overrides), setsFIREBASE_CONFIGon the container's runtime environment, and removesFIREBASE_CONFIGandfirebase.google.com/app-idwhen--clear-appis used.template.serviceAccountor the project's default Compute Engine service account) hasroles/firebase.sdkAdminServiceAgent, and if missing, logs a clear warning naming the service account and grantsroles/firebase.sdkAdminServiceAgentautomatically.Scenarios Tested
src/deploy/run/prepare.spec.ts,src/deploy/run/deploy.spec.ts,src/deploy/run/update.spec.ts,src/init/features/run.spec.ts):firebase.google.com/app-idinheritance across deploys, setting a new--app, clearing with--clear-app, user env var override precedence (buildConfig.environmentVariablesAndSecretsand containerenv), hard error when setting an invalid--appID vs. warning when an existing service's linked app cannot be fetched, and checking/grantingroles/firebase.sdkAdminServiceAgenton default and custom runtime service accounts (including graceful 403 warning).cloud-difftesting (scripts/run-deploy-tests/tests.ts):firebase run:services:update fbp-next15-crff-src --app <appId>and verified/sdk-autoinitrendersFIREBASE_CONFIG: {"storageBucket":"<project>.firebasestorage.app","projectId":"<project>"},Status: Connected, and the live Firebase StoragegetDownloadURLon both local-build (fbp-next15-crff-lfbp) and remote-build (fbp-next15-crff-src) services.firebase run:services:update fbp-next15-crff-lfbp --app nonexistent-app-idand verified it fails fast withUnable to lookup details for Firebase Web App nonexistent-app-id.firebase run:services:update <serviceId> --clear-appand verified both thefirebase.google.com/app-idannotation andFIREBASE_CONFIGruntime env var are removed.Sample Commands