From 347fa56387bdd4ecdba136a49bef6fb193ebbb0c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 02:27:07 +0000 Subject: [PATCH 01/21] feat: Support multi-instance function deployments This change introduces the ability to deploy multiple instances of the same function source, each with its own configuration. This is achieved by allowing multiple function configurations in `firebase.json`. If multiple configurations are present, each must have a unique `codebase` identifier. This change is the first step towards implementing "Function Kits", which will allow developers to easily add pre-built backend functionality to their Firebase projects. This change is backward-compatible. Existing `firebase.json` configurations will continue to work as before. --- schema/firebase-config.json | 3 ++ .../emulator-tests/functionsEmulator.spec.ts | 42 +++++++++++++++++++ src/deploy/functions/build.spec.ts | 21 ++++++++++ src/deploy/functions/build.ts | 6 ++- src/deploy/functions/prepare.ts | 1 + src/emulator/controller.ts | 1 + src/emulator/functionsEmulator.ts | 2 + src/firebaseConfig.ts | 1 + src/functions/projectConfig.spec.ts | 35 +++++++++++----- src/functions/projectConfig.ts | 10 ++++- 10 files changed, 108 insertions(+), 14 deletions(-) diff --git a/schema/firebase-config.json b/schema/firebase-config.json index 9eb0f6a618f..5bc1a802dc7 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -276,6 +276,9 @@ } ] }, + "prefix": { + "type": "string" + }, "runtime": { "enum": [ "nodejs18", diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index fc1e04ecd77..1e10fee9ef6 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -760,6 +760,48 @@ describe("FunctionsEmulator", function () { }).timeout(TIMEOUT_MED); }); + it("should support multiple codebases with the same source and apply prefixes", async () => { + const backend1: EmulatableBackend = { + ...TEST_BACKEND, + codebase: "one", + prefix: "prefix-one", + }; + const backend2: EmulatableBackend = { + ...TEST_BACKEND, + codebase: "two", + prefix: "prefix-two", + }; + + emu = new FunctionsEmulator({ + projectId: TEST_PROJECT_ID, + projectDir: MODULE_ROOT, + emulatableBackends: [backend1, backend2], + verbosity: "QUIET", + debugPort: false, + }); + + await writeSource(() => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; + }); + + await emu.start(); + await emu.connect(); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/prefix-one-functionId`) + .expect(200); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/prefix-two-functionId`) + .expect(200); + }); + describe("user-defined environment variables", () => { let cleanup: (() => Promise) | undefined; diff --git a/src/deploy/functions/build.spec.ts b/src/deploy/functions/build.spec.ts index ff279f8ce47..ebfe9550e62 100644 --- a/src/deploy/functions/build.spec.ts +++ b/src/deploy/functions/build.spec.ts @@ -224,6 +224,27 @@ describe("toBackend", () => { expect(endpointDef.func.serviceAccount).to.equal("service-account-1@"); } }); + + it("should apply the prefix to the function name", () => { + const desiredBuild: build.Build = build.of({ + func: { + platform: "gcfv1", + region: ["us-central1"], + project: "project", + runtime: "nodejs16", + entryPoint: "func", + httpsTrigger: {}, + }, + }); + const backend = build.toBackend(desiredBuild, {}, "my-prefix"); + expect(Object.keys(backend.endpoints).length).to.equal(1); + const regionalEndpoints = Object.values(backend.endpoints)[0]; + const endpoint = Object.values(regionalEndpoints)[0]; + expect(endpoint).to.not.equal(undefined); + if (endpoint) { + expect(endpoint.id).to.equal("my-prefix-func"); + } + }); }); describe("envWithType", () => { diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 9fca9b58d09..ccf0d59858d 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -287,6 +287,7 @@ interface ResolveBackendOpts { userEnvs: Record; nonInteractive?: boolean; isEmulator?: boolean; + prefix?: string; } /** @@ -316,7 +317,7 @@ export async function resolveBackend( } writeUserEnvs(toWrite, opts.userEnvOpt); - return { backend: toBackend(opts.build, paramValues), envs: paramValues }; + return { backend: toBackend(opts.build, paramValues, opts.prefix), envs: paramValues }; } // Exported for testing @@ -446,6 +447,7 @@ class Resolver { export function toBackend( build: Build, paramValues: Record, + prefix?: string, ): backend.Backend { const r = new Resolver(paramValues); const bkEndpoints: Array = []; @@ -481,7 +483,7 @@ export function toBackend( throw new FirebaseError("platform can't be undefined"); } const bkEndpoint: backend.Endpoint = { - id: endpointId, + id: prefix ? `${prefix}-${endpointId}` : endpointId, project: bdEndpoint.project, region: region, entryPoint: bdEndpoint.entryPoint, diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index a4546a5e1ad..3d1243e1302 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -135,6 +135,7 @@ export async function prepare( userEnvs, nonInteractive: options.nonInteractive, isEmulator: false, + prefix: config.prefix, }); let hasEnvsFromParams = false; diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 17dfb6a302c..41b9d0d61ae 100755 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -544,6 +544,7 @@ export async function startAll( functionsDir, runtime, codebase: cfg.codebase, + prefix: cfg.prefix, env: { ...options.extDevEnv, }, diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index c8f69de5343..4117617c2df 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -87,6 +87,7 @@ export interface EmulatableBackend { env: Record; secretEnv: backend.SecretEnvVar[]; codebase: string; + prefix?: string; predefinedTriggers?: ParsedTriggerDefinition[]; runtime?: Runtime; bin?: string; @@ -570,6 +571,7 @@ export class FunctionsEmulator implements EmulatorInstance { userEnvs, nonInteractive: false, isEmulator: true, + prefix: emulatableBackend.prefix, }); const discoveredBackend = resolution.backend; const endpoints = backend.allEndpoints(discoveredBackend); diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index b0c46bee651..9378f927ff7 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -170,6 +170,7 @@ export type FunctionConfig = { ignore?: string[]; runtime?: ActiveRuntime; codebase?: string; + prefix?: string; } & Deployable; export type FunctionsConfig = FunctionConfig | FunctionConfig[]; diff --git a/src/functions/projectConfig.spec.ts b/src/functions/projectConfig.spec.ts index de9e8d97b84..8d5e704f9f9 100644 --- a/src/functions/projectConfig.spec.ts +++ b/src/functions/projectConfig.spec.ts @@ -42,10 +42,22 @@ describe("projectConfig", () => { ); }); - it("fails validation given config w/ duplicate source", () => { - expect(() => - projectConfig.validate([TEST_CONFIG_0, { ...TEST_CONFIG_0, codebase: "unique-codebase" }]), - ).to.throw(FirebaseError, /source must be unique/); + it("passes validation for multi-instance config with same source", () => { + const config = [ + { source: "foo", codebase: "bar" }, + { source: "foo", codebase: "baz" }, + ]; + expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal( + config, + ); + }); + + it("fails validation for multi-instance config with missing codebase", () => { + const config = [{ source: "foo", codebase: "bar" }, { source: "foo" }]; + expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw( + FirebaseError, + /Each functions config must have a unique 'codebase' field/, + ); }); it("fails validation given codebase name with capital letters", () => { @@ -72,6 +84,14 @@ describe("projectConfig", () => { ]), ).to.throw(FirebaseError, /Invalid codebase name/); }); + + it("should allow a single function in an array to have a default codebase", () => { + const config = [{ source: "foo" }]; + const expected = [{ source: "foo", codebase: "default" }]; + expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal( + expected, + ); + }); }); describe("normalizeAndValidate", () => { @@ -104,13 +124,6 @@ describe("projectConfig", () => { ); }); - it("fails validation given config w/ duplicate source", () => { - expect(() => projectConfig.normalizeAndValidate([TEST_CONFIG_0, TEST_CONFIG_0])).to.throw( - FirebaseError, - /functions.source must be unique/, - ); - }); - it("fails validation given config w/ duplicate codebase", () => { expect(() => projectConfig.normalizeAndValidate([ diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts index 313db95d48a..e21b53a6203 100644 --- a/src/functions/projectConfig.ts +++ b/src/functions/projectConfig.ts @@ -76,8 +76,16 @@ export function assertUnique( * Validate functions config. */ export function validate(config: NormalizedConfig): ValidatedConfig { + if (config.length > 1) { + for (const c of config) { + if (!c.codebase) { + throw new FirebaseError( + "Each functions config must have a unique 'codebase' field when defining multiple functions.", + ); + } + } + } const validated = config.map((cfg) => validateSingle(cfg)) as ValidatedConfig; - assertUnique(validated, "source"); assertUnique(validated, "codebase"); return validated; } From 78949b3326ee21f97d018b98005f4e6a83a34cf5 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Tue, 29 Jul 2025 20:42:29 -0700 Subject: [PATCH 02/21] feat: Add validation for function prefixes and source/prefix pairs This change introduces two new validation rules for function configurations in `firebase.json`: 1. The `prefix` property, if specified, must only contain lowercase letters, numbers, and hyphens. 2. The combination of `source` and `prefix` must be unique across all function configurations. These changes ensure that function deployments with prefixes are valid and that there are no conflicts when deploying multiple functions from the same source directory. --- src/functions/projectConfig.spec.ts | 52 ++++++++++++++++++++++++++--- src/functions/projectConfig.ts | 44 ++++++++++++++++++------ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/functions/projectConfig.spec.ts b/src/functions/projectConfig.spec.ts index 8d5e704f9f9..e1c43e08296 100644 --- a/src/functions/projectConfig.spec.ts +++ b/src/functions/projectConfig.spec.ts @@ -45,18 +45,37 @@ describe("projectConfig", () => { it("passes validation for multi-instance config with same source", () => { const config = [ { source: "foo", codebase: "bar" }, - { source: "foo", codebase: "baz" }, + { source: "foo", codebase: "baz", prefix: "prefix-two" }, ]; expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal( config, ); }); - it("fails validation for multi-instance config with missing codebase", () => { - const config = [{ source: "foo", codebase: "bar" }, { source: "foo" }]; + it("passes validation for multi-instance config with one missing codebase", () => { + const config = [{ source: "foo", codebase: "bar", prefix: "bar-prefix" }, { source: "foo" }]; + const expected = [ + { source: "foo", codebase: "bar", prefix: "bar-prefix" }, + { source: "foo", codebase: "default" }, + ]; + expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal( + expected, + ); + }); + + it("fails validation for multi-instance config with missing codebase and a default codebase", () => { + const config = [{ source: "foo", codebase: "default" }, { source: "foo" }]; expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw( FirebaseError, - /Each functions config must have a unique 'codebase' field/, + /functions.codebase must be unique but 'default' was used more than once./, + ); + }); + + it("fails validation for multi-instance config with multiple missing codebases", () => { + const config = [{ source: "foo" }, { source: "foo" }]; + expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw( + FirebaseError, + /functions.codebase must be unique but 'default' was used more than once./, ); }); @@ -85,6 +104,31 @@ describe("projectConfig", () => { ).to.throw(FirebaseError, /Invalid codebase name/); }); + it("fails validation given prefix with invalid characters", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "abc.efg" }])).to.throw( + FirebaseError, + /Invalid prefix/, + ); + }); + + it("fails validation given prefix with capital letters", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "ABC" }])).to.throw( + FirebaseError, + /Invalid prefix/, + ); + }); + + it("fails validation given a duplicate source/prefix pair", () => { + const config = [ + { source: "foo", codebase: "bar", prefix: "a" }, + { source: "foo", codebase: "baz", prefix: "a" }, + ]; + expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw( + FirebaseError, + /More than one functions config specifies the same source directory/, + ); + }); + it("should allow a single function in an array to have a default codebase", () => { const config = [{ source: "foo" }]; const expected = [{ source: "foo", codebase: "default" }]; diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts index e21b53a6203..1fe282ea62b 100644 --- a/src/functions/projectConfig.ts +++ b/src/functions/projectConfig.ts @@ -2,7 +2,7 @@ import { FunctionsConfig, FunctionConfig } from "../firebaseConfig"; import { FirebaseError } from "../error"; export type NormalizedConfig = [FunctionConfig, ...FunctionConfig[]]; -export type ValidatedSingle = FunctionConfig & { source: string; codebase: string }; +export type ValidatedSingle = FunctionConfig & { source:string; codebase: string }; export type ValidatedConfig = [ValidatedSingle, ...ValidatedSingle[]]; export const DEFAULT_CODEBASE = "default"; @@ -37,6 +37,20 @@ export function validateCodebase(codebase: string): void { } } +/** + * Check that the prefix contains only allowed characters. + */ +export function validatePrefix(prefix: string): void { + if (prefix.length > 30) { + throw new FirebaseError("Invalid prefix. Prefix must be 30 characters or less."); + } + if (!/^[a-z0-9-]+$/.test(prefix)) { + throw new FirebaseError( + "Invalid prefix. Prefix can contain only lowercase letters, numeric characters, and dashes.", + ); + } +} + function validateSingle(config: FunctionConfig): ValidatedSingle { if (!config.source) { throw new FirebaseError("codebase source must be specified"); @@ -45,6 +59,9 @@ function validateSingle(config: FunctionConfig): ValidatedSingle { config.codebase = DEFAULT_CODEBASE; } validateCodebase(config.codebase); + if (config.prefix) { + validatePrefix(config.prefix); + } return { ...config, source: config.source, codebase: config.codebase }; } @@ -72,21 +89,26 @@ export function assertUnique( } } +function assertUniqueSourcePrefixPair(config: ValidatedConfig): void { + const sourcePrefixPairs = new Set(); + for (const c of config) { + const key = `${c.source || ""}-${c.prefix || ""}`; + if (sourcePrefixPairs.has(key)) { + throw new FirebaseError( + `More than one functions config specifies the same source directory ('${c.source}') and function name prefix ('${c.prefix || ""}').`, + ); + } + sourcePrefixPairs.add(key); + } +} + /** * Validate functions config. */ export function validate(config: NormalizedConfig): ValidatedConfig { - if (config.length > 1) { - for (const c of config) { - if (!c.codebase) { - throw new FirebaseError( - "Each functions config must have a unique 'codebase' field when defining multiple functions.", - ); - } - } - } const validated = config.map((cfg) => validateSingle(cfg)) as ValidatedConfig; assertUnique(validated, "codebase"); + assertUniqueSourcePrefixPair(validated); return validated; } @@ -108,4 +130,4 @@ export function configForCodebase(config: ValidatedConfig, codebase: string): Va throw new FirebaseError(`No functions config found for codebase ${codebase}`); } return codebaseCfg; -} +} \ No newline at end of file From 57f62505446d4cea872328fd1100618e21df0254 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 29 Jul 2025 20:49:31 -0700 Subject: [PATCH 03/21] Update src/functions/projectConfig.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/functions/projectConfig.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts index 1fe282ea62b..a76b26ae5d7 100644 --- a/src/functions/projectConfig.ts +++ b/src/functions/projectConfig.ts @@ -44,11 +44,11 @@ export function validatePrefix(prefix: string): void { if (prefix.length > 30) { throw new FirebaseError("Invalid prefix. Prefix must be 30 characters or less."); } - if (!/^[a-z0-9-]+$/.test(prefix)) { - throw new FirebaseError( - "Invalid prefix. Prefix can contain only lowercase letters, numeric characters, and dashes.", - ); - } +if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(prefix)) { + throw new FirebaseError( + "Invalid prefix. Prefix can contain only lowercase letters, numeric characters, and dashes, and cannot start or end with a dash.", + ); +} } function validateSingle(config: FunctionConfig): ValidatedSingle { From 09aea9fb1094538076b3be78cee1f9d32277dd2e Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Tue, 29 Jul 2025 21:32:06 -0700 Subject: [PATCH 04/21] refactor: Improve validation logic and test clarity This commit includes several improvements to the function configuration validation: - The error message for duplicate `source`/`prefix` pairs is now more descriptive and suggests a solution. - The test suite is more robust, with more specific error message checks and improved typing. - Linter warnings have been addressed. --- src/functions/projectConfig.spec.ts | 49 ++++++++++++++++++----------- src/functions/projectConfig.ts | 12 ++++--- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/functions/projectConfig.spec.ts b/src/functions/projectConfig.spec.ts index e1c43e08296..7f2dd86f85a 100644 --- a/src/functions/projectConfig.spec.ts +++ b/src/functions/projectConfig.spec.ts @@ -43,37 +43,39 @@ describe("projectConfig", () => { }); it("passes validation for multi-instance config with same source", () => { - const config = [ + const config: projectConfig.NormalizedConfig = [ { source: "foo", codebase: "bar" }, { source: "foo", codebase: "baz", prefix: "prefix-two" }, ]; - expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal( - config, - ); + expect(projectConfig.validate(config)).to.deep.equal(config); }); it("passes validation for multi-instance config with one missing codebase", () => { - const config = [{ source: "foo", codebase: "bar", prefix: "bar-prefix" }, { source: "foo" }]; + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "bar", prefix: "bar-prefix" }, + { source: "foo" }, + ]; const expected = [ { source: "foo", codebase: "bar", prefix: "bar-prefix" }, { source: "foo", codebase: "default" }, ]; - expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal( - expected, - ); + expect(projectConfig.validate(config)).to.deep.equal(expected); }); it("fails validation for multi-instance config with missing codebase and a default codebase", () => { - const config = [{ source: "foo", codebase: "default" }, { source: "foo" }]; - expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw( + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "default" }, + { source: "foo" }, + ]; + expect(() => projectConfig.validate(config)).to.throw( FirebaseError, /functions.codebase must be unique but 'default' was used more than once./, ); }); it("fails validation for multi-instance config with multiple missing codebases", () => { - const config = [{ source: "foo" }, { source: "foo" }]; - expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw( + const config: projectConfig.NormalizedConfig = [{ source: "foo" }, { source: "foo" }]; + expect(() => projectConfig.validate(config)).to.throw( FirebaseError, /functions.codebase must be unique but 'default' was used more than once./, ); @@ -119,22 +121,31 @@ describe("projectConfig", () => { }); it("fails validation given a duplicate source/prefix pair", () => { - const config = [ + const config: projectConfig.NormalizedConfig = [ { source: "foo", codebase: "bar", prefix: "a" }, { source: "foo", codebase: "baz", prefix: "a" }, ]; - expect(() => projectConfig.validate(config as projectConfig.NormalizedConfig)).to.throw( + expect(() => projectConfig.validate(config)).to.throw( FirebaseError, - /More than one functions config specifies the same source directory/, + /More than one functions config specifies the same source directory \('foo'\) and prefix \('a'\)/, + ); + }); + + it("fails validation for multi-instance config with same source and no prefixes", () => { + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "bar" }, + { source: "foo", codebase: "baz" }, + ]; + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /More than one functions config specifies the same source directory \('foo'\) and prefix \(''\)/, ); }); it("should allow a single function in an array to have a default codebase", () => { - const config = [{ source: "foo" }]; + const config: projectConfig.NormalizedConfig = [{ source: "foo" }]; const expected = [{ source: "foo", codebase: "default" }]; - expect(projectConfig.validate(config as projectConfig.NormalizedConfig)).to.deep.equal( - expected, - ); + expect(projectConfig.validate(config)).to.deep.equal(expected); }); }); diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts index a76b26ae5d7..2c0b8a6231f 100644 --- a/src/functions/projectConfig.ts +++ b/src/functions/projectConfig.ts @@ -2,7 +2,7 @@ import { FunctionsConfig, FunctionConfig } from "../firebaseConfig"; import { FirebaseError } from "../error"; export type NormalizedConfig = [FunctionConfig, ...FunctionConfig[]]; -export type ValidatedSingle = FunctionConfig & { source:string; codebase: string }; +export type ValidatedSingle = FunctionConfig & { source: string; codebase: string }; export type ValidatedConfig = [ValidatedSingle, ...ValidatedSingle[]]; export const DEFAULT_CODEBASE = "default"; @@ -92,10 +92,14 @@ export function assertUnique( function assertUniqueSourcePrefixPair(config: ValidatedConfig): void { const sourcePrefixPairs = new Set(); for (const c of config) { - const key = `${c.source || ""}-${c.prefix || ""}`; + const key = `${c.source}-${c.prefix || ""}`; if (sourcePrefixPairs.has(key)) { throw new FirebaseError( - `More than one functions config specifies the same source directory ('${c.source}') and function name prefix ('${c.prefix || ""}').`, + `More than one functions config specifies the same source directory ('${ + c.source + }') and prefix ('${ + c.prefix ?? "" + }'). Please add a unique 'prefix' to each function configuration that shares this source to resolve the conflict.`, ); } sourcePrefixPairs.add(key); @@ -130,4 +134,4 @@ export function configForCodebase(config: ValidatedConfig, codebase: string): Va throw new FirebaseError(`No functions config found for codebase ${codebase}`); } return codebaseCfg; -} \ No newline at end of file +} From 2de18020ccb008a1f2e035ebe1c1839a923b011c Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 30 Jul 2025 10:04:56 -0700 Subject: [PATCH 05/21] fix: refactor prefix processing at one level higher. --- src/deploy/functions/build.spec.ts | 21 -------- src/deploy/functions/build.ts | 6 +-- src/deploy/functions/prepare.spec.ts | 75 +++++++++++++++++++++++++--- src/deploy/functions/prepare.ts | 11 +++- src/emulator/functionsEmulator.ts | 8 ++- 5 files changed, 85 insertions(+), 36 deletions(-) diff --git a/src/deploy/functions/build.spec.ts b/src/deploy/functions/build.spec.ts index ebfe9550e62..ff279f8ce47 100644 --- a/src/deploy/functions/build.spec.ts +++ b/src/deploy/functions/build.spec.ts @@ -224,27 +224,6 @@ describe("toBackend", () => { expect(endpointDef.func.serviceAccount).to.equal("service-account-1@"); } }); - - it("should apply the prefix to the function name", () => { - const desiredBuild: build.Build = build.of({ - func: { - platform: "gcfv1", - region: ["us-central1"], - project: "project", - runtime: "nodejs16", - entryPoint: "func", - httpsTrigger: {}, - }, - }); - const backend = build.toBackend(desiredBuild, {}, "my-prefix"); - expect(Object.keys(backend.endpoints).length).to.equal(1); - const regionalEndpoints = Object.values(backend.endpoints)[0]; - const endpoint = Object.values(regionalEndpoints)[0]; - expect(endpoint).to.not.equal(undefined); - if (endpoint) { - expect(endpoint.id).to.equal("my-prefix-func"); - } - }); }); describe("envWithType", () => { diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index ccf0d59858d..9fca9b58d09 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -287,7 +287,6 @@ interface ResolveBackendOpts { userEnvs: Record; nonInteractive?: boolean; isEmulator?: boolean; - prefix?: string; } /** @@ -317,7 +316,7 @@ export async function resolveBackend( } writeUserEnvs(toWrite, opts.userEnvOpt); - return { backend: toBackend(opts.build, paramValues, opts.prefix), envs: paramValues }; + return { backend: toBackend(opts.build, paramValues), envs: paramValues }; } // Exported for testing @@ -447,7 +446,6 @@ class Resolver { export function toBackend( build: Build, paramValues: Record, - prefix?: string, ): backend.Backend { const r = new Resolver(paramValues); const bkEndpoints: Array = []; @@ -483,7 +481,7 @@ export function toBackend( throw new FirebaseError("platform can't be undefined"); } const bkEndpoint: backend.Endpoint = { - id: prefix ? `${prefix}-${endpointId}` : endpointId, + id: endpointId, project: bdEndpoint.project, region: region, entryPoint: bdEndpoint.entryPoint, diff --git a/src/deploy/functions/prepare.spec.ts b/src/deploy/functions/prepare.spec.ts index 0cb7a35a8c9..6bdba52ca6a 100644 --- a/src/deploy/functions/prepare.spec.ts +++ b/src/deploy/functions/prepare.spec.ts @@ -1,13 +1,18 @@ import { expect } from "chai"; - -import * as backend from "./backend"; +import * as sinon from "sinon"; +import * as build from "./build"; import * as prepare from "./prepare"; +import * as runtimes from "./runtimes"; +import { RuntimeDelegate } from "./runtimes"; +import { RUNTIMES } from "./runtimes/supported"; +import { FirebaseError } from "../../error"; +import { Options } from "../../options"; +import { ValidatedConfig } from "../../functions/projectConfig"; +import * as backend from "./backend"; import * as ensureApiEnabled from "../../ensureApiEnabled"; import * as serviceusage from "../../gcp/serviceusage"; import { BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT } from "../../functions/events/v1"; -import * as sinon from "sinon"; import * as prompt from "../../prompt"; -import { FirebaseError } from "../../error"; describe("prepare", () => { const ENDPOINT_BASE: Omit = { @@ -16,7 +21,7 @@ describe("prepare", () => { region: "region", project: "project", entryPoint: "entry", - runtime: "nodejs16", + runtime: "nodejs22", }; const ENDPOINT: backend.Endpoint = { @@ -24,6 +29,60 @@ describe("prepare", () => { httpsTrigger: {}, }; + describe("loadCodebases", () => { + let sandbox: sinon.SinonSandbox; + let runtimeDelegateStub: RuntimeDelegate; + let discoverBuildStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + discoverBuildStub = sandbox.stub(); + runtimeDelegateStub = { + language: "nodejs", + runtime: "nodejs22", + bin: "node", + validate: sandbox.stub().resolves(), + build: sandbox.stub().resolves(), + watch: sandbox.stub().resolves(() => Promise.resolve()), + discoverBuild: discoverBuildStub, + }; + discoverBuildStub.resolves( + build.of({ + test: { + platform: "gcfv2", + entryPoint: "test", + project: "project", + runtime: "nodejs22", + httpsTrigger: {}, + }, + }), + ); + sandbox.stub(runtimes, "getRuntimeDelegate").resolves(runtimeDelegateStub); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should apply the prefix to the function name", async () => { + const config: ValidatedConfig = [ + { source: "source", codebase: "codebase", prefix: "my-prefix", runtime: "nodejs22" }, + ]; + const options = { + config: { + path: (p: string) => p, + }, + projectId: "project", + } as unknown as Options; + const firebaseConfig = { projectId: "project" }; + const runtimeConfig = {}; + + const builds = await prepare.loadCodebases(config, options, firebaseConfig, runtimeConfig); + + expect(Object.keys(builds.codebase.endpoints)).to.deep.equal(["my-prefix-test"]); + }); + }); + describe("inferDetailsFromExisting", () => { it("merges env vars if .env is not used", () => { const oldE = { @@ -304,7 +363,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs16", + runtime: "nodejs22", httpsTrigger: {}, }; @@ -314,7 +373,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs16", + runtime: "nodejs22", callableTrigger: { genkitAction: "action", }, @@ -333,7 +392,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs16", + runtime: "nodejs22", callableTrigger: { genkitAction: "action", }, diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index 3d1243e1302..018fb267c28 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -135,7 +135,6 @@ export async function prepare( userEnvs, nonInteractive: options.nonInteractive, isEmulator: false, - prefix: config.prefix, }); let hasEnvsFromParams = false; @@ -474,13 +473,21 @@ export async function loadCodebases( "functions", `Loading and analyzing source code for codebase ${codebase} to determine what to deploy`, ); - wantBuilds[codebase] = await runtimeDelegate.discoverBuild(runtimeConfig, { + const build = await runtimeDelegate.discoverBuild(runtimeConfig, { ...firebaseEnvs, // Quota project is required when using GCP's Client-based APIs // Some GCP client SDKs, like Vertex AI, requires appropriate quota project setup // in order for .init() calls to succeed. GOOGLE_CLOUD_QUOTA_PROJECT: projectId, }); + if (codebaseConfig.prefix) { + const newEndpoints: Record = {}; + for (const id of Object.keys(build.endpoints)) { + newEndpoints[`${codebaseConfig.prefix}-${id}`] = build.endpoints[id]; + } + build.endpoints = newEndpoints; + } + wantBuilds[codebase] = build; wantBuilds[codebase].runtime = codebaseConfig.runtime; } return wantBuilds; diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 4117617c2df..c994cfcc5c1 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -564,6 +564,13 @@ export class FunctionsEmulator implements EmulatorInstance { ); await this.loadDynamicExtensionBackends(); } + if (emulatableBackend.prefix) { + const newEndpoints: Record = {}; + for (const id of Object.keys(discoveredBuild.endpoints)) { + newEndpoints[`${emulatableBackend.prefix}-${id}`] = discoveredBuild.endpoints[id]; + } + discoveredBuild.endpoints = newEndpoints; + } const resolution = await resolveBackend({ build: discoveredBuild, firebaseConfig: JSON.parse(firebaseConfig), @@ -571,7 +578,6 @@ export class FunctionsEmulator implements EmulatorInstance { userEnvs, nonInteractive: false, isEmulator: true, - prefix: emulatableBackend.prefix, }); const discoveredBackend = resolution.backend; const endpoints = backend.allEndpoints(discoveredBackend); From fbe84b70f4d7ce435aa860f395c2be3c8e460fe2 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 13 Aug 2025 20:49:03 -0700 Subject: [PATCH 06/21] fix: Show clear error when Functions emulator fails to start due to config validation Previously, validation errors (like duplicate source/prefix pairs) were caught and hidden with a generic warning about missing source directories. This caused confusing downstream failures like "Extensions Emulator is running but Functions emulator is not". Now validation errors are properly surfaced with clear error messages that match what users see during deploy, making it easier to diagnose configuration issues. --- src/emulator/controller.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts index 41b9d0d61ae..85578f513c9 100755 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -170,18 +170,16 @@ export function shouldStart(options: Options, name: Emulators): boolean { ); } - // Don't start the functions emulator if we can't find the source directory + // Don't start the functions emulator if we can't validate the functions config if (name === Emulators.FUNCTIONS && emulatorInTargets) { try { normalizeAndValidate(options.config.src.functions); return true; } catch (err: any) { EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled( - "WARN", + "ERROR", "functions", - `The functions emulator is configured but there is no functions source directory. Have you run ${clc.bold( - "firebase init functions", - )}?`, + `Failed to start Functions emulator: ${err.message}`, ); return false; } From 8de3c66400b1b5461a9607cd6069a421ebe415dc Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Wed, 13 Aug 2025 21:00:23 -0700 Subject: [PATCH 07/21] fix: ts error. --- src/emulator/functionsEmulator.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index c994cfcc5c1..1eaf3dcc699 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -53,8 +53,9 @@ import { import { functionIdsAreValid } from "../deploy/functions/validate"; import { Extension, ExtensionSpec, ExtensionVersion } from "../extensions/types"; import { accessSecretVersion } from "../gcp/secretManager"; -import * as runtimes from "../deploy/functions/runtimes"; import * as backend from "../deploy/functions/backend"; +import * as build from "../deploy/functions/build"; +import * as runtimes from "../deploy/functions/runtimes"; import * as functionsEnv from "../functions/env"; import { AUTH_BLOCKING_EVENTS, BEFORE_CREATE_EVENT } from "../functions/events/v1"; import { BlockingFunctionsConfig } from "../gcp/identityPlatform"; @@ -565,7 +566,7 @@ export class FunctionsEmulator implements EmulatorInstance { await this.loadDynamicExtensionBackends(); } if (emulatableBackend.prefix) { - const newEndpoints: Record = {}; + const newEndpoints: Record = {}; for (const id of Object.keys(discoveredBuild.endpoints)) { newEndpoints[`${emulatableBackend.prefix}-${id}`] = discoveredBuild.endpoints[id]; } From 4b2bc0d56f03df2e8ea0704ee571edf6309f5d1c Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 14 Aug 2025 13:35:57 -0700 Subject: [PATCH 08/21] style: run formatter. --- src/deploy/functions/prepare.spec.ts | 1 - src/functions/projectConfig.ts | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/deploy/functions/prepare.spec.ts b/src/deploy/functions/prepare.spec.ts index 6bdba52ca6a..6e2e8007df8 100644 --- a/src/deploy/functions/prepare.spec.ts +++ b/src/deploy/functions/prepare.spec.ts @@ -4,7 +4,6 @@ import * as build from "./build"; import * as prepare from "./prepare"; import * as runtimes from "./runtimes"; import { RuntimeDelegate } from "./runtimes"; -import { RUNTIMES } from "./runtimes/supported"; import { FirebaseError } from "../../error"; import { Options } from "../../options"; import { ValidatedConfig } from "../../functions/projectConfig"; diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts index 2c0b8a6231f..36e4acaf74b 100644 --- a/src/functions/projectConfig.ts +++ b/src/functions/projectConfig.ts @@ -44,11 +44,11 @@ export function validatePrefix(prefix: string): void { if (prefix.length > 30) { throw new FirebaseError("Invalid prefix. Prefix must be 30 characters or less."); } -if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(prefix)) { - throw new FirebaseError( - "Invalid prefix. Prefix can contain only lowercase letters, numeric characters, and dashes, and cannot start or end with a dash.", - ); -} + if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(prefix)) { + throw new FirebaseError( + "Invalid prefix. Prefix can contain only lowercase letters, numeric characters, and dashes, and cannot start or end with a dash.", + ); + } } function validateSingle(config: FunctionConfig): ValidatedSingle { From 95964cb1678072d1ad9df006c84470e1e4bbe558 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 14 Aug 2025 14:10:25 -0700 Subject: [PATCH 09/21] feat: Add applyPrefix function for multi-instance functions Refactor prefix application logic into a centralized applyPrefix function to improve maintainability and consistency across the codebase. The function updates endpoint keys in build objects and handles empty prefixes correctly. Also fixes projectConfig unique key generation to use JSON.stringify for more robust comparison of source-prefix pairs. --- src/deploy/functions/build.spec.ts | 37 ++++++++++++++++++++++++++++++ src/deploy/functions/build.ts | 14 +++++++++++ src/deploy/functions/prepare.ts | 16 ++++--------- src/emulator/functionsEmulator.ts | 8 +------ src/functions/projectConfig.ts | 2 +- 5 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/deploy/functions/build.spec.ts b/src/deploy/functions/build.spec.ts index ff279f8ce47..60c7287a57a 100644 --- a/src/deploy/functions/build.spec.ts +++ b/src/deploy/functions/build.spec.ts @@ -293,3 +293,40 @@ describe("envWithType", () => { expect(out.WHOOPS_SECRET.asString()).to.equal("super-secret"); }); }); + +describe("applyPrefix", () => { + it("updates endpoint keys with prefix and handles empty prefix", () => { + const testBuild: build.Build = { + endpoints: { + func1: { + region: "us-central1", + project: "test-project", + platform: "gcfv2", + runtime: "nodejs18", + entryPoint: "func1", + httpsTrigger: {}, + }, + func2: { + region: "us-west1", + project: "test-project", + platform: "gcfv1", + runtime: "nodejs16", + entryPoint: "func2", + httpsTrigger: {}, + }, + }, + params: [], + requiredAPIs: [], + }; + + // Test with prefix + build.applyPrefix(testBuild, "test"); + expect(Object.keys(testBuild.endpoints).sort()).to.deep.equal(["test-func1", "test-func2"]); + expect(testBuild.endpoints["test-func1"].entryPoint).to.equal("func1"); + expect(testBuild.endpoints["test-func2"].entryPoint).to.equal("func2"); + + // Test empty prefix does nothing + build.applyPrefix(testBuild, ""); + expect(Object.keys(testBuild.endpoints).sort()).to.deep.equal(["test-func1", "test-func2"]); + }); +}); diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 9fca9b58d09..a7e021f05c6 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -650,3 +650,17 @@ function discoverTrigger(endpoint: Endpoint, region: string, r: Resolver): backe } assertExhaustive(endpoint); } + +/** + * Prefixes all endpoint IDs in a build with a given prefix. + */ +export function applyPrefix(build: Build, prefix: string): void { + if (!prefix) { + return; + } + const newEndpoints: Record = {}; + for (const id of Object.keys(build.endpoints)) { + newEndpoints[`${prefix}-${id}`] = build.endpoints[id]; + } + build.endpoints = newEndpoints; +} diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index e4c8129cd11..3860eba671a 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -155,7 +155,7 @@ export async function prepare( } for (const endpoint of backend.allEndpoints(wantBackend)) { - endpoint.environmentVariables = { ...wantBackend.environmentVariables } || {}; + endpoint.environmentVariables = { ...(wantBackend.environmentVariables || {}) }; let resource: string; if (endpoint.platform === "gcfv1") { resource = `projects/${endpoint.project}/locations/${endpoint.region}/functions/${endpoint.id}`; @@ -475,22 +475,16 @@ export async function loadCodebases( "functions", `Loading and analyzing source code for codebase ${codebase} to determine what to deploy`, ); - const build = await runtimeDelegate.discoverBuild(runtimeConfig, { + const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, { ...firebaseEnvs, // Quota project is required when using GCP's Client-based APIs // Some GCP client SDKs, like Vertex AI, requires appropriate quota project setup // in order for .init() calls to succeed. GOOGLE_CLOUD_QUOTA_PROJECT: projectId, }); - if (codebaseConfig.prefix) { - const newEndpoints: Record = {}; - for (const id of Object.keys(build.endpoints)) { - newEndpoints[`${codebaseConfig.prefix}-${id}`] = build.endpoints[id]; - } - build.endpoints = newEndpoints; - } - wantBuilds[codebase] = build; - wantBuilds[codebase].runtime = codebaseConfig.runtime; + build.applyPrefix(discoveredBuild, codebaseConfig.prefix || ""); + wantBuilds[codebase] = discoveredBuild; + wantBuilds[codebase].runtime = discoveredBuild.runtime; } return wantBuilds; } diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 1eaf3dcc699..6922021fb57 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -565,13 +565,7 @@ export class FunctionsEmulator implements EmulatorInstance { ); await this.loadDynamicExtensionBackends(); } - if (emulatableBackend.prefix) { - const newEndpoints: Record = {}; - for (const id of Object.keys(discoveredBuild.endpoints)) { - newEndpoints[`${emulatableBackend.prefix}-${id}`] = discoveredBuild.endpoints[id]; - } - discoveredBuild.endpoints = newEndpoints; - } + build.applyPrefix(discoveredBuild, emulatableBackend.prefix || ""); const resolution = await resolveBackend({ build: discoveredBuild, firebaseConfig: JSON.parse(firebaseConfig), diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts index 36e4acaf74b..b28d8c78c6e 100644 --- a/src/functions/projectConfig.ts +++ b/src/functions/projectConfig.ts @@ -92,7 +92,7 @@ export function assertUnique( function assertUniqueSourcePrefixPair(config: ValidatedConfig): void { const sourcePrefixPairs = new Set(); for (const c of config) { - const key = `${c.source}-${c.prefix || ""}`; + const key = JSON.stringify({ source: c.source, prefix: c.prefix || "" }); if (sourcePrefixPairs.has(key)) { throw new FirebaseError( `More than one functions config specifies the same source directory ('${ From a7697b502f221205a6c51f7ec1619a9a041623e5 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 14 Aug 2025 14:28:12 -0700 Subject: [PATCH 10/21] refactor: improve applyPrefix tests and remove redundant assignment ### Description - Refactor applyPrefix test to use factory function and separate test cases to avoid mutation side effects - Remove redundant runtime assignment in prepare.ts since wantBuilds[codebase] is already assigned to discoveredBuild ### Scenarios Tested - applyPrefix function with prefix and empty prefix cases - Existing test suite continues to pass ### Sample Commands npm test npm run mocha:fast --- src/deploy/functions/build.spec.ts | 52 ++++++++++++++++-------------- src/deploy/functions/prepare.ts | 1 - 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/deploy/functions/build.spec.ts b/src/deploy/functions/build.spec.ts index 60c7287a57a..5380fb06b4d 100644 --- a/src/deploy/functions/build.spec.ts +++ b/src/deploy/functions/build.spec.ts @@ -295,38 +295,40 @@ describe("envWithType", () => { }); describe("applyPrefix", () => { - it("updates endpoint keys with prefix and handles empty prefix", () => { - const testBuild: build.Build = { - endpoints: { - func1: { - region: "us-central1", - project: "test-project", - platform: "gcfv2", - runtime: "nodejs18", - entryPoint: "func1", - httpsTrigger: {}, - }, - func2: { - region: "us-west1", - project: "test-project", - platform: "gcfv1", - runtime: "nodejs16", - entryPoint: "func2", - httpsTrigger: {}, - }, + const createTestBuild = (): build.Build => ({ + endpoints: { + func1: { + region: "us-central1", + project: "test-project", + platform: "gcfv2", + runtime: "nodejs18", + entryPoint: "func1", + httpsTrigger: {}, }, - params: [], - requiredAPIs: [], - }; + func2: { + region: "us-west1", + project: "test-project", + platform: "gcfv1", + runtime: "nodejs16", + entryPoint: "func2", + httpsTrigger: {}, + }, + }, + params: [], + requiredAPIs: [], + }); - // Test with prefix + it("should update endpoint keys with prefix", () => { + const testBuild = createTestBuild(); build.applyPrefix(testBuild, "test"); expect(Object.keys(testBuild.endpoints).sort()).to.deep.equal(["test-func1", "test-func2"]); expect(testBuild.endpoints["test-func1"].entryPoint).to.equal("func1"); expect(testBuild.endpoints["test-func2"].entryPoint).to.equal("func2"); + }); - // Test empty prefix does nothing + it("should do nothing for an empty prefix", () => { + const testBuild = createTestBuild(); build.applyPrefix(testBuild, ""); - expect(Object.keys(testBuild.endpoints).sort()).to.deep.equal(["test-func1", "test-func2"]); + expect(Object.keys(testBuild.endpoints).sort()).to.deep.equal(["func1", "func2"]); }); }); diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index 3860eba671a..93fc2667e8b 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -484,7 +484,6 @@ export async function loadCodebases( }); build.applyPrefix(discoveredBuild, codebaseConfig.prefix || ""); wantBuilds[codebase] = discoveredBuild; - wantBuilds[codebase].runtime = discoveredBuild.runtime; } return wantBuilds; } From c54603a0ecae4479fb663aecd00869fe55a8b0f4 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Thu, 14 Aug 2025 20:30:57 -0700 Subject: [PATCH 11/21] feat: Apply prefix to secret names in multi-instance functions When using prefixed multi-instance function deployments, secret names in secretEnvironmentVariables are now also prefixed to ensure proper isolation between different instances. The environment variable keys remain unchanged so function code doesn't need modification. Also fixes missing runtime assignment that was accidentally removed in a previous refactor and adds test coverage for runtime preservation. --- src/deploy/functions/build.spec.ts | 46 ++++++++++++++++++++++++++++ src/deploy/functions/build.ts | 10 +++++- src/deploy/functions/prepare.spec.ts | 18 +++++++++++ src/deploy/functions/prepare.ts | 1 + 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/deploy/functions/build.spec.ts b/src/deploy/functions/build.spec.ts index 5380fb06b4d..c88f96eab8b 100644 --- a/src/deploy/functions/build.spec.ts +++ b/src/deploy/functions/build.spec.ts @@ -331,4 +331,50 @@ describe("applyPrefix", () => { build.applyPrefix(testBuild, ""); expect(Object.keys(testBuild.endpoints).sort()).to.deep.equal(["func1", "func2"]); }); + + it("should prefix secret names in secretEnvironmentVariables", () => { + const testBuild: build.Build = { + endpoints: { + func1: { + region: "us-central1", + project: "test-project", + platform: "gcfv2", + runtime: "nodejs18", + entryPoint: "func1", + httpsTrigger: {}, + secretEnvironmentVariables: [ + { key: "API_KEY", secret: "api-secret", projectId: "test-project" }, + { key: "DB_PASSWORD", secret: "db-secret", projectId: "test-project" }, + ], + }, + func2: { + region: "us-west1", + project: "test-project", + platform: "gcfv1", + runtime: "nodejs16", + entryPoint: "func2", + httpsTrigger: {}, + secretEnvironmentVariables: [ + { key: "SERVICE_TOKEN", secret: "service-secret", projectId: "test-project" }, + ], + }, + }, + params: [], + requiredAPIs: [], + }; + + build.applyPrefix(testBuild, "staging"); + + expect(Object.keys(testBuild.endpoints).sort()).to.deep.equal([ + "staging-func1", + "staging-func2", + ]); + expect(testBuild.endpoints["staging-func1"].secretEnvironmentVariables).to.deep.equal([ + { key: "API_KEY", secret: "staging-api-secret", projectId: "test-project" }, + { key: "DB_PASSWORD", secret: "staging-db-secret", projectId: "test-project" }, + ]); + expect(testBuild.endpoints["staging-func2"].secretEnvironmentVariables).to.deep.equal([ + { key: "SERVICE_TOKEN", secret: "staging-service-secret", projectId: "test-project" }, + ]); + }); }); diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index a7e021f05c6..c85cdbdacca 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -660,7 +660,15 @@ export function applyPrefix(build: Build, prefix: string): void { } const newEndpoints: Record = {}; for (const id of Object.keys(build.endpoints)) { - newEndpoints[`${prefix}-${id}`] = build.endpoints[id]; + const endpoint = build.endpoints[id]; + newEndpoints[`${prefix}-${id}`] = endpoint; + + if (endpoint.secretEnvironmentVariables) { + endpoint.secretEnvironmentVariables = endpoint.secretEnvironmentVariables.map((secret) => ({ + ...secret, + secret: `${prefix}-${secret.secret}`, + })); + } } build.endpoints = newEndpoints; } diff --git a/src/deploy/functions/prepare.spec.ts b/src/deploy/functions/prepare.spec.ts index 6e2e8007df8..93b8a400f86 100644 --- a/src/deploy/functions/prepare.spec.ts +++ b/src/deploy/functions/prepare.spec.ts @@ -80,6 +80,24 @@ describe("prepare", () => { expect(Object.keys(builds.codebase.endpoints)).to.deep.equal(["my-prefix-test"]); }); + + it("should preserve runtime from codebase config", async () => { + const config: ValidatedConfig = [ + { source: "source", codebase: "codebase", runtime: "nodejs20" }, + ]; + const options = { + config: { + path: (p: string) => p, + }, + projectId: "project", + } as unknown as Options; + const firebaseConfig = { projectId: "project" }; + const runtimeConfig = {}; + + const builds = await prepare.loadCodebases(config, options, firebaseConfig, runtimeConfig); + + expect(builds.codebase.runtime).to.equal("nodejs20"); + }); }); describe("inferDetailsFromExisting", () => { diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index 93fc2667e8b..a0915353a90 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -482,6 +482,7 @@ export async function loadCodebases( // in order for .init() calls to succeed. GOOGLE_CLOUD_QUOTA_PROJECT: projectId, }); + discoveredBuild.runtime = codebaseConfig.runtime; build.applyPrefix(discoveredBuild, codebaseConfig.prefix || ""); wantBuilds[codebase] = discoveredBuild; } From 3220384fee4293d0324fdb3a218d3960c7fadce4 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 12:55:49 -0700 Subject: [PATCH 12/21] functions: add prefix validation and ID guard; apply prefix to secrets; JSON schema description; tests for overflow and invalid prefix --- schema/firebase-config.json | 3174 +++++++++++++-------------- src/deploy/functions/build.spec.ts | 32 + src/deploy/functions/build.ts | 17 +- src/firebaseConfig.ts | 3 + src/functions/projectConfig.spec.ts | 7 + src/functions/projectConfig.ts | 5 +- 6 files changed, 1573 insertions(+), 1665 deletions(-) diff --git a/schema/firebase-config.json b/schema/firebase-config.json index c86db3f356c..43badcc6cac 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -1,1770 +1,1620 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "definitions": { - "DataConnectSingle": { - "additionalProperties": false, - "properties": { - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "source": { - "type": "string" - } + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "DataConnectSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" }, - "required": [ - "source" - ], - "type": "object" + { + "type": "string" + } + ] }, - "DatabaseSingle": { - "additionalProperties": false, - "properties": { - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - } + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" }, - "required": [ - "rules" - ], - "type": "object" + { + "type": "string" + } + ] }, - "ExtensionsConfig": { - "additionalProperties": false, - "type": "object" + "source": { + "type": "string" + } + }, + "required": ["source"], + "type": "object" + }, + "DatabaseSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] }, - "FirestoreSingle": { - "additionalProperties": false, - "properties": { - "database": { + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + } + }, + "required": ["rules"], + "type": "object" + }, + "ExtensionsConfig": { + "additionalProperties": false, + "type": "object" + }, + "FirestoreSingle": { + "additionalProperties": false, + "properties": { + "database": { + "type": "string" + }, + "edition": { + "type": "string" + }, + "indexes": { + "type": "string" + }, + "location": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + } + }, + "type": "object" + }, + "FrameworksBackendOptions": { + "additionalProperties": false, + "properties": { + "concurrency": { + "description": "Number of requests a function can serve at once.", + "type": "number" + }, + "cors": { + "description": "If true, allows CORS on requests to this function.\nIf this is a `string` or `RegExp`, allows requests from domains that match the provided value.\nIf this is an `Array`, allows requests from domains matching at least one entry of the array.\nDefaults to true for {@link https.CallableFunction} and false otherwise.", + "type": ["string", "boolean"] + }, + "cpu": { + "anyOf": [ + { + "const": "gcf_gen1", + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Fractional number of CPUs to allocate to a function." + }, + "enforceAppCheck": { + "description": "Determines whether Firebase AppCheck is enforced. Defaults to false.", + "type": "boolean" + }, + "ingressSettings": { + "description": "Ingress settings which control where this function can be called from.", + "enum": ["ALLOW_ALL", "ALLOW_INTERNAL_AND_GCLB", "ALLOW_INTERNAL_ONLY"], + "type": "string" + }, + "invoker": { + "const": "public", + "description": "Invoker to set access control on https functions.", + "type": "string" + }, + "labels": { + "$ref": "#/definitions/Record", + "description": "User labels to set on the function." + }, + "maxInstances": { + "description": "Max number of instances to be running in parallel.", + "type": "number" + }, + "memory": { + "description": "Amount of memory to allocate to a function.", + "enum": ["128MiB", "16GiB", "1GiB", "256MiB", "2GiB", "32GiB", "4GiB", "512MiB", "8GiB"], + "type": "string" + }, + "minInstances": { + "description": "Min number of actual instances to be running at a given time.", + "type": "number" + }, + "omit": { + "description": "If true, do not deploy or emulate this function.", + "type": "boolean" + }, + "preserveExternalChanges": { + "description": "Controls whether function configuration modified outside of function source is preserved. Defaults to false.", + "type": "boolean" + }, + "region": { + "description": "HTTP functions can override global options and can specify multiple regions to deploy to.", + "type": "string" + }, + "secrets": { + "items": { + "type": "string" + }, + "type": "array" + }, + "serviceAccount": { + "description": "Specific service account for the function to run as.", + "type": "string" + }, + "timeoutSeconds": { + "description": "Timeout for the function in seconds, possible values are 0 to 540.\nHTTPS functions can specify a higher timeout.", + "type": "number" + }, + "vpcConnector": { + "description": "Connect cloud function to specified VPC connector.", + "type": "string" + }, + "vpcConnectorEgressSettings": { + "description": "Egress settings for VPC connector.", + "enum": ["ALL_TRAFFIC", "PRIVATE_RANGES_ONLY"], + "type": "string" + } + }, + "type": "object" + }, + "FunctionConfig": { + "additionalProperties": false, + "properties": { + "codebase": { + "type": "string" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "prefix": { + "type": "string", + "description": "Optional prefix to apply to all function IDs and secret names in this codebase. Must start with a lowercase letter, contain only lowercase letters, numbers, and dashes, cannot start or end with a dash, and be at most 30 characters.", + "maxLength": 30, + "pattern": "^[a-z](?:[a-z0-9-]*[a-z0-9])?$" + }, + "runtime": { + "enum": [ + "nodejs18", + "nodejs20", + "nodejs22", + "python310", + "python311", + "python312", + "python313" + ], + "type": "string" + }, + "source": { + "type": "string" + } + }, + "type": "object" + }, + "HostingHeaders": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "glob": { + "type": "string" + }, + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { "type": "string" - }, - "edition": { + }, + "value": { "type": "string" - }, - "indexes": { + } + }, + "required": ["key", "value"], + "type": "object" + }, + "type": "array" + } + }, + "required": ["glob", "headers"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { "type": "string" - }, - "location": { + }, + "value": { "type": "string" + } }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - } + "required": ["key", "value"], + "type": "object" + }, + "type": "array" }, - "type": "object" + "source": { + "type": "string" + } + }, + "required": ["headers", "source"], + "type": "object" }, - "FrameworksBackendOptions": { - "additionalProperties": false, - "properties": { - "concurrency": { - "description": "Number of requests a function can serve at once.", - "type": "number" - }, - "cors": { - "description": "If true, allows CORS on requests to this function.\nIf this is a `string` or `RegExp`, allows requests from domains that match the provided value.\nIf this is an `Array`, allows requests from domains matching at least one entry of the array.\nDefaults to true for {@link https.CallableFunction} and false otherwise.", - "type": [ - "string", - "boolean" - ] - }, - "cpu": { - "anyOf": [ - { - "const": "gcf_gen1", - "type": "string" - }, - { - "type": "number" - } - ], - "description": "Fractional number of CPUs to allocate to a function." - }, - "enforceAppCheck": { - "description": "Determines whether Firebase AppCheck is enforced. Defaults to false.", - "type": "boolean" - }, - "ingressSettings": { - "description": "Ingress settings which control where this function can be called from.", - "enum": [ - "ALLOW_ALL", - "ALLOW_INTERNAL_AND_GCLB", - "ALLOW_INTERNAL_ONLY" - ], + { + "additionalProperties": false, + "properties": { + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { "type": "string" - }, - "invoker": { - "const": "public", - "description": "Invoker to set access control on https functions.", + }, + "value": { "type": "string" + } }, - "labels": { - "$ref": "#/definitions/Record", - "description": "User labels to set on the function." - }, - "maxInstances": { - "description": "Max number of instances to be running in parallel.", - "type": "number" - }, - "memory": { - "description": "Amount of memory to allocate to a function.", - "enum": [ - "128MiB", - "16GiB", - "1GiB", - "256MiB", - "2GiB", - "32GiB", - "4GiB", - "512MiB", - "8GiB" - ], - "type": "string" + "required": ["key", "value"], + "type": "object" + }, + "type": "array" + }, + "regex": { + "type": "string" + } + }, + "required": ["headers", "regex"], + "type": "object" + } + ] + }, + "HostingRedirects": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "glob": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": ["destination", "glob"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": ["destination", "source"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "regex": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": ["destination", "regex"], + "type": "object" + } + ] + }, + "HostingRewrites": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "glob": { + "type": "string" + } + }, + "required": ["destination", "glob"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "glob": { + "type": "string" + }, + "region": { + "type": "string" + } + }, + "required": ["function", "glob"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" }, - "minInstances": { - "description": "Min number of actual instances to be running at a given time.", - "type": "number" + "region": { + "type": "string" + } + }, + "required": ["functionId"], + "type": "object" + }, + "glob": { + "type": "string" + } + }, + "required": ["function", "glob"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "glob": { + "type": "string" + }, + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" }, - "omit": { - "description": "If true, do not deploy or emulate this function.", - "type": "boolean" + "region": { + "type": "string" }, - "preserveExternalChanges": { - "description": "Controls whether function configuration modified outside of function source is preserved. Defaults to false.", - "type": "boolean" + "serviceId": { + "type": "string" + } + }, + "required": ["serviceId"], + "type": "object" + } + }, + "required": ["glob", "run"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "glob": { + "type": "string" + } + }, + "required": ["dynamicLinks", "glob"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": ["destination", "source"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "region": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": ["function", "source"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" }, "region": { - "description": "HTTP functions can override global options and can specify multiple regions to deploy to.", - "type": "string" + "type": "string" + } + }, + "required": ["functionId"], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": ["function", "source"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" }, - "secrets": { - "items": { - "type": "string" - }, - "type": "array" + "region": { + "type": "string" }, - "serviceAccount": { - "description": "Specific service account for the function to run as.", - "type": "string" + "serviceId": { + "type": "string" + } + }, + "required": ["serviceId"], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": ["run", "source"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "source": { + "type": "string" + } + }, + "required": ["dynamicLinks", "source"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "regex": { + "type": "string" + } + }, + "required": ["destination", "regex"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "regex": { + "type": "string" + }, + "region": { + "type": "string" + } + }, + "required": ["function", "regex"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" }, - "timeoutSeconds": { - "description": "Timeout for the function in seconds, possible values are 0 to 540.\nHTTPS functions can specify a higher timeout.", - "type": "number" + "region": { + "type": "string" + } + }, + "required": ["functionId"], + "type": "object" + }, + "regex": { + "type": "string" + } + }, + "required": ["function", "regex"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "regex": { + "type": "string" + }, + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" }, - "vpcConnector": { - "description": "Connect cloud function to specified VPC connector.", - "type": "string" + "region": { + "type": "string" }, - "vpcConnectorEgressSettings": { - "description": "Egress settings for VPC connector.", - "enum": [ - "ALL_TRAFFIC", - "PRIVATE_RANGES_ONLY" - ], - "type": "string" + "serviceId": { + "type": "string" } + }, + "required": ["serviceId"], + "type": "object" + } + }, + "required": ["regex", "run"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" }, - "type": "object" + "regex": { + "type": "string" + } + }, + "required": ["dynamicLinks", "regex"], + "type": "object" + } + ] + }, + "HostingSingle": { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": ["AUTO", "NONE"], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { + "items": { + "$ref": "#/definitions/HostingHeaders" + }, + "type": "array" + }, + "i18n": { + "additionalProperties": false, + "properties": { + "root": { + "type": "string" + } + }, + "required": ["root"], + "type": "object" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/definitions/HostingRedirects" + }, + "type": "array" + }, + "rewrites": { + "items": { + "$ref": "#/definitions/HostingRewrites" + }, + "type": "array" + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" }, - "FunctionConfig": { + "target": { + "type": "string" + }, + "trailingSlash": { + "type": "boolean" + } + }, + "type": "object" + }, + "Record": { + "additionalProperties": false, + "type": "object" + }, + "RemoteConfigConfig": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "template": { + "type": "string" + } + }, + "required": ["template"], + "type": "object" + }, + "StorageSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": ["rules"], + "type": "object" + } + }, + "properties": { + "$schema": { + "format": "uri", + "type": "string" + }, + "apphosting": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "alwaysDeployFromSource": { + "type": "boolean" + }, + "backendId": { + "type": "string" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "rootDir": { + "type": "string" + } + }, + "required": ["backendId", "ignore", "rootDir"], + "type": "object" + }, + { + "items": { "additionalProperties": false, "properties": { - "codebase": { + "alwaysDeployFromSource": { + "type": "boolean" + }, + "backendId": { + "type": "string" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "rootDir": { + "type": "string" + } + }, + "required": ["backendId", "ignore", "rootDir"], + "type": "object" + }, + "type": "array" + } + ] + }, + "database": { + "anyOf": [ + { + "$ref": "#/definitions/DatabaseSingle" + }, + { + "items": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "instance": { "type": "string" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { + }, + "postdeploy": { "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + { + "items": { + "type": "string" }, - { - "type": "string" - } + "type": "array" + }, + { + "type": "string" + } ] - }, - "predeploy": { + }, + "predeploy": { "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + { + "items": { + "type": "string" }, - { - "type": "string" - } + "type": "array" + }, + { + "type": "string" + } ] - }, - "prefix": { + }, + "rules": { "type": "string" - }, - "runtime": { - "enum": [ - "nodejs18", - "nodejs20", - "nodejs22", - "python310", - "python311", - "python312", - "python313" - ], + }, + "target": { "type": "string" - }, - "source": { + } + }, + "required": ["instance", "rules"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "instance": { "type": "string" - } - }, - "type": "object" - }, - "HostingHeaders": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "glob": { - "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" }, - "headers": { - "items": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "required": [ - "key", - "value" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "glob", - "headers" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "headers": { - "items": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "required": [ - "key", - "value" - ], - "type": "object" - }, - "type": "array" + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" }, - "source": { - "type": "string" - } - }, - "required": [ - "headers", - "source" - ], - "type": "object" + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } }, - { - "additionalProperties": false, - "properties": { - "headers": { - "items": { - "additionalProperties": false, - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "required": [ - "key", - "value" - ], - "type": "object" - }, - "type": "array" - }, - "regex": { - "type": "string" - } - }, - "required": [ - "headers", - "regex" - ], - "type": "object" - } + "required": ["rules", "target"], + "type": "object" + } ] + }, + "type": "array" + } + ] + }, + "dataconnect": { + "anyOf": [ + { + "$ref": "#/definitions/DataConnectSingle" }, - "HostingRedirects": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "glob": { - "type": "string" - }, - "type": { - "type": "number" - } - }, - "required": [ - "destination", - "glob" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "source": { - "type": "string" - }, - "type": { - "type": "number" - } - }, - "required": [ - "destination", - "source" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "regex": { - "type": "string" - }, - "type": { - "type": "number" - } - }, - "required": [ - "destination", - "regex" - ], - "type": "object" - } - ] + { + "items": { + "$ref": "#/definitions/DataConnectSingle" + }, + "type": "array" + } + ] + }, + "emulators": { + "additionalProperties": false, + "properties": { + "apphosting": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "rootDirectory": { + "type": "string" + }, + "startCommand": { + "type": "string" + }, + "startCommandOverride": { + "type": "string" + } + }, + "type": "object" }, - "HostingRewrites": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "glob": { - "type": "string" - } - }, - "required": [ - "destination", - "glob" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "type": "string" - }, - "glob": { - "type": "string" - }, - "region": { - "type": "string" - } - }, - "required": [ - "function", - "glob" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "additionalProperties": false, - "properties": { - "functionId": { - "type": "string" - }, - "pinTag": { - "type": "boolean" - }, - "region": { - "type": "string" - } - }, - "required": [ - "functionId" - ], - "type": "object" - }, - "glob": { - "type": "string" - } - }, - "required": [ - "function", - "glob" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "glob": { - "type": "string" - }, - "run": { - "additionalProperties": false, - "properties": { - "pinTag": { - "type": "boolean" - }, - "region": { - "type": "string" - }, - "serviceId": { - "type": "string" - } - }, - "required": [ - "serviceId" - ], - "type": "object" - } - }, - "required": [ - "glob", - "run" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "dynamicLinks": { - "type": "boolean" - }, - "glob": { - "type": "string" - } - }, - "required": [ - "dynamicLinks", - "glob" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "source": { - "type": "string" - } - }, - "required": [ - "destination", - "source" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "type": "string" - }, - "region": { - "type": "string" - }, - "source": { - "type": "string" - } - }, - "required": [ - "function", - "source" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "additionalProperties": false, - "properties": { - "functionId": { - "type": "string" - }, - "pinTag": { - "type": "boolean" - }, - "region": { - "type": "string" - } - }, - "required": [ - "functionId" - ], - "type": "object" - }, - "source": { - "type": "string" - } - }, - "required": [ - "function", - "source" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "run": { - "additionalProperties": false, - "properties": { - "pinTag": { - "type": "boolean" - }, - "region": { - "type": "string" - }, - "serviceId": { - "type": "string" - } - }, - "required": [ - "serviceId" - ], - "type": "object" - }, - "source": { - "type": "string" - } - }, - "required": [ - "run", - "source" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "dynamicLinks": { - "type": "boolean" - }, - "source": { - "type": "string" - } - }, - "required": [ - "dynamicLinks", - "source" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "regex": { - "type": "string" - } - }, - "required": [ - "destination", - "regex" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "type": "string" - }, - "regex": { - "type": "string" - }, - "region": { - "type": "string" - } - }, - "required": [ - "function", - "regex" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "additionalProperties": false, - "properties": { - "functionId": { - "type": "string" - }, - "pinTag": { - "type": "boolean" - }, - "region": { - "type": "string" - } - }, - "required": [ - "functionId" - ], - "type": "object" - }, - "regex": { - "type": "string" - } - }, - "required": [ - "function", - "regex" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "regex": { - "type": "string" - }, - "run": { - "additionalProperties": false, - "properties": { - "pinTag": { - "type": "boolean" - }, - "region": { - "type": "string" - }, - "serviceId": { - "type": "string" - } - }, - "required": [ - "serviceId" - ], - "type": "object" - } - }, - "required": [ - "regex", - "run" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "dynamicLinks": { - "type": "boolean" - }, - "regex": { - "type": "string" - } - }, - "required": [ - "dynamicLinks", - "regex" - ], - "type": "object" - } - ] + "auth": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" }, - "HostingSingle": { - "additionalProperties": false, - "properties": { - "appAssociation": { - "enum": [ - "AUTO", - "NONE" - ], - "type": "string" - }, - "cleanUrls": { - "type": "boolean" - }, - "frameworksBackend": { - "$ref": "#/definitions/FrameworksBackendOptions" - }, - "headers": { - "items": { - "$ref": "#/definitions/HostingHeaders" - }, - "type": "array" - }, - "i18n": { - "additionalProperties": false, - "properties": { - "root": { - "type": "string" - } - }, - "required": [ - "root" - ], - "type": "object" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { + "database": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "dataconnect": { + "additionalProperties": false, + "properties": { + "dataDir": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "postgresHost": { + "type": "string" + }, + "postgresPort": { + "type": "number" + } + }, + "type": "object" + }, + "eventarc": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "extensions": { + "properties": {}, + "type": "object" + }, + "firestore": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "websocketPort": { + "type": "number" + } + }, + "type": "object" + }, + "functions": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "hosting": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "hub": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "logging": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "pubsub": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "singleProjectMode": { + "type": "boolean" + }, + "storage": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "tasks": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "ui": { + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "extensions": { + "$ref": "#/definitions/ExtensionsConfig" + }, + "firestore": { + "anyOf": [ + { + "$ref": "#/definitions/FirestoreSingle" + }, + { + "items": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "database": { + "type": "string" + }, + "indexes": { + "type": "string" + }, + "postdeploy": { "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + { + "items": { + "type": "string" }, - { - "type": "string" - } + "type": "array" + }, + { + "type": "string" + } ] - }, - "predeploy": { + }, + "predeploy": { "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + { + "items": { + "type": "string" }, - { - "type": "string" - } + "type": "array" + }, + { + "type": "string" + } ] - }, - "public": { + }, + "rules": { "type": "string" - }, - "redirects": { - "items": { - "$ref": "#/definitions/HostingRedirects" - }, - "type": "array" - }, - "rewrites": { - "items": { - "$ref": "#/definitions/HostingRewrites" - }, - "type": "array" - }, - "site": { + }, + "target": { "type": "string" - }, - "source": { + } + }, + "required": ["target"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "database": { "type": "string" - }, - "target": { + }, + "indexes": { "type": "string" - }, - "trailingSlash": { - "type": "boolean" - } - }, - "type": "object" - }, - "Record": { - "additionalProperties": false, - "type": "object" - }, - "RemoteConfigConfig": { - "additionalProperties": false, - "properties": { - "postdeploy": { + }, + "postdeploy": { "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + { + "items": { + "type": "string" }, - { - "type": "string" - } + "type": "array" + }, + { + "type": "string" + } ] - }, - "predeploy": { + }, + "predeploy": { "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + { + "items": { + "type": "string" }, - { - "type": "string" - } + "type": "array" + }, + { + "type": "string" + } ] - }, - "template": { + }, + "rules": { "type": "string" - } - }, - "required": [ - "template" - ], - "type": "object" - }, - "StorageSingle": { - "additionalProperties": false, - "properties": { - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { + }, + "target": { "type": "string" + } }, - "target": { - "type": "string" - } - }, - "required": [ - "rules" - ], - "type": "object" + "required": ["database"], + "type": "object" + } + ] + }, + "type": "array" } + ] }, - "properties": { - "$schema": { - "format": "uri", - "type": "string" + "functions": { + "anyOf": [ + { + "$ref": "#/definitions/FunctionConfig" }, - "apphosting": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "alwaysDeployFromSource": { - "type": "boolean" - }, - "backendId": { - "type": "string" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "rootDir": { - "type": "string" - } - }, - "required": [ - "backendId", - "ignore", - "rootDir" - ], - "type": "object" - }, - { - "items": { - "additionalProperties": false, - "properties": { - "alwaysDeployFromSource": { - "type": "boolean" - }, - "backendId": { - "type": "string" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "rootDir": { - "type": "string" - } - }, - "required": [ - "backendId", - "ignore", - "rootDir" - ], - "type": "object" - }, - "type": "array" - } - ] - }, - "database": { - "anyOf": [ - { - "$ref": "#/definitions/DatabaseSingle" - }, - { - "items": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "instance": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "instance", - "rules" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "instance": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "rules", - "target" - ], - "type": "object" - } - ] - }, - "type": "array" - } - ] + { + "items": { + "$ref": "#/definitions/FunctionConfig" + }, + "type": "array" + } + ] + }, + "hosting": { + "anyOf": [ + { + "$ref": "#/definitions/HostingSingle" }, - "dataconnect": { + { + "items": { "anyOf": [ - { - "$ref": "#/definitions/DataConnectSingle" - }, - { + { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": ["AUTO", "NONE"], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { "items": { - "$ref": "#/definitions/DataConnectSingle" + "$ref": "#/definitions/HostingHeaders" }, "type": "array" - } - ] - }, - "emulators": { - "additionalProperties": false, - "properties": { - "apphosting": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - }, - "rootDirectory": { - "type": "string" - }, - "startCommand": { - "type": "string" - }, - "startCommandOverride": { - "type": "string" - } - }, - "type": "object" - }, - "auth": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "database": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "dataconnect": { - "additionalProperties": false, - "properties": { - "dataDir": { - "type": "string" - }, - "host": { - "type": "string" - }, - "port": { - "type": "number" - }, - "postgresHost": { - "type": "string" - }, - "postgresPort": { - "type": "number" - } - }, - "type": "object" - }, - "eventarc": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "extensions": { - "properties": {}, - "type": "object" - }, - "firestore": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - }, - "websocketPort": { - "type": "number" - } - }, - "type": "object" - }, - "functions": { + }, + "i18n": { "additionalProperties": false, "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } + "root": { + "type": "string" + } }, + "required": ["root"], "type": "object" - }, - "hosting": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } + }, + "ignore": { + "items": { + "type": "string" }, - "type": "object" - }, - "hub": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "logging": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" }, - "port": { - "type": "number" - } + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/definitions/HostingRedirects" }, - "type": "object" - }, - "pubsub": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } + "type": "array" + }, + "rewrites": { + "items": { + "$ref": "#/definitions/HostingRewrites" }, - "type": "object" - }, - "singleProjectMode": { + "type": "array" + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "trailingSlash": { "type": "boolean" - }, - "storage": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } + } + }, + "required": ["target"], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": ["AUTO", "NONE"], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { + "items": { + "$ref": "#/definitions/HostingHeaders" }, - "type": "object" - }, - "tasks": { + "type": "array" + }, + "i18n": { "additionalProperties": false, "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } + "root": { + "type": "string" + } }, + "required": ["root"], "type": "object" - }, - "ui": { - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" }, - "host": { - "type": "string" + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" }, - "port": { - "type": "number" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "extensions": { - "$ref": "#/definitions/ExtensionsConfig" - }, - "firestore": { - "anyOf": [ - { - "$ref": "#/definitions/FirestoreSingle" - }, - { + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { "items": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "database": { - "type": "string" - }, - "indexes": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "target" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "database": { - "type": "string" - }, - "indexes": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "database" - ], - "type": "object" - } - ] + "$ref": "#/definitions/HostingRedirects" }, "type": "array" - } - ] - }, - "functions": { - "anyOf": [ - { - "$ref": "#/definitions/FunctionConfig" - }, - { + }, + "rewrites": { "items": { - "$ref": "#/definitions/FunctionConfig" + "$ref": "#/definitions/HostingRewrites" }, "type": "array" - } + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "trailingSlash": { + "type": "boolean" + } + }, + "required": ["site"], + "type": "object" + } ] + }, + "type": "array" + } + ] + }, + "remoteconfig": { + "$ref": "#/definitions/RemoteConfigConfig" + }, + "storage": { + "anyOf": [ + { + "$ref": "#/definitions/StorageSingle" }, - "hosting": { - "anyOf": [ - { - "$ref": "#/definitions/HostingSingle" - }, - { + { + "items": { + "additionalProperties": false, + "properties": { + "bucket": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { "items": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "appAssociation": { - "enum": [ - "AUTO", - "NONE" - ], - "type": "string" - }, - "cleanUrls": { - "type": "boolean" - }, - "frameworksBackend": { - "$ref": "#/definitions/FrameworksBackendOptions" - }, - "headers": { - "items": { - "$ref": "#/definitions/HostingHeaders" - }, - "type": "array" - }, - "i18n": { - "additionalProperties": false, - "properties": { - "root": { - "type": "string" - } - }, - "required": [ - "root" - ], - "type": "object" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "public": { - "type": "string" - }, - "redirects": { - "items": { - "$ref": "#/definitions/HostingRedirects" - }, - "type": "array" - }, - "rewrites": { - "items": { - "$ref": "#/definitions/HostingRewrites" - }, - "type": "array" - }, - "site": { - "type": "string" - }, - "source": { - "type": "string" - }, - "target": { - "type": "string" - }, - "trailingSlash": { - "type": "boolean" - } - }, - "required": [ - "target" - ], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "appAssociation": { - "enum": [ - "AUTO", - "NONE" - ], - "type": "string" - }, - "cleanUrls": { - "type": "boolean" - }, - "frameworksBackend": { - "$ref": "#/definitions/FrameworksBackendOptions" - }, - "headers": { - "items": { - "$ref": "#/definitions/HostingHeaders" - }, - "type": "array" - }, - "i18n": { - "additionalProperties": false, - "properties": { - "root": { - "type": "string" - } - }, - "required": [ - "root" - ], - "type": "object" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "public": { - "type": "string" - }, - "redirects": { - "items": { - "$ref": "#/definitions/HostingRedirects" - }, - "type": "array" - }, - "rewrites": { - "items": { - "$ref": "#/definitions/HostingRewrites" - }, - "type": "array" - }, - "site": { - "type": "string" - }, - "source": { - "type": "string" - }, - "target": { - "type": "string" - }, - "trailingSlash": { - "type": "boolean" - } - }, - "required": [ - "site" - ], - "type": "object" - } - ] + "type": "string" }, "type": "array" - } - ] - }, - "remoteconfig": { - "$ref": "#/definitions/RemoteConfigConfig" - }, - "storage": { - "anyOf": [ - { - "$ref": "#/definitions/StorageSingle" - }, - { + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { "items": { - "additionalProperties": false, - "properties": { - "bucket": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": [ - "bucket", - "rules" - ], - "type": "object" + "type": "string" }, "type": "array" - } - ] + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": ["bucket", "rules"], + "type": "object" + }, + "type": "array" } - }, - "type": "object" + ] + } + }, + "type": "object" } - diff --git a/src/deploy/functions/build.spec.ts b/src/deploy/functions/build.spec.ts index c88f96eab8b..64b604aa164 100644 --- a/src/deploy/functions/build.spec.ts +++ b/src/deploy/functions/build.spec.ts @@ -377,4 +377,36 @@ describe("applyPrefix", () => { { key: "SERVICE_TOKEN", secret: "staging-service-secret", projectId: "test-project" }, ]); }); + + it("throws if combined function id exceeds 63 characters", () => { + const longId = "a".repeat(34); // with 30-char prefix + dash = 65 total + const testBuild: build.Build = build.of({ + [longId]: { + region: "us-central1", + project: "test-project", + platform: "gcfv2", + runtime: "nodejs18", + entryPoint: longId, + httpsTrigger: {}, + }, + }); + const longPrefix = "p".repeat(30); + expect(() => build.applyPrefix(testBuild, longPrefix)).to.throw(/exceeds 63 characters/); + }); + + it("throws if prefix makes function id invalid (must start with a letter)", () => { + const testBuild: build.Build = build.of({ + func: { + region: "us-central1", + project: "test-project", + platform: "gcfv2", + runtime: "nodejs18", + entryPoint: "func", + httpsTrigger: {}, + }, + }); + expect(() => build.applyPrefix(testBuild, "1abc")).to.throw( + /Function names must start with a letter/, + ); + }); }); diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 1a60c57cf54..bdd676b92ec 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -662,7 +662,22 @@ export function applyPrefix(build: Build, prefix: string): void { const newEndpoints: Record = {}; for (const id of Object.keys(build.endpoints)) { const endpoint = build.endpoints[id]; - newEndpoints[`${prefix}-${id}`] = endpoint; + const newId = `${prefix}-${id}`; + + // Enforce function id constraints early for clearer errors. + if (newId.length > 63) { + throw new FirebaseError( + `Function id '${newId}' exceeds 63 characters after applying prefix '${prefix}'. Please shorten the prefix or function name.`, + ); + } + const fnIdRegex = /^[a-zA-Z][a-zA-Z0-9_-]{0,62}$/; + if (!fnIdRegex.test(newId)) { + throw new FirebaseError( + `Function id '${newId}' is invalid after applying prefix '${prefix}'. Function names must start with a letter and can contain letters, numbers, underscores, and hyphens, with a maximum length of 63 characters.`, + ); + } + + newEndpoints[newId] = endpoint; if (endpoint.secretEnvironmentVariables) { endpoint.secretEnvironmentVariables = endpoint.secretEnvironmentVariables.map((secret) => ({ diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index c6252c8ecb9..bb7ff3e34c0 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -171,6 +171,9 @@ export type FunctionConfig = { ignore?: string[]; runtime?: ActiveRuntime; codebase?: string; + // Optional: Applies a prefix to all function IDs (and secret names) discovered for this codebase. + // Must start with a lowercase letter; may contain lowercase letters, numbers, and dashes; + // cannot start or end with a dash; maximum length 30 characters. prefix?: string; } & Deployable; diff --git a/src/functions/projectConfig.spec.ts b/src/functions/projectConfig.spec.ts index 7f2dd86f85a..59fba90b6f5 100644 --- a/src/functions/projectConfig.spec.ts +++ b/src/functions/projectConfig.spec.ts @@ -120,6 +120,13 @@ describe("projectConfig", () => { ); }); + it("fails validation given prefix starting with a digit", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "1abc" }])).to.throw( + FirebaseError, + /Invalid prefix/, + ); + }); + it("fails validation given a duplicate source/prefix pair", () => { const config: projectConfig.NormalizedConfig = [ { source: "foo", codebase: "bar", prefix: "a" }, diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts index b28d8c78c6e..5fa48b25566 100644 --- a/src/functions/projectConfig.ts +++ b/src/functions/projectConfig.ts @@ -44,9 +44,10 @@ export function validatePrefix(prefix: string): void { if (prefix.length > 30) { throw new FirebaseError("Invalid prefix. Prefix must be 30 characters or less."); } - if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(prefix)) { + // Must start with a letter so that the resulting function id also starts with a letter. + if (!/^[a-z](?:[a-z0-9-]*[a-z0-9])?$/.test(prefix)) { throw new FirebaseError( - "Invalid prefix. Prefix can contain only lowercase letters, numeric characters, and dashes, and cannot start or end with a dash.", + "Invalid prefix. Prefix must start with a lowercase letter, can contain only lowercase letters, numeric characters, and dashes, and cannot start or end with a dash.", ); } } From 3c90c9d991a0e98d0d08258803dae33b5e8dc33e Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 12:58:32 -0700 Subject: [PATCH 13/21] fix: regenerate firebase.json schema. --- schema/firebase-config.json | 3166 ++++++++++++++++++----------------- 1 file changed, 1658 insertions(+), 1508 deletions(-) diff --git a/schema/firebase-config.json b/schema/firebase-config.json index 43badcc6cac..c86db3f356c 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -1,1620 +1,1770 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "definitions": { - "DataConnectSingle": { - "additionalProperties": false, - "properties": { - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "source": { - "type": "string" - } - }, - "required": ["source"], - "type": "object" - }, - "DatabaseSingle": { - "additionalProperties": false, - "properties": { - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - } - }, - "required": ["rules"], - "type": "object" - }, - "ExtensionsConfig": { - "additionalProperties": false, - "type": "object" - }, - "FirestoreSingle": { - "additionalProperties": false, - "properties": { - "database": { - "type": "string" - }, - "edition": { - "type": "string" - }, - "indexes": { - "type": "string" - }, - "location": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - } - }, - "type": "object" - }, - "FrameworksBackendOptions": { - "additionalProperties": false, - "properties": { - "concurrency": { - "description": "Number of requests a function can serve at once.", - "type": "number" - }, - "cors": { - "description": "If true, allows CORS on requests to this function.\nIf this is a `string` or `RegExp`, allows requests from domains that match the provided value.\nIf this is an `Array`, allows requests from domains matching at least one entry of the array.\nDefaults to true for {@link https.CallableFunction} and false otherwise.", - "type": ["string", "boolean"] - }, - "cpu": { - "anyOf": [ - { - "const": "gcf_gen1", - "type": "string" - }, - { - "type": "number" - } - ], - "description": "Fractional number of CPUs to allocate to a function." - }, - "enforceAppCheck": { - "description": "Determines whether Firebase AppCheck is enforced. Defaults to false.", - "type": "boolean" - }, - "ingressSettings": { - "description": "Ingress settings which control where this function can be called from.", - "enum": ["ALLOW_ALL", "ALLOW_INTERNAL_AND_GCLB", "ALLOW_INTERNAL_ONLY"], - "type": "string" - }, - "invoker": { - "const": "public", - "description": "Invoker to set access control on https functions.", - "type": "string" - }, - "labels": { - "$ref": "#/definitions/Record", - "description": "User labels to set on the function." - }, - "maxInstances": { - "description": "Max number of instances to be running in parallel.", - "type": "number" - }, - "memory": { - "description": "Amount of memory to allocate to a function.", - "enum": ["128MiB", "16GiB", "1GiB", "256MiB", "2GiB", "32GiB", "4GiB", "512MiB", "8GiB"], - "type": "string" - }, - "minInstances": { - "description": "Min number of actual instances to be running at a given time.", - "type": "number" - }, - "omit": { - "description": "If true, do not deploy or emulate this function.", - "type": "boolean" - }, - "preserveExternalChanges": { - "description": "Controls whether function configuration modified outside of function source is preserved. Defaults to false.", - "type": "boolean" - }, - "region": { - "description": "HTTP functions can override global options and can specify multiple regions to deploy to.", - "type": "string" - }, - "secrets": { - "items": { - "type": "string" - }, - "type": "array" - }, - "serviceAccount": { - "description": "Specific service account for the function to run as.", - "type": "string" - }, - "timeoutSeconds": { - "description": "Timeout for the function in seconds, possible values are 0 to 540.\nHTTPS functions can specify a higher timeout.", - "type": "number" - }, - "vpcConnector": { - "description": "Connect cloud function to specified VPC connector.", - "type": "string" - }, - "vpcConnectorEgressSettings": { - "description": "Egress settings for VPC connector.", - "enum": ["ALL_TRAFFIC", "PRIVATE_RANGES_ONLY"], - "type": "string" - } - }, - "type": "object" - }, - "FunctionConfig": { - "additionalProperties": false, - "properties": { - "codebase": { - "type": "string" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "DataConnectSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "source": { + "type": "string" + } }, - { - "type": "string" - } - ] + "required": [ + "source" + ], + "type": "object" }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" + "DatabaseSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + } }, - { - "type": "string" - } - ] - }, - "prefix": { - "type": "string", - "description": "Optional prefix to apply to all function IDs and secret names in this codebase. Must start with a lowercase letter, contain only lowercase letters, numbers, and dashes, cannot start or end with a dash, and be at most 30 characters.", - "maxLength": 30, - "pattern": "^[a-z](?:[a-z0-9-]*[a-z0-9])?$" + "required": [ + "rules" + ], + "type": "object" }, - "runtime": { - "enum": [ - "nodejs18", - "nodejs20", - "nodejs22", - "python310", - "python311", - "python312", - "python313" - ], - "type": "string" + "ExtensionsConfig": { + "additionalProperties": false, + "type": "object" }, - "source": { - "type": "string" - } - }, - "type": "object" - }, - "HostingHeaders": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "glob": { - "type": "string" - }, - "headers": { - "items": { - "additionalProperties": false, - "properties": { - "key": { + "FirestoreSingle": { + "additionalProperties": false, + "properties": { + "database": { "type": "string" - }, - "value": { + }, + "edition": { "type": "string" - } - }, - "required": ["key", "value"], - "type": "object" - }, - "type": "array" - } - }, - "required": ["glob", "headers"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "headers": { - "items": { - "additionalProperties": false, - "properties": { - "key": { + }, + "indexes": { "type": "string" - }, - "value": { + }, + "location": { "type": "string" - } }, - "required": ["key", "value"], - "type": "object" - }, - "type": "array" + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + } }, - "source": { - "type": "string" - } - }, - "required": ["headers", "source"], - "type": "object" + "type": "object" }, - { - "additionalProperties": false, - "properties": { - "headers": { - "items": { - "additionalProperties": false, - "properties": { - "key": { + "FrameworksBackendOptions": { + "additionalProperties": false, + "properties": { + "concurrency": { + "description": "Number of requests a function can serve at once.", + "type": "number" + }, + "cors": { + "description": "If true, allows CORS on requests to this function.\nIf this is a `string` or `RegExp`, allows requests from domains that match the provided value.\nIf this is an `Array`, allows requests from domains matching at least one entry of the array.\nDefaults to true for {@link https.CallableFunction} and false otherwise.", + "type": [ + "string", + "boolean" + ] + }, + "cpu": { + "anyOf": [ + { + "const": "gcf_gen1", + "type": "string" + }, + { + "type": "number" + } + ], + "description": "Fractional number of CPUs to allocate to a function." + }, + "enforceAppCheck": { + "description": "Determines whether Firebase AppCheck is enforced. Defaults to false.", + "type": "boolean" + }, + "ingressSettings": { + "description": "Ingress settings which control where this function can be called from.", + "enum": [ + "ALLOW_ALL", + "ALLOW_INTERNAL_AND_GCLB", + "ALLOW_INTERNAL_ONLY" + ], "type": "string" - }, - "value": { + }, + "invoker": { + "const": "public", + "description": "Invoker to set access control on https functions.", "type": "string" - } }, - "required": ["key", "value"], - "type": "object" - }, - "type": "array" - }, - "regex": { - "type": "string" - } - }, - "required": ["headers", "regex"], - "type": "object" - } - ] - }, - "HostingRedirects": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "glob": { - "type": "string" - }, - "type": { - "type": "number" - } - }, - "required": ["destination", "glob"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "source": { - "type": "string" - }, - "type": { - "type": "number" - } - }, - "required": ["destination", "source"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "regex": { - "type": "string" - }, - "type": { - "type": "number" - } - }, - "required": ["destination", "regex"], - "type": "object" - } - ] - }, - "HostingRewrites": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "glob": { - "type": "string" - } - }, - "required": ["destination", "glob"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "type": "string" - }, - "glob": { - "type": "string" - }, - "region": { - "type": "string" - } - }, - "required": ["function", "glob"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "additionalProperties": false, - "properties": { - "functionId": { - "type": "string" - }, - "pinTag": { - "type": "boolean" + "labels": { + "$ref": "#/definitions/Record", + "description": "User labels to set on the function." }, - "region": { - "type": "string" - } - }, - "required": ["functionId"], - "type": "object" - }, - "glob": { - "type": "string" - } - }, - "required": ["function", "glob"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "glob": { - "type": "string" - }, - "run": { - "additionalProperties": false, - "properties": { - "pinTag": { - "type": "boolean" + "maxInstances": { + "description": "Max number of instances to be running in parallel.", + "type": "number" }, - "region": { - "type": "string" + "memory": { + "description": "Amount of memory to allocate to a function.", + "enum": [ + "128MiB", + "16GiB", + "1GiB", + "256MiB", + "2GiB", + "32GiB", + "4GiB", + "512MiB", + "8GiB" + ], + "type": "string" }, - "serviceId": { - "type": "string" - } - }, - "required": ["serviceId"], - "type": "object" - } - }, - "required": ["glob", "run"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "dynamicLinks": { - "type": "boolean" - }, - "glob": { - "type": "string" - } - }, - "required": ["dynamicLinks", "glob"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "source": { - "type": "string" - } - }, - "required": ["destination", "source"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "type": "string" - }, - "region": { - "type": "string" - }, - "source": { - "type": "string" - } - }, - "required": ["function", "source"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "additionalProperties": false, - "properties": { - "functionId": { - "type": "string" - }, - "pinTag": { - "type": "boolean" + "minInstances": { + "description": "Min number of actual instances to be running at a given time.", + "type": "number" }, - "region": { - "type": "string" - } - }, - "required": ["functionId"], - "type": "object" - }, - "source": { - "type": "string" - } - }, - "required": ["function", "source"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "run": { - "additionalProperties": false, - "properties": { - "pinTag": { - "type": "boolean" + "omit": { + "description": "If true, do not deploy or emulate this function.", + "type": "boolean" + }, + "preserveExternalChanges": { + "description": "Controls whether function configuration modified outside of function source is preserved. Defaults to false.", + "type": "boolean" }, "region": { - "type": "string" + "description": "HTTP functions can override global options and can specify multiple regions to deploy to.", + "type": "string" }, - "serviceId": { - "type": "string" - } - }, - "required": ["serviceId"], - "type": "object" - }, - "source": { - "type": "string" - } - }, - "required": ["run", "source"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "dynamicLinks": { - "type": "boolean" - }, - "source": { - "type": "string" - } - }, - "required": ["dynamicLinks", "source"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "destination": { - "type": "string" - }, - "regex": { - "type": "string" - } - }, - "required": ["destination", "regex"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "type": "string" - }, - "regex": { - "type": "string" - }, - "region": { - "type": "string" - } - }, - "required": ["function", "regex"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "function": { - "additionalProperties": false, - "properties": { - "functionId": { - "type": "string" - }, - "pinTag": { - "type": "boolean" + "secrets": { + "items": { + "type": "string" + }, + "type": "array" }, - "region": { - "type": "string" - } - }, - "required": ["functionId"], - "type": "object" - }, - "regex": { - "type": "string" - } - }, - "required": ["function", "regex"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "regex": { - "type": "string" - }, - "run": { - "additionalProperties": false, - "properties": { - "pinTag": { - "type": "boolean" + "serviceAccount": { + "description": "Specific service account for the function to run as.", + "type": "string" }, - "region": { - "type": "string" + "timeoutSeconds": { + "description": "Timeout for the function in seconds, possible values are 0 to 540.\nHTTPS functions can specify a higher timeout.", + "type": "number" + }, + "vpcConnector": { + "description": "Connect cloud function to specified VPC connector.", + "type": "string" }, - "serviceId": { - "type": "string" + "vpcConnectorEgressSettings": { + "description": "Egress settings for VPC connector.", + "enum": [ + "ALL_TRAFFIC", + "PRIVATE_RANGES_ONLY" + ], + "type": "string" } - }, - "required": ["serviceId"], - "type": "object" - } - }, - "required": ["regex", "run"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "dynamicLinks": { - "type": "boolean" - }, - "regex": { - "type": "string" - } - }, - "required": ["dynamicLinks", "regex"], - "type": "object" - } - ] - }, - "HostingSingle": { - "additionalProperties": false, - "properties": { - "appAssociation": { - "enum": ["AUTO", "NONE"], - "type": "string" - }, - "cleanUrls": { - "type": "boolean" - }, - "frameworksBackend": { - "$ref": "#/definitions/FrameworksBackendOptions" - }, - "headers": { - "items": { - "$ref": "#/definitions/HostingHeaders" - }, - "type": "array" - }, - "i18n": { - "additionalProperties": false, - "properties": { - "root": { - "type": "string" - } - }, - "required": ["root"], - "type": "object" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "public": { - "type": "string" - }, - "redirects": { - "items": { - "$ref": "#/definitions/HostingRedirects" - }, - "type": "array" - }, - "rewrites": { - "items": { - "$ref": "#/definitions/HostingRewrites" - }, - "type": "array" - }, - "site": { - "type": "string" - }, - "source": { - "type": "string" - }, - "target": { - "type": "string" - }, - "trailingSlash": { - "type": "boolean" - } - }, - "type": "object" - }, - "Record": { - "additionalProperties": false, - "type": "object" - }, - "RemoteConfigConfig": { - "additionalProperties": false, - "properties": { - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" }, - { - "type": "string" - } - ] - }, - "template": { - "type": "string" - } - }, - "required": ["template"], - "type": "object" - }, - "StorageSingle": { - "additionalProperties": false, - "properties": { - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": ["rules"], - "type": "object" - } - }, - "properties": { - "$schema": { - "format": "uri", - "type": "string" - }, - "apphosting": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "alwaysDeployFromSource": { - "type": "boolean" - }, - "backendId": { - "type": "string" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "rootDir": { - "type": "string" - } - }, - "required": ["backendId", "ignore", "rootDir"], - "type": "object" + "type": "object" }, - { - "items": { + "FunctionConfig": { "additionalProperties": false, "properties": { - "alwaysDeployFromSource": { - "type": "boolean" - }, - "backendId": { - "type": "string" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "rootDir": { - "type": "string" - } - }, - "required": ["backendId", "ignore", "rootDir"], - "type": "object" - }, - "type": "array" - } - ] - }, - "database": { - "anyOf": [ - { - "$ref": "#/definitions/DatabaseSingle" - }, - { - "items": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "instance": { + "codebase": { "type": "string" - }, - "postdeploy": { + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { "anyOf": [ - { - "items": { - "type": "string" + { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array" - }, - { - "type": "string" - } + { + "type": "string" + } ] - }, - "predeploy": { + }, + "predeploy": { "anyOf": [ - { - "items": { - "type": "string" + { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array" - }, - { - "type": "string" - } + { + "type": "string" + } ] - }, - "rules": { + }, + "prefix": { "type": "string" - }, - "target": { + }, + "runtime": { + "enum": [ + "nodejs18", + "nodejs20", + "nodejs22", + "python310", + "python311", + "python312", + "python313" + ], "type": "string" - } - }, - "required": ["instance", "rules"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "instance": { + }, + "source": { "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" + } + }, + "type": "object" + }, + "HostingHeaders": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "glob": { + "type": "string" }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "glob", + "headers" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "type": "array" }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } + "source": { + "type": "string" + } + }, + "required": [ + "headers", + "source" + ], + "type": "object" }, - "required": ["rules", "target"], - "type": "object" - } + { + "additionalProperties": false, + "properties": { + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "type": "array" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "headers", + "regex" + ], + "type": "object" + } ] - }, - "type": "array" - } - ] - }, - "dataconnect": { - "anyOf": [ - { - "$ref": "#/definitions/DataConnectSingle" - }, - { - "items": { - "$ref": "#/definitions/DataConnectSingle" - }, - "type": "array" - } - ] - }, - "emulators": { - "additionalProperties": false, - "properties": { - "apphosting": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - }, - "rootDirectory": { - "type": "string" - }, - "startCommand": { - "type": "string" - }, - "startCommandOverride": { - "type": "string" - } - }, - "type": "object" }, - "auth": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "database": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "dataconnect": { - "additionalProperties": false, - "properties": { - "dataDir": { - "type": "string" - }, - "host": { - "type": "string" - }, - "port": { - "type": "number" - }, - "postgresHost": { - "type": "string" - }, - "postgresPort": { - "type": "number" - } - }, - "type": "object" - }, - "eventarc": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "extensions": { - "properties": {}, - "type": "object" - }, - "firestore": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - }, - "websocketPort": { - "type": "number" - } - }, - "type": "object" - }, - "functions": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "hosting": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "hub": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "logging": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "pubsub": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "singleProjectMode": { - "type": "boolean" - }, - "storage": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "tasks": { - "additionalProperties": false, - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - }, - "ui": { - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean" - }, - "host": { - "type": "string" - }, - "port": { - "type": "number" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "extensions": { - "$ref": "#/definitions/ExtensionsConfig" - }, - "firestore": { - "anyOf": [ - { - "$ref": "#/definitions/FirestoreSingle" + "HostingRedirects": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "glob": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": [ + "destination", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "source": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": [ + "destination", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "regex": { + "type": "string" + }, + "type": { + "type": "number" + } + }, + "required": [ + "destination", + "regex" + ], + "type": "object" + } + ] }, - { - "items": { + "HostingRewrites": { "anyOf": [ - { - "additionalProperties": false, - "properties": { - "database": { - "type": "string" - }, - "indexes": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" + "glob": { + "type": "string" + } + }, + "required": [ + "destination", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": ["target"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "database": { - "type": "string" - }, - "indexes": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" + "glob": { + "type": "string" }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" + "region": { + "type": "string" + } + }, + "required": [ + "function", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } + "glob": { + "type": "string" + } + }, + "required": [ + "function", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "glob": { + "type": "string" + }, + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "serviceId": { + "type": "string" + } + }, + "required": [ + "serviceId" + ], + "type": "object" + } + }, + "required": [ + "glob", + "run" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "dynamicLinks", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": [ + "destination", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "region": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": [ + "function", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": [ + "function", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "serviceId": { + "type": "string" + } + }, + "required": [ + "serviceId" + ], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": [ + "run", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "source": { + "type": "string" + } + }, + "required": [ + "dynamicLinks", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "destination", + "regex" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "regex": { + "type": "string" + }, + "region": { + "type": "string" + } + }, + "required": [ + "function", + "regex" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "function", + "regex" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "regex": { + "type": "string" + }, + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "serviceId": { + "type": "string" + } + }, + "required": [ + "serviceId" + ], + "type": "object" + } + }, + "required": [ + "regex", + "run" + ], + "type": "object" }, - "required": ["database"], - "type": "object" - } + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "dynamicLinks", + "regex" + ], + "type": "object" + } ] - }, - "type": "array" - } - ] - }, - "functions": { - "anyOf": [ - { - "$ref": "#/definitions/FunctionConfig" - }, - { - "items": { - "$ref": "#/definitions/FunctionConfig" - }, - "type": "array" - } - ] - }, - "hosting": { - "anyOf": [ - { - "$ref": "#/definitions/HostingSingle" }, - { - "items": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "appAssociation": { - "enum": ["AUTO", "NONE"], + "HostingSingle": { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], "type": "string" - }, - "cleanUrls": { + }, + "cleanUrls": { "type": "boolean" - }, - "frameworksBackend": { + }, + "frameworksBackend": { "$ref": "#/definitions/FrameworksBackendOptions" - }, - "headers": { + }, + "headers": { "items": { - "$ref": "#/definitions/HostingHeaders" + "$ref": "#/definitions/HostingHeaders" }, "type": "array" - }, - "i18n": { + }, + "i18n": { "additionalProperties": false, "properties": { - "root": { - "type": "string" - } + "root": { + "type": "string" + } }, - "required": ["root"], + "required": [ + "root" + ], "type": "object" - }, - "ignore": { + }, + "ignore": { "items": { - "type": "string" + "type": "string" }, "type": "array" - }, - "postdeploy": { + }, + "postdeploy": { "anyOf": [ - { - "items": { - "type": "string" + { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array" - }, - { - "type": "string" - } + { + "type": "string" + } ] - }, - "predeploy": { + }, + "predeploy": { "anyOf": [ - { - "items": { - "type": "string" + { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array" - }, - { - "type": "string" - } + { + "type": "string" + } ] - }, - "public": { + }, + "public": { "type": "string" - }, - "redirects": { + }, + "redirects": { "items": { - "$ref": "#/definitions/HostingRedirects" + "$ref": "#/definitions/HostingRedirects" }, "type": "array" - }, - "rewrites": { + }, + "rewrites": { "items": { - "$ref": "#/definitions/HostingRewrites" + "$ref": "#/definitions/HostingRewrites" }, "type": "array" - }, - "site": { + }, + "site": { "type": "string" - }, - "source": { + }, + "source": { "type": "string" - }, - "target": { + }, + "target": { "type": "string" - }, - "trailingSlash": { + }, + "trailingSlash": { "type": "boolean" - } - }, - "required": ["target"], - "type": "object" - }, - { - "additionalProperties": false, - "properties": { - "appAssociation": { - "enum": ["AUTO", "NONE"], + } + }, + "type": "object" + }, + "Record": { + "additionalProperties": false, + "type": "object" + }, + "RemoteConfigConfig": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "template": { "type": "string" - }, - "cleanUrls": { - "type": "boolean" - }, - "frameworksBackend": { - "$ref": "#/definitions/FrameworksBackendOptions" - }, - "headers": { - "items": { - "$ref": "#/definitions/HostingHeaders" - }, - "type": "array" - }, - "i18n": { - "additionalProperties": false, - "properties": { - "root": { - "type": "string" - } - }, - "required": ["root"], - "type": "object" - }, - "ignore": { - "items": { - "type": "string" - }, - "type": "array" - }, - "postdeploy": { + } + }, + "required": [ + "template" + ], + "type": "object" + }, + "StorageSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { "anyOf": [ - { - "items": { - "type": "string" + { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array" - }, - { - "type": "string" - } + { + "type": "string" + } ] - }, - "predeploy": { + }, + "predeploy": { "anyOf": [ - { - "items": { - "type": "string" + { + "items": { + "type": "string" + }, + "type": "array" }, - "type": "array" - }, - { - "type": "string" - } + { + "type": "string" + } ] - }, - "public": { + }, + "rules": { + "type": "string" + }, + "target": { "type": "string" - }, - "redirects": { + } + }, + "required": [ + "rules" + ], + "type": "object" + } + }, + "properties": { + "$schema": { + "format": "uri", + "type": "string" + }, + "apphosting": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "alwaysDeployFromSource": { + "type": "boolean" + }, + "backendId": { + "type": "string" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "rootDir": { + "type": "string" + } + }, + "required": [ + "backendId", + "ignore", + "rootDir" + ], + "type": "object" + }, + { "items": { - "$ref": "#/definitions/HostingRedirects" + "additionalProperties": false, + "properties": { + "alwaysDeployFromSource": { + "type": "boolean" + }, + "backendId": { + "type": "string" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "rootDir": { + "type": "string" + } + }, + "required": [ + "backendId", + "ignore", + "rootDir" + ], + "type": "object" }, "type": "array" - }, - "rewrites": { + } + ] + }, + "database": { + "anyOf": [ + { + "$ref": "#/definitions/DatabaseSingle" + }, + { "items": { - "$ref": "#/definitions/HostingRewrites" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "instance": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "instance", + "rules" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "instance": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "rules", + "target" + ], + "type": "object" + } + ] }, "type": "array" - }, - "site": { - "type": "string" - }, - "source": { - "type": "string" - }, - "target": { - "type": "string" - }, - "trailingSlash": { - "type": "boolean" - } + } + ] + }, + "dataconnect": { + "anyOf": [ + { + "$ref": "#/definitions/DataConnectSingle" }, - "required": ["site"], - "type": "object" - } + { + "items": { + "$ref": "#/definitions/DataConnectSingle" + }, + "type": "array" + } ] - }, - "type": "array" - } - ] - }, - "remoteconfig": { - "$ref": "#/definitions/RemoteConfigConfig" - }, - "storage": { - "anyOf": [ - { - "$ref": "#/definitions/StorageSingle" }, - { - "items": { + "emulators": { "additionalProperties": false, "properties": { - "bucket": { - "type": "string" - }, - "postdeploy": { - "anyOf": [ - { + "apphosting": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "rootDirectory": { + "type": "string" + }, + "startCommand": { + "type": "string" + }, + "startCommandOverride": { + "type": "string" + } + }, + "type": "object" + }, + "auth": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "database": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "dataconnect": { + "additionalProperties": false, + "properties": { + "dataDir": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "postgresHost": { + "type": "string" + }, + "postgresPort": { + "type": "number" + } + }, + "type": "object" + }, + "eventarc": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "extensions": { + "properties": {}, + "type": "object" + }, + "firestore": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "websocketPort": { + "type": "number" + } + }, + "type": "object" + }, + "functions": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "hosting": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "hub": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "logging": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "pubsub": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "singleProjectMode": { + "type": "boolean" + }, + "storage": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "tasks": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "ui": { + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "extensions": { + "$ref": "#/definitions/ExtensionsConfig" + }, + "firestore": { + "anyOf": [ + { + "$ref": "#/definitions/FirestoreSingle" + }, + { "items": { - "type": "string" + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "database": { + "type": "string" + }, + "indexes": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "database": { + "type": "string" + }, + "indexes": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "database" + ], + "type": "object" + } + ] }, "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { + } + ] + }, + "functions": { + "anyOf": [ + { + "$ref": "#/definitions/FunctionConfig" + }, + { "items": { - "type": "string" + "$ref": "#/definitions/FunctionConfig" }, "type": "array" - }, - { - "type": "string" - } - ] - }, - "rules": { - "type": "string" - }, - "target": { - "type": "string" - } - }, - "required": ["bucket", "rules"], - "type": "object" - }, - "type": "array" + } + ] + }, + "hosting": { + "anyOf": [ + { + "$ref": "#/definitions/HostingSingle" + }, + { + "items": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { + "items": { + "$ref": "#/definitions/HostingHeaders" + }, + "type": "array" + }, + "i18n": { + "additionalProperties": false, + "properties": { + "root": { + "type": "string" + } + }, + "required": [ + "root" + ], + "type": "object" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/definitions/HostingRedirects" + }, + "type": "array" + }, + "rewrites": { + "items": { + "$ref": "#/definitions/HostingRewrites" + }, + "type": "array" + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "trailingSlash": { + "type": "boolean" + } + }, + "required": [ + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { + "items": { + "$ref": "#/definitions/HostingHeaders" + }, + "type": "array" + }, + "i18n": { + "additionalProperties": false, + "properties": { + "root": { + "type": "string" + } + }, + "required": [ + "root" + ], + "type": "object" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/definitions/HostingRedirects" + }, + "type": "array" + }, + "rewrites": { + "items": { + "$ref": "#/definitions/HostingRewrites" + }, + "type": "array" + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "trailingSlash": { + "type": "boolean" + } + }, + "required": [ + "site" + ], + "type": "object" + } + ] + }, + "type": "array" + } + ] + }, + "remoteconfig": { + "$ref": "#/definitions/RemoteConfigConfig" + }, + "storage": { + "anyOf": [ + { + "$ref": "#/definitions/StorageSingle" + }, + { + "items": { + "additionalProperties": false, + "properties": { + "bucket": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "bucket", + "rules" + ], + "type": "object" + }, + "type": "array" + } + ] } - ] - } - }, - "type": "object" + }, + "type": "object" } + From 0d88c103e9750a21de77e35ff76cc810e39657d0 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 14:25:26 -0700 Subject: [PATCH 14/21] fix: fix emulator integration test. --- scripts/emulator-tests/functions/index.js | 12 ++ .../emulator-tests/functionsEmulator.spec.ts | 2 +- scripts/emulator-tests/test_output.log | 107 ++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 scripts/emulator-tests/functions/index.js create mode 100644 scripts/emulator-tests/test_output.log diff --git a/scripts/emulator-tests/functions/index.js b/scripts/emulator-tests/functions/index.js new file mode 100644 index 00000000000..09886f1dbb0 --- /dev/null +++ b/scripts/emulator-tests/functions/index.js @@ -0,0 +1,12 @@ +module.exports = (() => { + return { + functionId: require("firebase-functions").https.onRequest((req, resp) => { + return new Promise((resolve) => { + setTimeout(() => { + resp.sendStatus(200); + resolve(); + }, 3000); + }); + }), + }; + })(); diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index 1e10fee9ef6..c3c5ef3dc0d 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -790,7 +790,7 @@ describe("FunctionsEmulator", function () { }; }); - await emu.start(); + await registry.EmulatorRegistry.start(emu); await emu.connect(); await supertest(emu.createHubServer()) diff --git a/scripts/emulator-tests/test_output.log b/scripts/emulator-tests/test_output.log new file mode 100644 index 00000000000..3f26c9869e6 --- /dev/null +++ b/scripts/emulator-tests/test_output.log @@ -0,0 +1,107 @@ + +> firebase-tools@14.12.1 test:emulator +> bash ./scripts/emulator-tests/run.sh + ++ rm -rf dev ++ tsc --build scripts/emulator-tests/tsconfig.dev.json ++ trap cleanup EXIT ++ cp package.json dev/package.json ++ cd scripts/emulator-tests/functions ++ npm ci +npm warn EBADENGINE Unsupported engine { +npm warn EBADENGINE package: 'test-fns@0.0.1', +npm warn EBADENGINE required: { node: '20' }, +npm warn EBADENGINE current: { node: 'v22.12.0', npm: '10.9.0' } +npm warn EBADENGINE } +npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. +npm warn deprecated glob@8.1.0: Glob versions prior to v9 are no longer supported +npm warn deprecated google-p12-pem@4.0.1: Package is no longer maintained + +added 273 packages, and audited 274 packages in 2s + +23 packages are looking for funding + run `npm fund` for details + +13 vulnerabilities (5 low, 1 moderate, 3 high, 4 critical) + +To address issues that do not require attention, run: + npm audit fix + +To address all issues (including breaking changes), run: + npm audit fix --force + +Run `npm audit` for details. ++ mocha dev/scripts/emulator-tests/functionsEmulator.spec.js dev/scripts/emulator-tests/functionsEmulatorRuntime.spec.js dev/scripts/emulator-tests/unzipEmulators.spec.js + + + FunctionsEmulator + ✔ should enforce timeout (1852ms) + Hub + ✔ should route requests to /:project_id/us-central1/:trigger_id to default region HTTPS Function (822ms) + ✔ should route requests to /:project_id/:other-region/:trigger_id to the region's HTTPS Function (822ms) + ✔ should 404 when a function doesn't exist in the region + ✔ should route requests to /:project_id/:region/:trigger_id/ to HTTPS Function (717ms) + ✔ should 404 when a function does not exist + ✔ should properly route to a namespaced/grouped HTTPs function (822ms) + ✔ should route requests to /:project_id/:region/:trigger_id/a/b to HTTPS Function (820ms) + ✔ should reject requests to a non-emulator path + ✔ should rewrite req.path to hide /:project_id/:region/:trigger_id (823ms) + ✔ should return the correct url, baseUrl, originalUrl for the root route (824ms) + ✔ should return the correct url, baseUrl, originalUrl with query params (820ms) + ✔ should return the correct url, baseUrl, originalUrl for a subroute (827ms) + ✔ should return the correct url, baseUrl, originalUrl for any region (825ms) + ✔ should route request body (850ms) + ✔ should route query parameters (822ms) + ✔ should override callable auth (825ms) + ✔ should override callable auth with unicode (825ms) + ✔ should override callable auth with a poorly padded ID Token (719ms) + ✔ should preserve the Authorization header for callable auth (724ms) + ✔ should respond to requests to /backends to with info about the running backends + 1) should support multiple codebases with the same source and apply prefixes + 2) "after each" hook for "should support multiple codebases with the same source and apply prefixes" + + FunctionsEmulator-Runtime + Stubs, Mocks, and Helpers + _InitializeNetworkFiltering + ✔ should log outgoing unknown HTTP requests via 'http' (917ms) + 3) should support multiple codebases with the same source and apply prefixes + ✔ should log outgoing unknown HTTP requests via 'https' (1826ms) + ✔ should log outgoing Google API requests (922ms) + _InitializeFirebaseAdminStubs(...) + ✔ should provide stubbed default app from initializeApp (824ms) + ✔ should provide a stubbed app with custom options (719ms) + ✔ should provide non-stubbed non-default app from initializeApp (721ms) + ✔ should route all sub-fields accordingly (719ms) + ✔ should expose Firestore prod when the emulator is not running (713ms) + ✔ should expose a stubbed Firestore when the emulator is running (715ms) + ✔ should expose RTDB prod when the emulator is not running (718ms) + ✔ should expose a stubbed RTDB when the emulator is running (717ms) + _InitializeFunctionsConfigHelper() + ✔ should tell the user if they've accessed a non-existent function field (720ms) + Runtime + HTTPS + ✔ should handle a GET request (715ms) + ✔ should handle a POST request with form data (717ms) + ✔ should handle a POST request with JSON data (719ms) + ✔ should handle a POST request with text data (724ms) + ✔ should handle a POST request with any other type (714ms) + ✔ should handle a POST request and store rawBody (716ms) + ✔ should forward request to Express app (817ms) + ✔ should handle `x-forwarded-host` (715ms) + Cloud Firestore + ✔ should provide Change for firestore.onWrite() (719ms) + ✔ should provide Change for firestore.onUpdate() (717ms) + ✔ should provide Change for firestore.onDelete() (721ms) + ✔ should provide Change for firestore.onCreate() (722ms) + Error handling + ✔ Should handle regular functions for Express handlers (713ms) + ✔ Should handle async functions for Express handlers (715ms) + ✔ Should handle async/runWith functions for Express handlers (713ms) + Debug + ✔ handles debug message to change function target (714ms) + ✔ disables configured timeout when in debug mode (3721ms) + + unzipEmulators + ✔ should unzip a ui emulator zip file (266ms) +++ cleanup +++ rm -rf dev From 1a0479fbce8c24f087468a7067a9654288d7126e Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 14:31:12 -0700 Subject: [PATCH 15/21] fix: emulator test for prefix functionality and remove auto-generated files - Fix 'should support multiple codebases with the same source and apply prefixes' test - Register emulator with EmulatorRegistry before calling connect() to provide host/port info - Remove auto-generated test_output.log from repository - Add test_output.log and scripts/emulator-tests/functions/index.js to .gitignore --- .gitignore | 2 + scripts/emulator-tests/test_output.log | 107 ------------------------- 2 files changed, 2 insertions(+), 107 deletions(-) delete mode 100644 scripts/emulator-tests/test_output.log diff --git a/.gitignore b/.gitignore index b5fe8fb3f3d..9857f71603c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ firebase-debug.log firebase-debug.*.log npm-debug.log ui-debug.log +test_output.log +scripts/emulator-tests/functions/index.js yarn.lock .npmrc diff --git a/scripts/emulator-tests/test_output.log b/scripts/emulator-tests/test_output.log deleted file mode 100644 index 3f26c9869e6..00000000000 --- a/scripts/emulator-tests/test_output.log +++ /dev/null @@ -1,107 +0,0 @@ - -> firebase-tools@14.12.1 test:emulator -> bash ./scripts/emulator-tests/run.sh - -+ rm -rf dev -+ tsc --build scripts/emulator-tests/tsconfig.dev.json -+ trap cleanup EXIT -+ cp package.json dev/package.json -+ cd scripts/emulator-tests/functions -+ npm ci -npm warn EBADENGINE Unsupported engine { -npm warn EBADENGINE package: 'test-fns@0.0.1', -npm warn EBADENGINE required: { node: '20' }, -npm warn EBADENGINE current: { node: 'v22.12.0', npm: '10.9.0' } -npm warn EBADENGINE } -npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. -npm warn deprecated glob@8.1.0: Glob versions prior to v9 are no longer supported -npm warn deprecated google-p12-pem@4.0.1: Package is no longer maintained - -added 273 packages, and audited 274 packages in 2s - -23 packages are looking for funding - run `npm fund` for details - -13 vulnerabilities (5 low, 1 moderate, 3 high, 4 critical) - -To address issues that do not require attention, run: - npm audit fix - -To address all issues (including breaking changes), run: - npm audit fix --force - -Run `npm audit` for details. -+ mocha dev/scripts/emulator-tests/functionsEmulator.spec.js dev/scripts/emulator-tests/functionsEmulatorRuntime.spec.js dev/scripts/emulator-tests/unzipEmulators.spec.js - - - FunctionsEmulator - ✔ should enforce timeout (1852ms) - Hub - ✔ should route requests to /:project_id/us-central1/:trigger_id to default region HTTPS Function (822ms) - ✔ should route requests to /:project_id/:other-region/:trigger_id to the region's HTTPS Function (822ms) - ✔ should 404 when a function doesn't exist in the region - ✔ should route requests to /:project_id/:region/:trigger_id/ to HTTPS Function (717ms) - ✔ should 404 when a function does not exist - ✔ should properly route to a namespaced/grouped HTTPs function (822ms) - ✔ should route requests to /:project_id/:region/:trigger_id/a/b to HTTPS Function (820ms) - ✔ should reject requests to a non-emulator path - ✔ should rewrite req.path to hide /:project_id/:region/:trigger_id (823ms) - ✔ should return the correct url, baseUrl, originalUrl for the root route (824ms) - ✔ should return the correct url, baseUrl, originalUrl with query params (820ms) - ✔ should return the correct url, baseUrl, originalUrl for a subroute (827ms) - ✔ should return the correct url, baseUrl, originalUrl for any region (825ms) - ✔ should route request body (850ms) - ✔ should route query parameters (822ms) - ✔ should override callable auth (825ms) - ✔ should override callable auth with unicode (825ms) - ✔ should override callable auth with a poorly padded ID Token (719ms) - ✔ should preserve the Authorization header for callable auth (724ms) - ✔ should respond to requests to /backends to with info about the running backends - 1) should support multiple codebases with the same source and apply prefixes - 2) "after each" hook for "should support multiple codebases with the same source and apply prefixes" - - FunctionsEmulator-Runtime - Stubs, Mocks, and Helpers - _InitializeNetworkFiltering - ✔ should log outgoing unknown HTTP requests via 'http' (917ms) - 3) should support multiple codebases with the same source and apply prefixes - ✔ should log outgoing unknown HTTP requests via 'https' (1826ms) - ✔ should log outgoing Google API requests (922ms) - _InitializeFirebaseAdminStubs(...) - ✔ should provide stubbed default app from initializeApp (824ms) - ✔ should provide a stubbed app with custom options (719ms) - ✔ should provide non-stubbed non-default app from initializeApp (721ms) - ✔ should route all sub-fields accordingly (719ms) - ✔ should expose Firestore prod when the emulator is not running (713ms) - ✔ should expose a stubbed Firestore when the emulator is running (715ms) - ✔ should expose RTDB prod when the emulator is not running (718ms) - ✔ should expose a stubbed RTDB when the emulator is running (717ms) - _InitializeFunctionsConfigHelper() - ✔ should tell the user if they've accessed a non-existent function field (720ms) - Runtime - HTTPS - ✔ should handle a GET request (715ms) - ✔ should handle a POST request with form data (717ms) - ✔ should handle a POST request with JSON data (719ms) - ✔ should handle a POST request with text data (724ms) - ✔ should handle a POST request with any other type (714ms) - ✔ should handle a POST request and store rawBody (716ms) - ✔ should forward request to Express app (817ms) - ✔ should handle `x-forwarded-host` (715ms) - Cloud Firestore - ✔ should provide Change for firestore.onWrite() (719ms) - ✔ should provide Change for firestore.onUpdate() (717ms) - ✔ should provide Change for firestore.onDelete() (721ms) - ✔ should provide Change for firestore.onCreate() (722ms) - Error handling - ✔ Should handle regular functions for Express handlers (713ms) - ✔ Should handle async functions for Express handlers (715ms) - ✔ Should handle async/runWith functions for Express handlers (713ms) - Debug - ✔ handles debug message to change function target (714ms) - ✔ disables configured timeout when in debug mode (3721ms) - - unzipEmulators - ✔ should unzip a ui emulator zip file (266ms) -++ cleanup -++ rm -rf dev From 99fe88343474cc14bf878c813ee3e6205c9e1d78 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 14:44:38 -0700 Subject: [PATCH 16/21] fix: remove auto-generated scripts/emulator-tests/functions/index.js This file is auto-generated during test runs and should not be committed. --- scripts/emulator-tests/functions/index.js | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 scripts/emulator-tests/functions/index.js diff --git a/scripts/emulator-tests/functions/index.js b/scripts/emulator-tests/functions/index.js deleted file mode 100644 index 09886f1dbb0..00000000000 --- a/scripts/emulator-tests/functions/index.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = (() => { - return { - functionId: require("firebase-functions").https.onRequest((req, resp) => { - return new Promise((resolve) => { - setTimeout(() => { - resp.sendStatus(200); - resolve(); - }, 3000); - }); - }), - }; - })(); From 0d2cc63fb6fd020d4d52d6bc0cee6e04dca1a616 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 14:51:29 -0700 Subject: [PATCH 17/21] docs: add helpful comments to FunctionConfig properties - Document source, ignore, runtime, and codebase properties - Provide examples and clarify default behaviors - Match documentation style used for prefix property --- src/firebaseConfig.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index bb7ff3e34c0..940c1843c13 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -167,9 +167,18 @@ export type DatabaseConfig = DatabaseSingle | DatabaseMultiple; export type FirestoreConfig = FirestoreSingle | FirestoreMultiple; export type FunctionConfig = { + // Optional: Directory containing the Cloud Functions source code. + // Defaults to "functions" if not specified. source?: string; + // Optional: List of glob patterns for files and directories to ignore during deployment. + // Uses gitignore-style syntax. Commonly includes node_modules, .git, etc. ignore?: string[]; + // Optional: The Node.js runtime version to use for Cloud Functions. + // Example: "nodejs18", "nodejs20". Must be a supported runtime version. runtime?: ActiveRuntime; + // Optional: A unique identifier for this functions codebase when using multiple codebases. + // Allows organizing functions into separate deployment units within the same project. + // Must be unique across all codebases in firebase.json. codebase?: string; // Optional: Applies a prefix to all function IDs (and secret names) discovered for this codebase. // Must start with a lowercase letter; may contain lowercase letters, numbers, and dashes; From 6f3149b5b08050f1d4da5cecf21a210e563e593b Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 15:05:41 -0700 Subject: [PATCH 18/21] docs: update applyPrefix comment to mention secret name prefixing Clarify that the function prefixes both endpoint IDs and secret names to ensure isolation between different codebases --- src/deploy/functions/build.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index bdd676b92ec..e6c192cc270 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -653,7 +653,9 @@ function discoverTrigger(endpoint: Endpoint, region: string, r: Resolver): backe } /** - * Prefixes all endpoint IDs in a build with a given prefix. + * Prefixes all endpoint IDs and secret names in a build with a given prefix. + * This ensures that functions and their associated secrets from different codebases + * remain isolated and don't conflict when deployed to the same project. */ export function applyPrefix(build: Build, prefix: string): void { if (!prefix) { From 9778139163537e918b396622d42ddfe6abf2653e Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 15:38:58 -0700 Subject: [PATCH 19/21] fix: fix emulator integration test and prevent process hanging - Update prefix test to properly manage emulator lifecycle through registry - Fix FunctionsEmulator to close file watchers on stop() to prevent hanging - Store watchers created in connect() and close them in stop() - This fixes test hanging issue that occurred when connect() was called --- .../emulator-tests/functionsEmulator.spec.ts | 22 +++++++++++-------- src/emulator/functionsEmulator.ts | 9 ++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index c3c5ef3dc0d..6f962858ae0 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -772,7 +772,7 @@ describe("FunctionsEmulator", function () { prefix: "prefix-two", }; - emu = new FunctionsEmulator({ + const prefixEmu = new FunctionsEmulator({ projectId: TEST_PROJECT_ID, projectDir: MODULE_ROOT, emulatableBackends: [backend1, backend2], @@ -790,16 +790,20 @@ describe("FunctionsEmulator", function () { }; }); - await registry.EmulatorRegistry.start(emu); - await emu.connect(); + try { + await registry.EmulatorRegistry.start(prefixEmu); + await prefixEmu.connect(); - await supertest(emu.createHubServer()) - .get(`/${TEST_PROJECT_ID}/us-central1/prefix-one-functionId`) - .expect(200); + await supertest(prefixEmu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/prefix-one-functionId`) + .expect(200); - await supertest(emu.createHubServer()) - .get(`/${TEST_PROJECT_ID}/us-central1/prefix-two-functionId`) - .expect(200); + await supertest(prefixEmu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/prefix-two-functionId`) + .expect(200); + } finally { + await registry.EmulatorRegistry.stop(Emulators.FUNCTIONS); + } }); describe("user-defined environment variables", () => { diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 6922021fb57..3b5384ade3e 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -222,6 +222,7 @@ export class FunctionsEmulator implements EmulatorInstance { private staticBackends: EmulatableBackend[] = []; private dynamicBackends: EmulatableBackend[] = []; + private watchers: chokidar.FSWatcher[] = []; debugMode = false; @@ -484,6 +485,8 @@ export class FunctionsEmulator implements EmulatorInstance { persistent: true, }); + this.watchers.push(watcher); + const debouncedLoadTriggers = debounce(() => this.loadTriggers(backend), 1000); watcher.on("change", (filePath) => { this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`); @@ -511,6 +514,12 @@ export class FunctionsEmulator implements EmulatorInstance { for (const pool of Object.values(this.workerPools)) { pool.exit(); } + + for (const watcher of this.watchers) { + await watcher.close(); + } + this.watchers = []; + if (this.destroyServer) { await this.destroyServer(); } From 673a0586ba2ef634ac7e4746d80b05863b38a137 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 18:55:36 -0700 Subject: [PATCH 20/21] fix: address PR feedback for multi-instance functions support - Use Object.entries() instead of Object.keys() in build.ts - Use latest('nodejs') for runtime in prepare.spec.ts - Reorganize imports in prepare.spec.ts to group asterisk imports --- CHANGELOG.md | 1 + src/deploy/functions/build.ts | 6 ++++-- src/deploy/functions/prepare.spec.ts | 25 +++++++++++++------------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..cd3bd69ab36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Added prefix support for multi-instance Cloud Functions extension parameters. (#8911) \ No newline at end of file diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index e6c192cc270..6c68bdeb24c 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -321,6 +321,9 @@ export async function resolveBackend( } // Exported for testing +/** + * + */ export function envWithTypes( definedParams: params.Param[], rawEnvs: Record, @@ -662,8 +665,7 @@ export function applyPrefix(build: Build, prefix: string): void { return; } const newEndpoints: Record = {}; - for (const id of Object.keys(build.endpoints)) { - const endpoint = build.endpoints[id]; + for (const [id, endpoint] of Object.entries(build.endpoints)) { const newId = `${prefix}-${id}`; // Enforce function id constraints early for clearer errors. diff --git a/src/deploy/functions/prepare.spec.ts b/src/deploy/functions/prepare.spec.ts index 93b8a400f86..57d7a28ba1f 100644 --- a/src/deploy/functions/prepare.spec.ts +++ b/src/deploy/functions/prepare.spec.ts @@ -3,15 +3,16 @@ import * as sinon from "sinon"; import * as build from "./build"; import * as prepare from "./prepare"; import * as runtimes from "./runtimes"; +import * as backend from "./backend"; +import * as ensureApiEnabled from "../../ensureApiEnabled"; +import * as serviceusage from "../../gcp/serviceusage"; +import * as prompt from "../../prompt"; import { RuntimeDelegate } from "./runtimes"; import { FirebaseError } from "../../error"; import { Options } from "../../options"; import { ValidatedConfig } from "../../functions/projectConfig"; -import * as backend from "./backend"; -import * as ensureApiEnabled from "../../ensureApiEnabled"; -import * as serviceusage from "../../gcp/serviceusage"; import { BEFORE_CREATE_EVENT, BEFORE_SIGN_IN_EVENT } from "../../functions/events/v1"; -import * as prompt from "../../prompt"; +import { latest } from "./runtimes/supported"; describe("prepare", () => { const ENDPOINT_BASE: Omit = { @@ -20,7 +21,7 @@ describe("prepare", () => { region: "region", project: "project", entryPoint: "entry", - runtime: "nodejs22", + runtime: latest("nodejs"), }; const ENDPOINT: backend.Endpoint = { @@ -38,7 +39,7 @@ describe("prepare", () => { discoverBuildStub = sandbox.stub(); runtimeDelegateStub = { language: "nodejs", - runtime: "nodejs22", + runtime: latest("nodejs"), bin: "node", validate: sandbox.stub().resolves(), build: sandbox.stub().resolves(), @@ -51,7 +52,7 @@ describe("prepare", () => { platform: "gcfv2", entryPoint: "test", project: "project", - runtime: "nodejs22", + runtime: latest("nodejs"), httpsTrigger: {}, }, }), @@ -380,7 +381,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs22", + runtime: latest("nodejs"), httpsTrigger: {}, }; @@ -390,7 +391,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs22", + runtime: latest("nodejs"), callableTrigger: { genkitAction: "action", }, @@ -409,7 +410,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs22", + runtime: latest("nodejs"), callableTrigger: { genkitAction: "action", }, @@ -539,7 +540,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs22", + runtime: latest("nodejs"), httpsTrigger: {}, secretEnvironmentVariables: [ { @@ -567,7 +568,7 @@ describe("prepare", () => { region: "us-central1", project: "project", entryPoint: "entry", - runtime: "nodejs22", + runtime: latest("nodejs"), httpsTrigger: {}, }; From 24c971f64c34014f2ec8375656d11b76e7d9f955 Mon Sep 17 00:00:00 2001 From: Daniel Young Lee Date: Mon, 18 Aug 2025 19:15:35 -0700 Subject: [PATCH 21/21] style: run formatter. --- src/emulator/functionsEmulator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index 3b5384ade3e..89ee837e252 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -514,12 +514,12 @@ export class FunctionsEmulator implements EmulatorInstance { for (const pool of Object.values(this.workerPools)) { pool.exit(); } - + for (const watcher of this.watchers) { await watcher.close(); } this.watchers = []; - + if (this.destroyServer) { await this.destroyServer(); }