From 4bea57827f8256925beb54005fd2148d74577d5f Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Tue, 9 Dec 2025 11:45:03 +0000 Subject: [PATCH 1/4] test: add failing test for issue 8841 --- src/gcp/cloudfunctionsv2.spec.ts | 20 ++++++++++++++++++++ src/gcp/cloudfunctionsv2.ts | 8 ++++++++ 2 files changed, 28 insertions(+) diff --git a/src/gcp/cloudfunctionsv2.spec.ts b/src/gcp/cloudfunctionsv2.spec.ts index f45fbfa7861..0b6827cac7e 100644 --- a/src/gcp/cloudfunctionsv2.spec.ts +++ b/src/gcp/cloudfunctionsv2.spec.ts @@ -427,6 +427,26 @@ describe("cloudfunctionsv2", () => { }, }); }); + + it("should set buildConfig.serviceAccount when serviceAccount is specified", () => { + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + serviceAccount: "custom@project.iam.gserviceaccount.com", + httpsTrigger: {}, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + buildConfig: { + ...CLOUD_FUNCTION_V2.buildConfig, + serviceAccount: "projects/project/serviceAccounts/custom@project.iam.gserviceaccount.com", + }, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + serviceAccountEmail: "custom@project.iam.gserviceaccount.com", + }, + }); + }); }); describe("endpointFromFunction", () => { diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index aaafac63f78..9a8ba01aaa4 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -51,6 +51,10 @@ export interface BuildConfig { source: Source; sourceToken?: string; environmentVariables: Record; + // TODO(#8841): Add serviceAccount field to support custom service accounts for builds. + // The GCF v2 API supports buildConfig.serviceAccount but we're not setting it, + // causing deployments to fail when the default compute SA is deleted. + // See: https://github.com/firebase/firebase-tools/issues/8841 // Output only build?: string; @@ -454,6 +458,10 @@ export function functionFromEndpoint(endpoint: backend.Endpoint): InputCloudFunc }, // We don't use build environment variables, environmentVariables: {}, + // TODO(#8841): Set serviceAccount here to match the runtime service account. + // When endpoint.serviceAccount is specified, we should set: + // serviceAccount: proto.formatServiceAccount(endpoint.serviceAccount, endpoint.project, true) + // This ensures the custom SA is used for building, not just runtime. }, serviceConfig: {}, }; From 203a9ea7883a280760578aa144d9e2a926355046 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 9 Dec 2025 15:53:10 +0300 Subject: [PATCH 2/4] fix(functions): set buildConfig.serviceAccount for GCFv2 deployments --- src/gcp/cloudfunctionsv2.spec.ts | 16 ++++++++++++++++ src/gcp/cloudfunctionsv2.ts | 18 ++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/gcp/cloudfunctionsv2.spec.ts b/src/gcp/cloudfunctionsv2.spec.ts index 0b6827cac7e..ede46bf3961 100644 --- a/src/gcp/cloudfunctionsv2.spec.ts +++ b/src/gcp/cloudfunctionsv2.spec.ts @@ -244,6 +244,10 @@ describe("cloudfunctionsv2", () => { const fullGcfFunction: cloudfunctionsv2.InputCloudFunction = { ...CLOUD_FUNCTION_V2, + buildConfig: { + ...CLOUD_FUNCTION_V2.buildConfig, + serviceAccount: "projects/project/serviceAccounts/inlined@google.com", + }, labels: { ...CLOUD_FUNCTION_V2.labels, foo: "bar", @@ -332,6 +336,10 @@ describe("cloudfunctionsv2", () => { const saGcfFunction: cloudfunctionsv2.InputCloudFunction = { ...CLOUD_FUNCTION_V2, + buildConfig: { + ...CLOUD_FUNCTION_V2.buildConfig, + serviceAccount: "projects/project/serviceAccounts/sa@google.com", + }, eventTrigger: { eventType: events.v2.DATABASE_EVENTS[0], eventFilters: [ @@ -405,6 +413,10 @@ describe("cloudfunctionsv2", () => { }), ).to.deep.equal({ ...CLOUD_FUNCTION_V2, + buildConfig: { + ...CLOUD_FUNCTION_V2.buildConfig, + serviceAccount: `projects/${ENDPOINT.project}/serviceAccounts/sa@${ENDPOINT.project}.iam.gserviceaccount.com`, + }, serviceConfig: { ...CLOUD_FUNCTION_V2.serviceConfig, serviceAccountEmail: `sa@${ENDPOINT.project}.iam.gserviceaccount.com`, @@ -421,6 +433,10 @@ describe("cloudfunctionsv2", () => { }), ).to.deep.equal({ ...CLOUD_FUNCTION_V2, + buildConfig: { + ...CLOUD_FUNCTION_V2.buildConfig, + serviceAccount: null, + }, serviceConfig: { ...CLOUD_FUNCTION_V2.serviceConfig, serviceAccountEmail: null, diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index 9a8ba01aaa4..35d7fb7ebf2 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -51,10 +51,7 @@ export interface BuildConfig { source: Source; sourceToken?: string; environmentVariables: Record; - // TODO(#8841): Add serviceAccount field to support custom service accounts for builds. - // The GCF v2 API supports buildConfig.serviceAccount but we're not setting it, - // causing deployments to fail when the default compute SA is deleted. - // See: https://github.com/firebase/firebase-tools/issues/8841 + serviceAccount?: string | null; // Output only build?: string; @@ -458,10 +455,15 @@ export function functionFromEndpoint(endpoint: backend.Endpoint): InputCloudFunc }, // We don't use build environment variables, environmentVariables: {}, - // TODO(#8841): Set serviceAccount here to match the runtime service account. - // When endpoint.serviceAccount is specified, we should set: - // serviceAccount: proto.formatServiceAccount(endpoint.serviceAccount, endpoint.project, true) - // This ensures the custom SA is used for building, not just runtime. + ...(endpoint.serviceAccount !== undefined && { + serviceAccount: endpoint.serviceAccount + ? `projects/${endpoint.project}/serviceAccounts/${proto.formatServiceAccount( + endpoint.serviceAccount, + endpoint.project, + true, + )}` + : null, + }), }, serviceConfig: {}, }; From 14df88caf45136930165e10f8f044989c1d4efa5 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 9 Dec 2025 18:45:19 +0300 Subject: [PATCH 3/4] refactor(functions): use proto.convertIfPresent for buildConfig.serviceAccount --- src/gcp/cloudfunctionsv2.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index 35d7fb7ebf2..ad7c31c6168 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -455,20 +455,25 @@ export function functionFromEndpoint(endpoint: backend.Endpoint): InputCloudFunc }, // We don't use build environment variables, environmentVariables: {}, - ...(endpoint.serviceAccount !== undefined && { - serviceAccount: endpoint.serviceAccount - ? `projects/${endpoint.project}/serviceAccounts/${proto.formatServiceAccount( - endpoint.serviceAccount, - endpoint.project, - true, - )}` - : null, - }), }, serviceConfig: {}, }; proto.copyIfPresent(gcfFunction, endpoint, "labels"); + proto.convertIfPresent( + gcfFunction.buildConfig, + endpoint, + "serviceAccount", + "serviceAccount", + (from) => + !from + ? null + : `projects/${endpoint.project}/serviceAccounts/${proto.formatServiceAccount( + from, + endpoint.project, + true, + )}`, + ); proto.copyIfPresent( gcfFunction.serviceConfig, endpoint, From 7692be0563c5130f3958d63b51cfe56044790eb4 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Tue, 9 Dec 2025 20:21:30 +0300 Subject: [PATCH 4/4] refactor(functions): service account logic in functionFromEndpoint --- src/gcp/cloudfunctionsv2.spec.ts | 20 ------------------ src/gcp/cloudfunctionsv2.ts | 36 +++++++++++--------------------- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/src/gcp/cloudfunctionsv2.spec.ts b/src/gcp/cloudfunctionsv2.spec.ts index ede46bf3961..ff2c8ecef06 100644 --- a/src/gcp/cloudfunctionsv2.spec.ts +++ b/src/gcp/cloudfunctionsv2.spec.ts @@ -443,26 +443,6 @@ describe("cloudfunctionsv2", () => { }, }); }); - - it("should set buildConfig.serviceAccount when serviceAccount is specified", () => { - expect( - cloudfunctionsv2.functionFromEndpoint({ - ...ENDPOINT, - serviceAccount: "custom@project.iam.gserviceaccount.com", - httpsTrigger: {}, - }), - ).to.deep.equal({ - ...CLOUD_FUNCTION_V2, - buildConfig: { - ...CLOUD_FUNCTION_V2.buildConfig, - serviceAccount: "projects/project/serviceAccounts/custom@project.iam.gserviceaccount.com", - }, - serviceConfig: { - ...CLOUD_FUNCTION_V2.serviceConfig, - serviceAccountEmail: "custom@project.iam.gserviceaccount.com", - }, - }); - }); }); describe("endpointFromFunction", () => { diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index ad7c31c6168..e564c12bcad 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -460,20 +460,6 @@ export function functionFromEndpoint(endpoint: backend.Endpoint): InputCloudFunc }; proto.copyIfPresent(gcfFunction, endpoint, "labels"); - proto.convertIfPresent( - gcfFunction.buildConfig, - endpoint, - "serviceAccount", - "serviceAccount", - (from) => - !from - ? null - : `projects/${endpoint.project}/serviceAccounts/${proto.formatServiceAccount( - from, - endpoint.project, - true, - )}`, - ); proto.copyIfPresent( gcfFunction.serviceConfig, endpoint, @@ -482,16 +468,18 @@ export function functionFromEndpoint(endpoint: backend.Endpoint): InputCloudFunc "ingressSettings", "timeoutSeconds", ); - proto.convertIfPresent( - gcfFunction.serviceConfig, - endpoint, - "serviceAccountEmail", - "serviceAccount", - (from) => - !from - ? null - : proto.formatServiceAccount(from, endpoint.project, true /* removeTypePrefix */), - ); + + if (Object.prototype.hasOwnProperty.call(endpoint, "serviceAccount")) { + const serviceAccount = endpoint.serviceAccount; + if (!serviceAccount) { + gcfFunction.buildConfig.serviceAccount = null; + gcfFunction.serviceConfig.serviceAccountEmail = null; + } else { + const email = proto.formatServiceAccount(serviceAccount, endpoint.project, true); + gcfFunction.buildConfig.serviceAccount = `projects/${endpoint.project}/serviceAccounts/${email}`; + gcfFunction.serviceConfig.serviceAccountEmail = email; + } + } // Memory must be set because the default value of GCF gen 2 is Megabytes and // we use mebibytes const mem = endpoint.availableMemoryMb || backend.DEFAULT_MEMORY;