From cff4fb2ed0aa440b17e4d2a22a3a09f0663203c3 Mon Sep 17 00:00:00 2001 From: Varun Shetty Date: Wed, 12 Aug 2026 21:30:21 +0000 Subject: [PATCH 1/6] Adding codebase support to firebase functions:delete --- src/commands/functions-delete.ts | 43 ++++++++++++++++--- .../functions/functionsDeployHelper.spec.ts | 38 ++++++++++++++++ src/deploy/functions/functionsDeployHelper.ts | 42 ++++++++++-------- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/commands/functions-delete.ts b/src/commands/functions-delete.ts index 8feb45734b2..320ce253683 100644 --- a/src/commands/functions-delete.ts +++ b/src/commands/functions-delete.ts @@ -12,6 +12,7 @@ import * as args from "../deploy/functions/args"; import * as helper from "../deploy/functions/functionsDeployHelper"; import * as utils from "../utils"; import * as backend from "../deploy/functions/backend"; +import * as projectConfig from "../functions/projectConfig"; import * as planner from "../deploy/functions/release/planner"; import * as fabricator from "../deploy/functions/release/fabricator"; import * as executor from "../deploy/functions/release/executor"; @@ -19,7 +20,7 @@ import * as reporter from "../deploy/functions/release/reporter"; import { getProjectNumber } from "../getProjectNumber"; export const command = new Command("functions:delete [filters...]") - .description("delete one or more Cloud Functions by name or group name.") + .description("delete one or more Cloud Functions by name, group name, or codebase.") .option( "--region ", "Specify region of the function to be deleted. " + @@ -32,17 +33,25 @@ export const command = new Command("functions:delete [filters...]") return utils.reject("Must supply at least function or group name."); } + // Normalize project configuration to discover all registered codebases. + // Gracefully fall back to an empty configuration if running outside a Firebase directory or without functions. + // Parse filters using parseFunctionSelector for 1:1 syntax parity with `firebase deploy --only functions:...`. + const config = options.config?.src?.functions + ? projectConfig.normalizeAndValidate(options.config.src.functions) + : []; + const parsedFilters = filters.flatMap((f) => helper.parseFunctionSelector(f, config)); + const context: args.Context = { projectId: needProjectId(options), - filters: filters.map((f) => ({ idChunks: f.split(/[-.]/) })), + filters: parsedFilters, }; - const [config, existingBackend] = await Promise.all([ + const [firebaseConfig, existingBackend] = await Promise.all([ functionsConfig.getFirebaseConfig(options), backend.existingBackend(context), ]); await backend.checkAvailability(context, /* want=*/ backend.empty()); - const appEngineLocation = functionsConfig.getAppEngineLocation(config); + const appEngineLocation = functionsConfig.getAppEngineLocation(firebaseConfig); if (options.region) { existingBackend.endpoints = { [options.region]: existingBackend.endpoints[options.region] }; @@ -67,6 +76,30 @@ export const command = new Command("functions:delete [filters...]") ); } + // Inform the user when a name collision exists between a codebase name and a function name in default. + // Codebase deletion takes precedence by design, but we provide the explicit 'default:' workaround. + const codebaseNames = config.map((c) => c.codebase); + const defaultEndpoints = backend + .allEndpoints(existingBackend) + .filter((ep) => !ep.codebase || ep.codebase === projectConfig.DEFAULT_CODEBASE); + + for (const f of filters) { + if (!f.includes(":") && codebaseNames.includes(f)) { + const hasDefaultCollision = defaultEndpoints.some( + (ep) => ep.id === f || ep.id.startsWith(`${f}-`), + ); + if (hasDefaultCollision) { + utils.logLabeledBullet( + "functions", + `Target '${clc.bold(f)}' matches both a codebase and a function. Codebase deletion takes precedence. ` + + `(To delete the function in the default codebase instead, run: ${clc.bold( + `firebase functions:delete default:${f}`, + )})`, + ); + } + } + } + const deleteList = allEpToDelete.map((func) => `\t${helper.getFunctionLabel(func)}`).join("\n"); const confirmDeletion = await confirm({ message: @@ -104,7 +137,7 @@ export const command = new Command("functions:delete [filters...]") await reporter.logAndTrackDeployStats(summary); reporter.printErrors(summary); - } catch (err: any) { + } catch (err: unknown) { throw new FirebaseError("Failed to delete functions", { original: err as Error, exit: 1, diff --git a/src/deploy/functions/functionsDeployHelper.spec.ts b/src/deploy/functions/functionsDeployHelper.spec.ts index 07b8922f7c5..0fb85a717ef 100644 --- a/src/deploy/functions/functionsDeployHelper.spec.ts +++ b/src/deploy/functions/functionsDeployHelper.spec.ts @@ -135,6 +135,44 @@ describe("functionsDeployHelper", () => { }), ).to.be.true; }); + + it("should match all functions in a codebase when idChunks is not provided", () => { + const func1 = { ...ENDPOINT, id: "func1", codebase: "my-codebase" }; + const func2 = { ...ENDPOINT, id: "func2", codebase: "my-codebase" }; + const otherFunc = { ...ENDPOINT, id: "func3", codebase: "other-codebase" }; + const undefinedFunc = { ...ENDPOINT, id: "func4", codebase: undefined }; + + const filter: EndpointFilter = { codebase: "my-codebase" }; + expect(helper.endpointMatchesFilter(func1, filter)).to.be.true; + expect(helper.endpointMatchesFilter(func2, filter)).to.be.true; + expect(helper.endpointMatchesFilter(otherFunc, filter)).to.be.false; + expect(helper.endpointMatchesFilter(undefinedFunc, filter)).to.be.false; + }); + + it("should not match overlapping codebase names", () => { + const instance1Func = { ...ENDPOINT, id: "foo", codebase: "kit-firestore-to-bigquery" }; + const instance2Func = { ...ENDPOINT, id: "foo", codebase: "kit-firestore-to-bigquery-abcd" }; + + const filter: EndpointFilter = { + codebase: "kit-firestore-to-bigquery", + }; + + expect(helper.endpointMatchesFilter(instance1Func, filter)).to.be.true; + expect(helper.endpointMatchesFilter(instance2Func, filter)).to.be.false; + }); + + it("should not match functions with overlapping word prefixes", () => { + const appFunc = { ...ENDPOINT, id: "app-render" }; + const appleFunc = { ...ENDPOINT, id: "apple-pay" }; + + const filter: EndpointFilter = { + codebase: DEFAULT_CODEBASE, + idChunks: ["app"], + }; + + expect(helper.endpointMatchesFilter(appFunc, filter)).to.be.true; + expect(helper.endpointMatchesFilter(appleFunc, filter)).to.be.false; + }); }); describe("endpointMatchesAnyFilters", () => { diff --git a/src/deploy/functions/functionsDeployHelper.ts b/src/deploy/functions/functionsDeployHelper.ts index 77a8ee0fce4..a7aac7789f1 100644 --- a/src/deploy/functions/functionsDeployHelper.ts +++ b/src/deploy/functions/functionsDeployHelper.ts @@ -1,5 +1,10 @@ import * as backend from "./backend"; -import { DEFAULT_CODEBASE, ValidatedConfig, isKitConfig } from "../../functions/projectConfig"; +import { + DEFAULT_CODEBASE, + ValidatedConfig, + ValidatedSingle, + isKitConfig, +} from "../../functions/projectConfig"; import { assertExhaustive } from "../../functional"; export interface EndpointFilter { @@ -27,37 +32,35 @@ export function endpointMatchesAnyFilter( /** * Returns true if endpoint matches the given filter. + * Supports filtering by codebase, exact function name, or hierarchical function group. */ export function endpointMatchesFilter(endpoint: backend.Endpoint, filter: EndpointFilter): boolean { - // Only enforce codebase-based filtering when both the endpoint and filter provides them. - // This allows us to filter using idChunks across all codebases. + // Only enforce codebase-based filtering when both the endpoint and filter provide them. + // This allows us to filter using idChunks across all codebases or target a specific codebase. if (endpoint.codebase && filter.codebase) { if (endpoint.codebase !== filter.codebase) { return false; } } - if (!filter.idChunks) { - // If idChunks is not provided, we match all functions. + // If idChunks is not provided or empty, the filter matches all functions within the targeted codebase. + if (!filter.idChunks || filter.idChunks.length === 0) { + if (filter.codebase) { + return (endpoint.codebase || DEFAULT_CODEBASE) === filter.codebase; + } return true; } - const idChunks = endpoint.id.split("-"); - if (idChunks.length < filter.idChunks.length) { - return false; - } - for (let i = 0; i < filter.idChunks.length; i += 1) { - if (idChunks[i] !== filter.idChunks[i]) { - return false; - } - } - return true; + // Exact function match (e.g. 'myFunc') or hierarchical group match (e.g. 'groupA' matches 'groupA-func1'). + // Enforces a strict hyphen boundary so 'app' does not match 'apple-pay'. + const filterPrefix = filter.idChunks.join("-"); + return endpoint.id === filterPrefix || endpoint.id.startsWith(`${filterPrefix}-`); } /** * Returns all codebase names and kit instance IDs defined in the configuration. */ -export function getCodebasesFromConfig(config: ValidatedConfig): string[] { +export function getCodebasesFromConfig(config: ValidatedSingle[] = []): string[] { return [ ...new Set(config.flatMap((c) => (isKitConfig(c) ? Object.keys(c.instances) : [c.codebase]))), ]; @@ -66,7 +69,10 @@ export function getCodebasesFromConfig(config: ValidatedConfig): string[] { /** * Returns list of filters after parsing selector. */ -export function parseFunctionSelector(selector: string, config: ValidatedConfig): EndpointFilter[] { +export function parseFunctionSelector( + selector: string, + config: ValidatedSingle[] = [], +): EndpointFilter[] { const fragments = selector.split(":"); const target = fragments[0]; @@ -241,6 +247,6 @@ export function isCodebaseFiltered(codebase: string, filters: EndpointFilter[]): } /** Checks if a function should be filtered given a list of endpoints. */ -export function isEndpointFiltered(endpoint: backend.Endpoint, filters: EndpointFilter[]) { +export function isEndpointFiltered(endpoint: backend.Endpoint, filters: EndpointFilter[]): boolean { return filters.some((filter) => endpointMatchesFilter(endpoint, filter)); } From defe96e9a508917bc20632d0e94f225db5a7e7b9 Mon Sep 17 00:00:00 2001 From: Varun Shetty Date: Wed, 12 Aug 2026 22:47:16 +0000 Subject: [PATCH 2/6] test: add test for targeting specific functions across codebases --- .../functions/functionsDeployHelper.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/deploy/functions/functionsDeployHelper.spec.ts b/src/deploy/functions/functionsDeployHelper.spec.ts index 0fb85a717ef..9e401ac6ddb 100644 --- a/src/deploy/functions/functionsDeployHelper.spec.ts +++ b/src/deploy/functions/functionsDeployHelper.spec.ts @@ -149,6 +149,19 @@ describe("functionsDeployHelper", () => { expect(helper.endpointMatchesFilter(undefinedFunc, filter)).to.be.false; }); + it("should match a specific function in a specific codebase when multiple codebases have functions with the same name", () => { + const funcInCodebaseA = { ...ENDPOINT, id: "foo", codebase: "codebaseA" }; + const funcInCodebaseB = { ...ENDPOINT, id: "foo", codebase: "codebaseB" }; + + const filter: EndpointFilter = { + codebase: "codebaseA", + idChunks: ["foo"], + }; + + expect(helper.endpointMatchesFilter(funcInCodebaseA, filter)).to.be.true; + expect(helper.endpointMatchesFilter(funcInCodebaseB, filter)).to.be.false; + }); + it("should not match overlapping codebase names", () => { const instance1Func = { ...ENDPOINT, id: "foo", codebase: "kit-firestore-to-bigquery" }; const instance2Func = { ...ENDPOINT, id: "foo", codebase: "kit-firestore-to-bigquery-abcd" }; @@ -268,6 +281,20 @@ describe("functionsDeployHelper", () => { }, ], }, + { + desc: "parses codebase-qualified selector (codebase:func)", + selector: "codebaseA:foo", + config: [ + { source: "functions", codebase: "codebaseA" }, + { source: "other", codebase: "codebaseB" }, + ] as ValidatedConfig, + expected: [ + { + codebase: "codebaseA", + idChunks: ["foo"], + }, + ], + }, ]; for (const tc of testcases) { From 0071ef7113a8c68b71ceb00d4ef852c5ef879e34 Mon Sep 17 00:00:00 2001 From: Varun Shetty Date: Thu, 13 Aug 2026 01:41:42 +0000 Subject: [PATCH 3/6] fix(functions): use getCodebasesFromConfig in functions:delete collision check --- src/commands/functions-delete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/functions-delete.ts b/src/commands/functions-delete.ts index 320ce253683..2eadeb3dca3 100644 --- a/src/commands/functions-delete.ts +++ b/src/commands/functions-delete.ts @@ -78,7 +78,7 @@ export const command = new Command("functions:delete [filters...]") // Inform the user when a name collision exists between a codebase name and a function name in default. // Codebase deletion takes precedence by design, but we provide the explicit 'default:' workaround. - const codebaseNames = config.map((c) => c.codebase); + const codebaseNames = helper.getCodebasesFromConfig(config); const defaultEndpoints = backend .allEndpoints(existingBackend) .filter((ep) => !ep.codebase || ep.codebase === projectConfig.DEFAULT_CODEBASE); From 84bad741b64703a1646e9ba96a07c784c142fae0 Mon Sep 17 00:00:00 2001 From: Varun Shetty Date: Thu, 13 Aug 2026 19:42:36 +0000 Subject: [PATCH 4/6] fix(functions): handle legacy endpoints and allow bare function deletion across codebases --- src/commands/functions-delete.ts | 4 ++- .../functions/functionsDeployHelper.spec.ts | 32 +++++++++++++++++-- src/deploy/functions/functionsDeployHelper.ts | 25 +++++++++------ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/commands/functions-delete.ts b/src/commands/functions-delete.ts index 2eadeb3dca3..f7d9e1c038f 100644 --- a/src/commands/functions-delete.ts +++ b/src/commands/functions-delete.ts @@ -39,7 +39,9 @@ export const command = new Command("functions:delete [filters...]") const config = options.config?.src?.functions ? projectConfig.normalizeAndValidate(options.config.src.functions) : []; - const parsedFilters = filters.flatMap((f) => helper.parseFunctionSelector(f, config)); + const parsedFilters = filters.flatMap((f) => + helper.parseFunctionSelector(f, config, /* defaultCodebase= */ undefined), + ); const context: args.Context = { projectId: needProjectId(options), diff --git a/src/deploy/functions/functionsDeployHelper.spec.ts b/src/deploy/functions/functionsDeployHelper.spec.ts index 9e401ac6ddb..5ca99a16126 100644 --- a/src/deploy/functions/functionsDeployHelper.spec.ts +++ b/src/deploy/functions/functionsDeployHelper.spec.ts @@ -95,19 +95,26 @@ describe("functionsDeployHelper", () => { expect( helper.endpointMatchesFilter(func, { ...BASE_FILTER, - codebase: "my-codebase", + codebase: DEFAULT_CODEBASE, idChunks: ["group", "subgroup", "func"], }), ).to.be.true; expect( helper.endpointMatchesFilter(func, { ...BASE_FILTER, - codebase: "my-codebase", + codebase: DEFAULT_CODEBASE, idChunks: ["group", "subgroup"], }), ).to.be.true; expect(helper.endpointMatchesFilter(func, { ...BASE_FILTER, idChunks: ["group"] })).to.be .true; + expect( + helper.endpointMatchesFilter(func, { + ...BASE_FILTER, + codebase: "non-default-codebase", + idChunks: ["group", "subgroup", "func"], + }), + ).to.be.false; }); it("should match function matching ids given no codebase", () => { @@ -220,6 +227,7 @@ describe("functionsDeployHelper", () => { desc: string; selector: string; config: ValidatedConfig; + defaultCodebase?: string; expected: EndpointFilter[]; } @@ -228,6 +236,7 @@ describe("functionsDeployHelper", () => { desc: "parses selector without codebase (not a codebase name)", selector: "func", config: [{ source: "functions", codebase: DEFAULT_CODEBASE }] as ValidatedConfig, + defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: DEFAULT_CODEBASE, @@ -242,6 +251,7 @@ describe("functionsDeployHelper", () => { { source: "functions", codebase: DEFAULT_CODEBASE }, { source: "other", codebase: "func" }, ] as ValidatedConfig, + defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: "func", @@ -252,6 +262,7 @@ describe("functionsDeployHelper", () => { desc: "parses group selector (with '.') without codebase", selector: "g1.func", config: [{ source: "functions", codebase: DEFAULT_CODEBASE }] as ValidatedConfig, + defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: DEFAULT_CODEBASE, @@ -263,6 +274,7 @@ describe("functionsDeployHelper", () => { desc: "parses group selector (with '-') without codebase", selector: "g1-func", config: [{ source: "functions", codebase: DEFAULT_CODEBASE }] as ValidatedConfig, + defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: DEFAULT_CODEBASE, @@ -295,11 +307,25 @@ describe("functionsDeployHelper", () => { }, ], }, + { + desc: "parses bare selector without default codebase", + selector: "foo", + config: [ + { source: "functions", codebase: "codebaseA" }, + { source: "other", codebase: "codebaseB" }, + ] as ValidatedConfig, + defaultCodebase: undefined, + expected: [ + { + idChunks: ["foo"], + }, + ], + }, ]; for (const tc of testcases) { it(tc.desc, () => { - const actual = parseFunctionSelector(tc.selector, tc.config); + const actual = parseFunctionSelector(tc.selector, tc.config, tc.defaultCodebase); expect(actual.length).to.equal(tc.expected.length); expect(actual).to.deep.include.members(tc.expected); diff --git a/src/deploy/functions/functionsDeployHelper.ts b/src/deploy/functions/functionsDeployHelper.ts index a7aac7789f1..ccd8aa43c04 100644 --- a/src/deploy/functions/functionsDeployHelper.ts +++ b/src/deploy/functions/functionsDeployHelper.ts @@ -35,19 +35,17 @@ export function endpointMatchesAnyFilter( * Supports filtering by codebase, exact function name, or hierarchical function group. */ export function endpointMatchesFilter(endpoint: backend.Endpoint, filter: EndpointFilter): boolean { - // Only enforce codebase-based filtering when both the endpoint and filter provide them. - // This allows us to filter using idChunks across all codebases or target a specific codebase. - if (endpoint.codebase && filter.codebase) { - if (endpoint.codebase !== filter.codebase) { + // If the filter targets a specific codebase, verify that the endpoint belongs to it. + // Endpoints without an explicit codebase label default to the default codebase. + if (filter.codebase) { + const endpointCodebase = endpoint.codebase || DEFAULT_CODEBASE; + if (endpointCodebase !== filter.codebase) { return false; } } // If idChunks is not provided or empty, the filter matches all functions within the targeted codebase. if (!filter.idChunks || filter.idChunks.length === 0) { - if (filter.codebase) { - return (endpoint.codebase || DEFAULT_CODEBASE) === filter.codebase; - } return true; } @@ -72,6 +70,7 @@ export function getCodebasesFromConfig(config: ValidatedSingle[] = []): string[] export function parseFunctionSelector( selector: string, config: ValidatedSingle[] = [], + defaultCodebase?: string, ): EndpointFilter[] { const fragments = selector.split(":"); const target = fragments[0]; @@ -89,8 +88,14 @@ export function parseFunctionSelector( } if (fragments.length < 2) { - // It's not a codebase or kit instance name, assume it is a function id in default codebase - return [{ codebase: DEFAULT_CODEBASE, idChunks: fragments[0].split(/[-.]/) }]; + // If not a known codebase name and no codebase prefix provided, + // apply defaultCodebase if specified (e.g. for deploy --only). + return [ + { + ...(defaultCodebase ? { codebase: defaultCodebase } : {}), + idChunks: fragments[0].split(/[-.]/), + }, + ]; } return [ { @@ -137,7 +142,7 @@ export function getEndpointFilters( if (selector.startsWith("functions:")) { selector = selector.replace("functions:", ""); if (selector.length > 0) { - filters.push(...parseFunctionSelector(selector, config)); + filters.push(...parseFunctionSelector(selector, config, DEFAULT_CODEBASE)); } } } From b4f95b8506d07bec36a957077c93ca3062ead73e Mon Sep 17 00:00:00 2001 From: Varun Shetty Date: Mon, 17 Aug 2026 16:15:14 +0000 Subject: [PATCH 5/6] Address review comments --- src/commands/functions-delete.ts | 76 ++++++++++++------- .../functions/functionsDeployHelper.spec.ts | 21 +---- src/deploy/functions/functionsDeployHelper.ts | 13 +--- 3 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/commands/functions-delete.ts b/src/commands/functions-delete.ts index f7d9e1c038f..019a9b918b5 100644 --- a/src/commands/functions-delete.ts +++ b/src/commands/functions-delete.ts @@ -39,25 +39,46 @@ export const command = new Command("functions:delete [filters...]") const config = options.config?.src?.functions ? projectConfig.normalizeAndValidate(options.config.src.functions) : []; - const parsedFilters = filters.flatMap((f) => - helper.parseFunctionSelector(f, config, /* defaultCodebase= */ undefined), - ); - const context: args.Context = { projectId: needProjectId(options), - filters: parsedFilters, + filters: [], }; - - const [firebaseConfig, existingBackend] = await Promise.all([ + const [firebaseConfig, letExistingBackend] = await Promise.all([ functionsConfig.getFirebaseConfig(options), backend.existingBackend(context), ]); + let existingBackend = letExistingBackend; await backend.checkAvailability(context, /* want=*/ backend.empty()); const appEngineLocation = functionsConfig.getAppEngineLocation(firebaseConfig); if (options.region) { - existingBackend.endpoints = { [options.region]: existingBackend.endpoints[options.region] }; + existingBackend = backend.matchingBackend( + existingBackend, + (ep) => ep.region === options.region, + ); } + + // Discover all codebases defined in configuration OR active in existing prod backend. + const activeCodebases = [ + ...new Set([ + ...helper.getCodebasesFromConfig(config), + ...backend + .allEndpoints(existingBackend) + .map((ep) => ep.codebase || projectConfig.DEFAULT_CODEBASE), + ]), + ]; + + const parsedFilters = filters.flatMap((f) => { + const parsed = helper.parseFunctionSelector(f, config); + return parsed.map((filter) => + !f.includes(":") && filter.codebase === projectConfig.DEFAULT_CODEBASE && filter.idChunks + ? { idChunks: filter.idChunks } + : filter, + ); + }); + + context.filters = parsedFilters; + const plan = await planner.createDeploymentPlan({ wantBackend: backend.empty(), haveBackend: existingBackend, @@ -78,27 +99,28 @@ export const command = new Command("functions:delete [filters...]") ); } - // Inform the user when a name collision exists between a codebase name and a function name in default. - // Codebase deletion takes precedence by design, but we provide the explicit 'default:' workaround. - const codebaseNames = helper.getCodebasesFromConfig(config); - const defaultEndpoints = backend - .allEndpoints(existingBackend) - .filter((ep) => !ep.codebase || ep.codebase === projectConfig.DEFAULT_CODEBASE); - + // Inform the user when a name collision exists between a codebase name and a function name. + // Codebase deletion takes precedence by design, but we provide the explicit ':' workaround. + const allEndpoints = backend.allEndpoints(existingBackend); for (const f of filters) { - if (!f.includes(":") && codebaseNames.includes(f)) { - const hasDefaultCollision = defaultEndpoints.some( - (ep) => ep.id === f || ep.id.startsWith(`${f}-`), + if (f.includes(":") || !activeCodebases.includes(f)) { + continue; + } + const matchingEndpoints = allEndpoints.filter( + (ep) => ep.id === f || ep.id.startsWith(`${f}-`), + ); + if (matchingEndpoints.length > 0) { + const ep = matchingEndpoints[0]; + const prefix = ep.codebase || projectConfig.DEFAULT_CODEBASE; + utils.logLabeledBullet( + "functions", + `Target '${clc.bold(f)}' matches both a codebase and a function (${helper.getFunctionLabel( + ep, + )}). Codebase deletion takes precedence. ` + + `(To delete the function instead, run: ${clc.bold( + `firebase functions:delete ${prefix}:${f}`, + )})`, ); - if (hasDefaultCollision) { - utils.logLabeledBullet( - "functions", - `Target '${clc.bold(f)}' matches both a codebase and a function. Codebase deletion takes precedence. ` + - `(To delete the function in the default codebase instead, run: ${clc.bold( - `firebase functions:delete default:${f}`, - )})`, - ); - } } } diff --git a/src/deploy/functions/functionsDeployHelper.spec.ts b/src/deploy/functions/functionsDeployHelper.spec.ts index 5ca99a16126..4b133b4cb64 100644 --- a/src/deploy/functions/functionsDeployHelper.spec.ts +++ b/src/deploy/functions/functionsDeployHelper.spec.ts @@ -227,7 +227,6 @@ describe("functionsDeployHelper", () => { desc: string; selector: string; config: ValidatedConfig; - defaultCodebase?: string; expected: EndpointFilter[]; } @@ -236,7 +235,6 @@ describe("functionsDeployHelper", () => { desc: "parses selector without codebase (not a codebase name)", selector: "func", config: [{ source: "functions", codebase: DEFAULT_CODEBASE }] as ValidatedConfig, - defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: DEFAULT_CODEBASE, @@ -251,7 +249,6 @@ describe("functionsDeployHelper", () => { { source: "functions", codebase: DEFAULT_CODEBASE }, { source: "other", codebase: "func" }, ] as ValidatedConfig, - defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: "func", @@ -262,7 +259,6 @@ describe("functionsDeployHelper", () => { desc: "parses group selector (with '.') without codebase", selector: "g1.func", config: [{ source: "functions", codebase: DEFAULT_CODEBASE }] as ValidatedConfig, - defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: DEFAULT_CODEBASE, @@ -274,7 +270,6 @@ describe("functionsDeployHelper", () => { desc: "parses group selector (with '-') without codebase", selector: "g1-func", config: [{ source: "functions", codebase: DEFAULT_CODEBASE }] as ValidatedConfig, - defaultCodebase: DEFAULT_CODEBASE, expected: [ { codebase: DEFAULT_CODEBASE, @@ -307,25 +302,11 @@ describe("functionsDeployHelper", () => { }, ], }, - { - desc: "parses bare selector without default codebase", - selector: "foo", - config: [ - { source: "functions", codebase: "codebaseA" }, - { source: "other", codebase: "codebaseB" }, - ] as ValidatedConfig, - defaultCodebase: undefined, - expected: [ - { - idChunks: ["foo"], - }, - ], - }, ]; for (const tc of testcases) { it(tc.desc, () => { - const actual = parseFunctionSelector(tc.selector, tc.config, tc.defaultCodebase); + const actual = parseFunctionSelector(tc.selector, tc.config); expect(actual.length).to.equal(tc.expected.length); expect(actual).to.deep.include.members(tc.expected); diff --git a/src/deploy/functions/functionsDeployHelper.ts b/src/deploy/functions/functionsDeployHelper.ts index ccd8aa43c04..4d10cc8dfb8 100644 --- a/src/deploy/functions/functionsDeployHelper.ts +++ b/src/deploy/functions/functionsDeployHelper.ts @@ -70,7 +70,6 @@ export function getCodebasesFromConfig(config: ValidatedSingle[] = []): string[] export function parseFunctionSelector( selector: string, config: ValidatedSingle[] = [], - defaultCodebase?: string, ): EndpointFilter[] { const fragments = selector.split(":"); const target = fragments[0]; @@ -88,14 +87,8 @@ export function parseFunctionSelector( } if (fragments.length < 2) { - // If not a known codebase name and no codebase prefix provided, - // apply defaultCodebase if specified (e.g. for deploy --only). - return [ - { - ...(defaultCodebase ? { codebase: defaultCodebase } : {}), - idChunks: fragments[0].split(/[-.]/), - }, - ]; + // It's not a codebase or kit instance name, assume it is a function id in default codebase + return [{ codebase: DEFAULT_CODEBASE, idChunks: fragments[0].split(/[-.]/) }]; } return [ { @@ -142,7 +135,7 @@ export function getEndpointFilters( if (selector.startsWith("functions:")) { selector = selector.replace("functions:", ""); if (selector.length > 0) { - filters.push(...parseFunctionSelector(selector, config, DEFAULT_CODEBASE)); + filters.push(...parseFunctionSelector(selector, config)); } } } From c6f7a2119861b0d98c75d6f82862663dad7da539 Mon Sep 17 00:00:00 2001 From: Varun Shetty Date: Tue, 18 Aug 2026 00:26:48 +0000 Subject: [PATCH 6/6] checking live endpoints for delete --- src/commands/functions-delete.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/commands/functions-delete.ts b/src/commands/functions-delete.ts index 019a9b918b5..78eafb57991 100644 --- a/src/commands/functions-delete.ts +++ b/src/commands/functions-delete.ts @@ -33,12 +33,6 @@ export const command = new Command("functions:delete [filters...]") return utils.reject("Must supply at least function or group name."); } - // Normalize project configuration to discover all registered codebases. - // Gracefully fall back to an empty configuration if running outside a Firebase directory or without functions. - // Parse filters using parseFunctionSelector for 1:1 syntax parity with `firebase deploy --only functions:...`. - const config = options.config?.src?.functions - ? projectConfig.normalizeAndValidate(options.config.src.functions) - : []; const context: args.Context = { projectId: needProjectId(options), filters: [], @@ -58,18 +52,19 @@ export const command = new Command("functions:delete [filters...]") ); } - // Discover all codebases defined in configuration OR active in existing prod backend. + // Discover all active codebases directly from live endpoints in prod backend. + // If a codebase is not live in prod, there is nothing to delete. const activeCodebases = [ - ...new Set([ - ...helper.getCodebasesFromConfig(config), - ...backend + ...new Set( + backend .allEndpoints(existingBackend) .map((ep) => ep.codebase || projectConfig.DEFAULT_CODEBASE), - ]), + ), ]; + const liveCodebasesConfig = activeCodebases.map((codebase) => ({ source: "", codebase })); const parsedFilters = filters.flatMap((f) => { - const parsed = helper.parseFunctionSelector(f, config); + const parsed = helper.parseFunctionSelector(f, liveCodebasesConfig); return parsed.map((filter) => !f.includes(":") && filter.codebase === projectConfig.DEFAULT_CODEBASE && filter.idChunks ? { idChunks: filter.idChunks }