Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
- Add GCP API client functions to support App Hosting deploy from source feature. (#8545)
- Changed firebase init template for functions to pin runtime version on init. (#8553)
- Fix an issue where updating a Cloud Function that retires would add incorrect fields to the updateMask. (#8560)
- Provision App Hosting compute service account during init flow. (#8580)
23 changes: 15 additions & 8 deletions src/apphosting/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
// SSL.
const maybeNodeError = err as { cause: { code: string }; code: string };
if (
/HANDSHAKE_FAILURE/.test(maybeNodeError?.cause?.code) ||

Check warning on line 52 in src/apphosting/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Use `String#includes()` method with a string instead
"EPROTO" === maybeNodeError?.code
) {
return false;
Expand All @@ -76,14 +76,7 @@
webAppName: string | null,
serviceAccount: string | null,
): Promise<void> {
await Promise.all([
ensure(projectId, developerConnectOrigin(), "apphosting", true),
ensure(projectId, cloudbuildOrigin(), "apphosting", true),
ensure(projectId, secretManagerOrigin(), "apphosting", true),
ensure(projectId, cloudRunApiOrigin(), "apphosting", true),
ensure(projectId, artifactRegistryDomain(), "apphosting", true),
ensure(projectId, iamOrigin(), "apphosting", true),
]);
await ensureRequiredApisEnabled(projectId);

// Hack: Because IAM can take ~45 seconds to propagate, we provision the service account as soon as
// possible to reduce the likelihood that the subsequent Cloud Build fails. See b/336862200.
Expand Down Expand Up @@ -179,6 +172,20 @@
logSuccess(`Your backend is now deployed at:\n\thttps://${backend.uri}`);
}

/**
* Check that all GCP APIs required for App Hosting are enabled.
*/
export async function ensureRequiredApisEnabled(projectId: string): Promise<void> {
await Promise.all([
ensure(projectId, developerConnectOrigin(), "apphosting", true),
ensure(projectId, cloudbuildOrigin(), "apphosting", true),
ensure(projectId, secretManagerOrigin(), "apphosting", true),
ensure(projectId, cloudRunApiOrigin(), "apphosting", true),
ensure(projectId, artifactRegistryDomain(), "apphosting", true),
ensure(projectId, iamOrigin(), "apphosting", true),
]);
}

/**
* Set up a new App Hosting-type Developer Connect GitRepoLink, optionally with a specific connection ID
*/
Expand Down Expand Up @@ -248,7 +255,7 @@
* Prompts the user for a backend id and verifies that it doesn't match a pre-existing backend.
*/
export async function promptNewBackendId(projectId: string, location: string): Promise<string> {
while (true) {

Check warning on line 258 in src/apphosting/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected constant condition
const backendId = await input({
default: "my-web-app",
message: "Provide a name for your backend [1-30 characters]",
Expand Down Expand Up @@ -546,7 +553,7 @@
message: locationDisambugationPrompt,
choices: [...backendsByLocation.keys()],
});
return backendsByLocation.get(location)!;

Check warning on line 556 in src/apphosting/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
{
value: "apphosting",
name: "App Hosting: Configure an apphosting.yaml file for App Hosting",
name: "App Hosting: Enable web app deployments with App Hosting",
Copy link
Member

Choose a reason for hiding this comment

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

This seems wrong to me? are we enabling deployments, or making a backend? I think the flow as-written doesn't link to github?

Copy link
Contributor Author

@blidd-google blidd-google May 14, 2025

Choose a reason for hiding this comment

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

cc @jpreid7396 my understanding is that the description is meant to be generic enough to cover set up for deploy from source, but also any other features we might want to include as part of this flow. I'll defer to Julia here about the language specifics

checked: false,
hidden: false,
},
Expand Down Expand Up @@ -178,7 +178,7 @@

const setup: Setup = {
config: config.src,
rcfile: config.readProjectFile(".firebaserc", {

Check warning on line 181 in src/commands/init.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
json: true,
fallback: {},
}),
Expand Down
22 changes: 19 additions & 3 deletions src/init/features/apphosting.ts
Copy link
Member

Choose a reason for hiding this comment

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

In

 projectId, location, backendId, /* serviceAccount= */ null, /* repository= */ undefined, webApp?.id, ); 

It's a real trap right now to make a backend without attaching a git repo (or doing deploy from source), since AFAIK we don't have a command to add one later

Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ import * as clc from "colorette";
import { existsSync } from "fs";
import * as ora from "ora";
import * as path from "path";
import { Setup } from "..";
import { webApps } from "../../apphosting/app";
import {
createBackend,
ensureAppHostingComputeServiceAccount,
ensureRequiredApisEnabled,
promptExistingBackend,
promptLocation,
promptNewBackendId,
} from "../../apphosting/backend";
import { Config } from "../../config";
import { FirebaseError } from "../../error";
import { AppHostingSingle } from "../../firebaseConfig";
import { ensureApiEnabled } from "../../gcp/apphosting";
import { isBillingEnabled } from "../../gcp/cloudbilling";
import { input, select } from "../../prompt";
import { readTemplateSync } from "../../templates";
import * as utils from "../../utils";
import { logBullet } from "../../utils";
import { input, select } from "../../prompt";
import { Setup } from "..";
import { isBillingEnabled } from "../../gcp/cloudbilling";

const APPHOSTING_YAML_TEMPLATE = readTemplateSync("init/apphosting/apphosting.yaml");

Expand All @@ -31,6 +34,19 @@ export async function doSetup(setup: Setup, config: Config): Promise<void> {
"Firebase App Hosting requires billing to be enabled on your project. Please enable billing by following the steps at https://cloud.google.com/billing/docs/how-to/modify-project",
);
}
await ensureApiEnabled({ projectId });
await ensureRequiredApisEnabled(projectId);
try {
await ensureAppHostingComputeServiceAccount(projectId, /* serviceAccount= */ "");
} catch (err) {
if ((err as FirebaseError).status === 400) {
utils.logWarning(
"Your App Hosting compute service account is still being provisioned. Please try again in a few moments.",
);
}
throw err;
}

utils.logBullet(
"This command links your local project to Firebase App Hosting. You will be able to deploy your web app with `firebase deploy` after setup.",
);
Expand Down
7 changes: 5 additions & 2 deletions src/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
featureInfo?: SetupInfo;

/** Basic Project information */
project?: Record<string, any>;

Check warning on line 20 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
projectId?: string;
projectLocation?: string;
isBillingEnabled?: boolean;

hosting?: Record<string, any>;

Check warning on line 25 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
}

export interface SetupInfo {
Expand All @@ -31,6 +31,7 @@

interface Feature {
name: string;
displayName?: string;
// OLD WAY: A single setup function to ask questions and actuate the setup.
doSetup?: (setup: Setup, config: Config, options: Options) => Promise<unknown>;

Expand Down Expand Up @@ -64,16 +65,18 @@
{ name: "remoteconfig", doSetup: features.remoteconfig },
{ name: "hosting:github", doSetup: features.hostingGithub },
{ name: "genkit", doSetup: features.genkit },
{ name: "apphosting", doSetup: features.apphosting },
{ name: "apphosting", displayName: "App Hosting", doSetup: features.apphosting },
];

const featureMap = new Map(featuresList.map((feature) => [feature.name, feature]));

export async function init(setup: Setup, config: any, options: any): Promise<any> {

Check warning on line 73 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 73 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 73 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 73 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const nextFeature = setup.features?.shift();
if (nextFeature) {
const f = lookupFeature(nextFeature);
logger.info(clc.bold(`\n${clc.white("===")} ${capitalize(nextFeature)} Setup`));
logger.info(
clc.bold(`\n${clc.white("===")} ${f.displayName || capitalize(nextFeature)} Setup`),
);

if (f.doSetup) {
await f.doSetup(setup, config, options);
Expand Down
Loading