From eaaffe17ff5da2c50957338e145ba77fde34d3b9 Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Mon, 28 Sep 2026 16:53:05 -0400 Subject: [PATCH 1/4] Create a basic, initial schema for firebase.json --- schema/firebase-config.json | 69 ++++++++++++++++++++++++++++++ src/firebaseConfig.ts | 14 ++++++ src/firebaseConfigValidate.spec.ts | 17 ++++++++ 3 files changed, 100 insertions(+) diff --git a/schema/firebase-config.json b/schema/firebase-config.json index 4780e005206..97ff97e0526 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -1221,6 +1221,62 @@ ], "type": "object" }, + "RunSingle": { + "additionalProperties": false, + "properties": { + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "localBuild": { + "type": "boolean" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "region": { + "type": "string" + }, + "rootDir": { + "type": "string" + }, + "serviceAccount": { + "type": "string" + }, + "serviceId": { + "type": "string" + } + }, + "required": [ + "serviceId" + ], + "type": "object" + }, "StorageSingle": { "additionalProperties": false, "properties": { @@ -2008,6 +2064,19 @@ "remoteconfig": { "$ref": "#/definitions/RemoteConfigConfig" }, + "run": { + "anyOf": [ + { + "$ref": "#/definitions/RunSingle" + }, + { + "items": { + "$ref": "#/definitions/RunSingle" + }, + "type": "array" + } + ] + }, "storage": { "anyOf": [ { diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index c6061d37030..38c7a9ef7b9 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -366,6 +366,19 @@ export type AppHostingMultiple = AppHostingSingle[]; export type AppHostingConfig = AppHostingSingle | AppHostingMultiple; +export interface RunSingle extends Deployable { + serviceId: string; + region?: string; + rootDir?: string; + ignore?: string[]; + serviceAccount?: string; + localBuild?: boolean; +} + +export type RunMultiple = RunSingle[]; + +export type RunConfig = RunSingle | RunMultiple; + export interface AuthConfig { providers?: { anonymous?: boolean; @@ -392,4 +405,5 @@ export type FirebaseConfig = { dataconnect?: DataConnectConfig; apphosting?: AppHostingConfig; auth?: AuthConfig; + run?: RunConfig; }; diff --git a/src/firebaseConfigValidate.spec.ts b/src/firebaseConfigValidate.spec.ts index 327011a577b..56b3d149798 100644 --- a/src/firebaseConfigValidate.spec.ts +++ b/src/firebaseConfigValidate.spec.ts @@ -24,6 +24,23 @@ describe("firebaseConfigValidate", () => { expect(isValid).to.be.true; }); + it("should accept a valid run config", () => { + const config: FirebaseConfig = { + run: [ + { + serviceId: "my-service", + region: "us-central1", + rootDir: ".", + }, + ], + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.true; + }); + it("should report an extra top-level field", () => { // This config has an extra 'bananas' top-level property const config = { From 03f1e55bbeca04a4371d4be93ad616dd02ff0d7b Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Tue, 29 Sep 2026 10:35:24 -0400 Subject: [PATCH 2/4] improve testing --- src/firebaseConfigValidate.spec.ts | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/firebaseConfigValidate.spec.ts b/src/firebaseConfigValidate.spec.ts index 56b3d149798..fc104913d73 100644 --- a/src/firebaseConfigValidate.spec.ts +++ b/src/firebaseConfigValidate.spec.ts @@ -41,6 +41,92 @@ describe("firebaseConfigValidate", () => { expect(isValid).to.be.true; }); + it("should accept a single-object run config", () => { + const config: FirebaseConfig = { + run: { + serviceId: "my-service", + region: "us-central1", + localBuild: true, + }, + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.true; + }); + + it("should reject a run config missing serviceId", () => { + const config = { + run: [ + { + region: "us-central1", + rootDir: ".", + }, + ], + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.false; + expect(validator.errors).to.exist; + expect( + (validator.errors ?? []).some( + (e) => + e.keyword === "required" && + e.instancePath === "/run/0" && + e.params.missingProperty === "serviceId", + ), + ).to.be.true; + }); + + it("should reject a run config with an unknown field", () => { + const config = { + run: { + serviceId: "my-service", + bananas: true, + }, + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.false; + expect(validator.errors).to.exist; + expect( + (validator.errors ?? []).some( + (e) => + e.keyword === "additionalProperties" && + e.instancePath === "/run" && + e.params.additionalProperty === "bananas", + ), + ).to.be.true; + }); + + it("should reject a run config with an incorrect type", () => { + const config = { + run: { + serviceId: "my-service", + localBuild: "yes", + }, + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.false; + expect(validator.errors).to.exist; + expect( + (validator.errors ?? []).some( + (e) => + e.keyword === "type" && + e.instancePath === "/run/localBuild" && + e.params.type === "boolean", + ), + ).to.be.true; + }); + it("should report an extra top-level field", () => { // This config has an extra 'bananas' top-level property const config = { From a7290870374499dd25e6b6db0c149e42e22f03ec Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Tue, 29 Sep 2026 21:53:09 -0400 Subject: [PATCH 3/4] Require region, remove serviceAccount, and register run in Config.KNOWN_PROPS --- schema/firebase-config.json | 4 +- src/config.ts | 1 + src/firebaseConfig.ts | 3 +- src/firebaseConfigValidate.spec.ts | 112 ++++++++++++----------------- 4 files changed, 48 insertions(+), 72 deletions(-) diff --git a/schema/firebase-config.json b/schema/firebase-config.json index 97ff97e0526..e646dd371d3 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -1265,14 +1265,12 @@ "rootDir": { "type": "string" }, - "serviceAccount": { - "type": "string" - }, "serviceId": { "type": "string" } }, "required": [ + "region", "serviceId" ], "type": "object" diff --git a/src/config.ts b/src/config.ts index ffd9998c88b..cd20b9e1fa0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -34,6 +34,7 @@ export class Config { "apphosting", "auth", "ailogic", + "run", ]; public options: any; diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index 38c7a9ef7b9..7137aedc70b 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -368,10 +368,9 @@ export type AppHostingConfig = AppHostingSingle | AppHostingMultiple; export interface RunSingle extends Deployable { serviceId: string; - region?: string; + region: string; rootDir?: string; ignore?: string[]; - serviceAccount?: string; localBuild?: boolean; } diff --git a/src/firebaseConfigValidate.spec.ts b/src/firebaseConfigValidate.spec.ts index fc104913d73..0ee2a2751fe 100644 --- a/src/firebaseConfigValidate.spec.ts +++ b/src/firebaseConfigValidate.spec.ts @@ -24,6 +24,24 @@ describe("firebaseConfigValidate", () => { expect(isValid).to.be.true; }); + function expectValidationError( + config: unknown, + keyword: string, + instancePath: string, + params: Record, + ): void { + const validator = getValidator(); + expect(validator(config)).to.be.false; + expect( + (validator.errors ?? []).some( + (e) => + e.keyword === keyword && + e.instancePath === instancePath && + Object.entries(params).every(([k, v]) => e.params[k] === v), + ), + ).to.be.true; + } + it("should accept a valid run config", () => { const config: FirebaseConfig = { run: [ @@ -36,9 +54,7 @@ describe("firebaseConfigValidate", () => { }; const validator = getValidator(); - const isValid = validator(config); - - expect(isValid).to.be.true; + expect(validator(config)).to.be.true; }); it("should accept a single-object run config", () => { @@ -51,80 +67,42 @@ describe("firebaseConfigValidate", () => { }; const validator = getValidator(); - const isValid = validator(config); - - expect(isValid).to.be.true; + expect(validator(config)).to.be.true; }); it("should reject a run config missing serviceId", () => { - const config = { - run: [ - { - region: "us-central1", - rootDir: ".", - }, - ], - }; - - const validator = getValidator(); - const isValid = validator(config); + expectValidationError( + { run: [{ region: "us-central1", rootDir: "." }] }, + "required", + "/run/0", + { + missingProperty: "serviceId", + }, + ); + }); - expect(isValid).to.be.false; - expect(validator.errors).to.exist; - expect( - (validator.errors ?? []).some( - (e) => - e.keyword === "required" && - e.instancePath === "/run/0" && - e.params.missingProperty === "serviceId", - ), - ).to.be.true; + it("should reject a run config missing region", () => { + expectValidationError({ run: { serviceId: "my-service" } }, "required", "/run", { + missingProperty: "region", + }); }); it("should reject a run config with an unknown field", () => { - const config = { - run: { - serviceId: "my-service", - bananas: true, - }, - }; - - const validator = getValidator(); - const isValid = validator(config); - - expect(isValid).to.be.false; - expect(validator.errors).to.exist; - expect( - (validator.errors ?? []).some( - (e) => - e.keyword === "additionalProperties" && - e.instancePath === "/run" && - e.params.additionalProperty === "bananas", - ), - ).to.be.true; + expectValidationError( + { run: { serviceId: "my-service", region: "us-central1", bananas: true } }, + "additionalProperties", + "/run", + { additionalProperty: "bananas" }, + ); }); it("should reject a run config with an incorrect type", () => { - const config = { - run: { - serviceId: "my-service", - localBuild: "yes", - }, - }; - - const validator = getValidator(); - const isValid = validator(config); - - expect(isValid).to.be.false; - expect(validator.errors).to.exist; - expect( - (validator.errors ?? []).some( - (e) => - e.keyword === "type" && - e.instancePath === "/run/localBuild" && - e.params.type === "boolean", - ), - ).to.be.true; + expectValidationError( + { run: { serviceId: "my-service", region: "us-central1", localBuild: "yes" } }, + "type", + "/run/localBuild", + { type: "boolean" }, + ); }); it("should report an extra top-level field", () => { From ee94ae44271d136fcb8b2ca55236a087258a1b20 Mon Sep 17 00:00:00 2001 From: Aryan Falahatpisheh Date: Wed, 30 Sep 2026 13:50:27 -0400 Subject: [PATCH 4/4] Remove unused Deployable predeploy/postdeploy from RunSingle --- schema/firebase-config.json | 26 -------------------------- src/firebaseConfig.ts | 2 +- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/schema/firebase-config.json b/schema/firebase-config.json index e646dd371d3..1338124e049 100644 --- a/schema/firebase-config.json +++ b/schema/firebase-config.json @@ -1233,32 +1233,6 @@ "localBuild": { "type": "boolean" }, - "postdeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, - "predeploy": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "string" - } - ] - }, "region": { "type": "string" }, diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts index 7137aedc70b..71943fd7faa 100644 --- a/src/firebaseConfig.ts +++ b/src/firebaseConfig.ts @@ -366,7 +366,7 @@ export type AppHostingMultiple = AppHostingSingle[]; export type AppHostingConfig = AppHostingSingle | AppHostingMultiple; -export interface RunSingle extends Deployable { +export interface RunSingle { serviceId: string; region: string; rootDir?: string;