diff --git a/src/apphosting/backend.spec.ts b/src/apphosting/backend.spec.ts index 13495dabd2d..89856904935 100644 --- a/src/apphosting/backend.spec.ts +++ b/src/apphosting/backend.spec.ts @@ -120,10 +120,43 @@ describe("apphosting setup functions", () => { labels: deploymentTool.labels(), serviceAccount: "custom-service-account", appId: webAppId, + runtime: undefined, }; - expect(createBackendStub).to.be.calledWith(projectId, location, backendInput); + expect(createBackendStub).to.be.calledWith(projectId, location, backendInput, backendId); }); + const runtimes = ["nodejs22", ""]; + for (const runtime of runtimes) { + it(`should create a new backend with runtime ${runtime}`, async () => { + createBackendStub.resolves(op); + pollOperationStub.resolves(completeBackend); + + await createBackend( + projectId, + location, + backendId, + "custom-service-account", + cloudBuildConnRepo, + webAppId, + "/", + runtime, + ); + + const backendInput: Omit = { + servingLocality: "GLOBAL_ACCESS", + codebase: { + repository: cloudBuildConnRepo.name, + rootDirectory: "/", + }, + labels: deploymentTool.labels(), + serviceAccount: "custom-service-account", + appId: webAppId, + runtime: runtime ? { value: runtime } : undefined, + }; + expect(createBackendStub).to.be.calledWith(projectId, location, backendInput, backendId); + }); + } + it("should set default rollout policy to 100% all at once", async () => { const completeTraffic: apphosting.Traffic = { name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`, diff --git a/src/apphosting/backend.ts b/src/apphosting/backend.ts index c3320c9cf12..bfcb037a00b 100644 --- a/src/apphosting/backend.ts +++ b/src/apphosting/backend.ts @@ -29,6 +29,8 @@ import fetch from "node-fetch"; import { orchestrateRollout } from "./rollout"; import * as fuzzy from "fuzzy"; +const DEFAULT_RUNTIME = "nodejs"; + const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute"; const apphostingPollerOptions: Omit = { @@ -79,6 +81,7 @@ export async function doSetup( serviceAccount?: string, primaryRegion?: string, rootDir?: string, + runtime?: string, ): Promise { await ensureRequiredApisEnabled(projectId); @@ -125,6 +128,21 @@ export async function doSetup( throw new FirebaseError("Internal error: location or backendId is not defined."); } + if (!runtime) { + if (nonInteractive) { + runtime = DEFAULT_RUNTIME; + } else { + runtime = await select({ + message: "Which runtime do you want to use?", + choices: [ + { name: "Node.js (default)", value: DEFAULT_RUNTIME }, + { name: "Node.js 22", value: "nodejs22" }, + ], + default: DEFAULT_RUNTIME, + }); + } + } + const webApp = await webApps.getOrCreateWebApp( projectId, webAppName ? webAppName : null, @@ -143,6 +161,7 @@ export async function doSetup( gitRepositoryLink, webApp?.id, rootDir, + runtime, ); createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`); @@ -352,6 +371,7 @@ export async function createBackend( repository: GitRepositoryLink | undefined, webAppId: string | undefined, rootDir = "/", + runtime?: string, ): Promise { const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId); const backendReqBody: Omit = { @@ -365,6 +385,7 @@ export async function createBackend( labels: deploymentTool.labels(), serviceAccount: serviceAccount || defaultServiceAccount, appId: webAppId, + runtime: runtime ? { value: runtime } : undefined, }; async function createBackendAndPoll(): Promise { diff --git a/src/commands/apphosting-backends-create.ts b/src/commands/apphosting-backends-create.ts index b09a02d585c..3f962c7e37f 100644 --- a/src/commands/apphosting-backends-create.ts +++ b/src/commands/apphosting-backends-create.ts @@ -28,6 +28,7 @@ export const command = new Command("apphosting:backends:create") "specify the primary region for the backend. Required with --non-interactive.", ) .option("--root-dir ", "specify the root directory for the backend.") + .option("--runtime ", "specify the runtime for the backend (e.g., nodejs, nodejs22)") .before(requireAuth) .before(ensureApiEnabled) .before(requireTosAcceptance(APPHOSTING_TOS_ID)) @@ -45,5 +46,6 @@ export const command = new Command("apphosting:backends:create") options.serviceAccount as string | undefined, options.primaryRegion as string | undefined, options.rootDir as string | undefined, + options.runtime as string | undefined, ); }); diff --git a/src/gcp/apphosting.ts b/src/gcp/apphosting.ts index 57c02079b0d..b92aff1d319 100644 --- a/src/gcp/apphosting.ts +++ b/src/gcp/apphosting.ts @@ -30,6 +30,10 @@ interface Codebase { */ export type ServingLocality = "GLOBAL_ACCESS" | "REGIONAL_STRICT"; +export interface Runtime { + value: string; +} + /** A Backend, the primary resource of Frameworks. */ export interface Backend { name: string; @@ -43,6 +47,7 @@ export interface Backend { serviceAccount?: string; appId?: string; managedResources?: ManagedResource[]; + runtime?: Runtime; } export interface ManagedResource {