Skip to content

Support Firebase SDK auto-initialization for Cloud Run services - #11208

Open
falahat wants to merge 10 commits into
bapi_local_buildsfrom
bapi_sdk_autoinit
Open

falahat wants to merge 10 commits into
bapi_local_buildsfrom
bapi_sdk_autoinit

Conversation

@falahat

@falahat falahat commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds Firebase Web App linking and SDK Auto-Initialization support (firebase.google.com/app-id service annotation):

  • Prompts to link or create a Firebase Web App during firebase init run.
  • Adds --app <appId> and --clear-app flags to firebase run:services:update <serviceId>.
  • During prepare/deploy, fetches the linked Web App's config via managementApps.getAppConfig, passes FIREBASE_CONFIG and FIREBASE_WEBAPP_CONFIG into the build environment (respecting user overrides), sets FIREBASE_CONFIG on the container's runtime environment, and removes FIREBASE_CONFIG and firebase.google.com/app-id when --clear-app is used.
  • Checks whether the service's runtime service account (template.serviceAccount or the project's default Compute Engine service account) has roles/firebase.sdkAdminServiceAgent, and if missing, logs a clear warning naming the service account and grants roles/firebase.sdkAdminServiceAgent automatically.

Scenarios Tested

  • Unit tests (src/deploy/run/prepare.spec.ts, src/deploy/run/deploy.spec.ts, src/deploy/run/update.spec.ts, src/init/features/run.spec.ts):
    • Sticky firebase.google.com/app-id inheritance across deploys, setting a new --app, clearing with --clear-app, user env var override precedence (buildConfig.environmentVariablesAndSecrets and container env), hard error when setting an invalid --app ID vs. warning when an existing service's linked app cannot be fetched, and checking/granting roles/firebase.sdkAdminServiceAgent on default and custom runtime service accounts (including graceful 403 warning).
  • Manual CLI & cloud-diff testing (scripts/run-deploy-tests/tests.ts):
    • Tested firebase run:services:update fbp-next15-crff-src --app <appId> and verified /sdk-autoinit renders FIREBASE_CONFIG: {"storageBucket":"<project>.firebasestorage.app","projectId":"<project>"}, Status: Connected, and the live Firebase Storage getDownloadURL on both local-build (fbp-next15-crff-lfbp) and remote-build (fbp-next15-crff-src) services.
    • Tested firebase run:services:update fbp-next15-crff-lfbp --app nonexistent-app-id and verified it fails fast with Unable to lookup details for Firebase Web App nonexistent-app-id.
    • Tested firebase run:services:update <serviceId> --clear-app and verified both the firebase.google.com/app-id annotation and FIREBASE_CONFIG runtime env var are removed.

Sample Commands

firebase run:services:update my-service --app <appId>
firebase run:services:update my-service --clear-app
npx mocha src/deploy/run/*.spec.ts src/init/features/run.spec.ts

@falahat

falahat commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@falahat
falahat added this pull request to stack #11209 September 30, 2026 04:19

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }.

Comment thread src/init/features/run.ts
Comment thread src/deploy/run/update.ts
Comment on lines +56 to +60
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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;
}

Comment thread src/deploy/run/prepare.ts Outdated
Comment thread src/deploy/run/deploy.ts Outdated
Comment thread src/deploy/run/prepare.ts Outdated
baseImage,
appId,
...(autoInitEnv?.FIREBASE_CONFIG && { firebaseConfig: autoInitEnv.FIREBASE_CONFIG }),
...(Object.keys(buildEnv).length && { buildEnv }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
...(Object.keys(buildEnv).length && { buildEnv }),
...(Object.keys(buildEnv).length > 0 ? { buildEnv } : {}),

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/init/features/run.ts
Comment thread src/deploy/run/prepare.ts Outdated
@falahat

falahat commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/deploy/run/prepare.ts
Comment on lines +161 to +164
const serviceAccount =
existing?.template?.serviceAccount ||
(await getDefaultServiceAccount(await getProjectNumber(context)));
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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 }),
      );
    }

Comment thread src/deploy/run/prepare.ts
@@ -1,17 +1,27 @@
import { FirebaseError } from "../../error";
import { getAutoinitEnvVars } from "../../apphosting/utils";
import { FirebaseError, getErrStatus } from "../../error";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import the getError utility from ../../error to safely convert unknown error types into standard Error instances.

Suggested change
import { FirebaseError, getErrStatus } from "../../error";
import { FirebaseError, getErrStatus, getError } from "../../error";

Comment thread src/deploy/run/prepare.ts
if (requireValidApp) {
throw new FirebaseError(
`Unable to lookup details for Firebase Web App ${appId} on service ${serviceId}.`,
{ original: err instanceof Error ? err : undefined },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
{ original: err instanceof Error ? err : undefined },
{ original: getError(err) },

Comment thread src/deploy/run/args.ts
Comment on lines +10 to +11
/** Runtime FIREBASE_CONFIG JSON for the linked Firebase Web App. */
firebaseConfig?: string;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread src/deploy/run/deploy.ts
Comment on lines +144 to +152
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;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

investigate more why FIREBASE_CONFIG would be a secret. If so, shouldn't we always make it a secret?

Comment on lines +266 to +286
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;
});
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, if autoinit fails, we should fail clearly and NOT do a half-correct deployment.

Comment thread src/deploy/run/prepare.ts
Comment on lines +111 to +151
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;
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is pretty hard to read, overly indented, and has complex nested if/else logic.

Comment thread src/deploy/run/prepare.ts
if (getErrStatus(err) === 403) {
logLabeledWarning(
"run",
`Failed to grant ${ADMIN_SDK_ROLE} to ${serviceAccount}. Make sure you have the resourcemanager.projects.setIamPolicy permission.`,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add extra text "or as an admin to grant this role."

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant