From 9b755796df3973b3550e5b88874d6d529bc5e1e9 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 21 Jul 2026 13:14:03 +0100 Subject: [PATCH 1/8] fix: add validation for names of task queue funcs --- CHANGELOG.md | 1 + src/deploy/functions/validate.spec.ts | 37 +++++++++++++++++++++++++++ src/deploy/functions/validate.ts | 22 ++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2816c4eeb..83e5de2193c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,3 @@ +- Validate task queue function names at deploy time, since Cloud Tasks queue IDs (derived from the function name) cannot contain underscores (#7365). - Add `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name` HTTP headers to `OneMcpServer` requests per the MCP 0728 standard release candidate (https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/ and https://modelcontextprotocol.io/seps/2243-http-standardization). - Fixes Storage Emulator to support JSON uploads larger than 100KB without hanging or throwing 413 error (#8355) diff --git a/src/deploy/functions/validate.spec.ts b/src/deploy/functions/validate.spec.ts index 94c93a9b445..6e888c70e85 100644 --- a/src/deploy/functions/validate.spec.ts +++ b/src/deploy/functions/validate.spec.ts @@ -114,6 +114,43 @@ describe("validate", () => { }); }); + describe("taskQueueFunctionNamesAreValid", () => { + const ENDPOINT_BASE: backend.Endpoint = { + platform: "gcfv2", + id: "id", + region: "us-east1", + project: "project", + entryPoint: "id", + runtime: "nodejs16", + httpsTrigger: {}, + }; + + it("should not throw on hyphenated task queue function names", () => { + const endpoints: backend.Endpoint[] = [ + { ...ENDPOINT_BASE, id: "my-task-function", taskQueueTrigger: {} }, + ]; + expect(() => { + validate.taskQueueFunctionNamesAreValid(endpoints); + }).to.not.throw(); + }); + + it("should throw on underscores in task queue function names", () => { + const endpoints: backend.Endpoint[] = [ + { ...ENDPOINT_BASE, id: "dummy_function", taskQueueTrigger: {} }, + ]; + expect(() => { + validate.taskQueueFunctionNamesAreValid(endpoints); + }).to.throw(FirebaseError, /dummy_function/); + }); + + it("should ignore underscores in non-task-queue function names", () => { + const endpoints: backend.Endpoint[] = [{ ...ENDPOINT_BASE, id: "dummy_function" }]; + expect(() => { + validate.taskQueueFunctionNamesAreValid(endpoints); + }).to.not.throw(); + }); + }); + describe("endpointsAreValid", () => { const ENDPOINT_BASE: backend.Endpoint = { platform: "gcfv2", diff --git a/src/deploy/functions/validate.ts b/src/deploy/functions/validate.ts index 8edb534e26f..004944cb348 100644 --- a/src/deploy/functions/validate.ts +++ b/src/deploy/functions/validate.ts @@ -91,6 +91,7 @@ export function endpointsAreValid( validateLifecycleHooks(wantBackend, existingBackend); const endpoints = backend.allEndpoints(wantBackend); functionIdsAreValid(endpoints); + taskQueueFunctionNamesAreValid(endpoints); validateTimeoutConfig(endpoints); for (const ep of endpoints) { validateScheduledTimeout(ep); @@ -329,6 +330,27 @@ export function functionIdsAreValid(functions: { id: string; platform: string }[ } } +/** + * Validate that task queue function names conform to Cloud Tasks queue ID naming rules. + * Unlike Cloud Functions, Cloud Tasks queue IDs (which we derive from the function name) + * cannot contain underscores. See + * https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues#Queue + * @throws { FirebaseError } Task queue function names must be valid Cloud Tasks queue IDs. + */ +export function taskQueueFunctionNamesAreValid(endpoints: backend.Endpoint[]): void { + const queueId = /^[a-zA-Z0-9-]{1,100}$/; + const invalidIds = endpoints + .filter(backend.isTaskQueueTriggered) + .filter((ep) => !queueId.test(ep.id)); + if (invalidIds.length !== 0) { + const msg = + `${invalidIds.map((f) => f.id).join(", ")} task queue function name(s) can only contain ` + + `letters, numbers, and hyphens (no underscores), and not exceed 100 characters in length. ` + + `This is because the function's name is used as the Cloud Tasks queue ID.`; + throw new FirebaseError(msg); + } +} + /** * Validate secret environment variables setting, if any. * A bad secret configuration can lead to a significant delay in function deploys. From eedc6f4fdbc5fce19131599054cb2454f3e2e0eb Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 13 Aug 2026 15:03:41 +0100 Subject: [PATCH 2/8] fix: allow deleting task queue functions with invalid queue ids Function names may contain underscores, but Cloud Tasks queue IDs may not, and the queue name is derived from the function id. Deleting such a function called updateQueue with an illegal name and failed with a 400, leaving no way to remove it via the CLI. Skip disableTaskQueue when the id is not a legal queue ID, since no queue by that name can exist. Also drops three already-released entries that a main merge reintroduced into the changelog. --- CHANGELOG.md | 5 +---- src/deploy/functions/release/fabricator.spec.ts | 10 ++++++++++ src/deploy/functions/release/fabricator.ts | 6 ++++++ src/deploy/functions/validate.spec.ts | 9 +++++---- src/deploy/functions/validate.ts | 8 ++++---- src/gcp/cloudtasks.spec.ts | 16 ++++++++++++++++ src/gcp/cloudtasks.ts | 9 +++++++++ 7 files changed, 51 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f23b5e79a96..5238b2d0670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1 @@ -- Validate task queue function names at deploy time, since Cloud Tasks queue IDs (derived from the function name) cannot contain underscores (#7365). -- Add `MCP-Protocol-Version`, `Mcp-Method`, and `Mcp-Name` HTTP headers to `OneMcpServer` requests per the MCP 0728 standard release candidate (https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/ and https://modelcontextprotocol.io/seps/2243-http-standardization). -- Fixes Storage Emulator to support JSON uploads larger than 100KB without hanging or throwing 413 error (#8355) -- Add `extdeprecationwarnings` experiment to display phased deprecation notices and guidance across `ext:*` CLI commands. +- Validate task queue function names at deploy time, and allow deletion of task queue functions whose name is not a legal Cloud Tasks queue ID (#7365). diff --git a/src/deploy/functions/release/fabricator.spec.ts b/src/deploy/functions/release/fabricator.spec.ts index bcb17f49f66..7b293057b92 100644 --- a/src/deploy/functions/release/fabricator.spec.ts +++ b/src/deploy/functions/release/fabricator.spec.ts @@ -58,6 +58,7 @@ describe("Fabricator", () => { scheduler.jobFromEndpoint.restore(); tasks.queueFromEndpoint.restore(); tasks.queueNameForEndpoint.restore(); + tasks.isValidQueueId.restore(); runv2.serviceFromEndpoint.restore(); gcf.createFunction.rejects(new Error("unexpected gcf.createFunction")); gcf.updateFunction.rejects(new Error("unexpected gcf.updateFunction")); @@ -1223,6 +1224,15 @@ describe("Fabricator", () => { }); }); + it("skips ids that cannot be queue ids", async () => { + const ep = endpoint({ + taskQueueTrigger: {}, + }) as backend.Endpoint & backend.TaskQueueTriggered; + ep.id = "dummy_function"; + await fab.disableTaskQueue(ep); + expect(tasks.updateQueue).to.not.have.been.called; + }); + it("wraps errors", async () => { const ep = endpoint({ taskQueueTrigger: {}, diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts index b43339dd955..4adf5b08673 100644 --- a/src/deploy/functions/release/fabricator.ts +++ b/src/deploy/functions/release/fabricator.ts @@ -1042,6 +1042,12 @@ export class Fabricator { } async disableTaskQueue(endpoint: backend.Endpoint & backend.TaskQueueTriggered): Promise { + // The queue name is derived from the function id, so a function whose id is not a legal + // queue ID cannot have a queue to disable. Older CLI versions let such functions deploy, + // and Cloud Tasks rejects the name outright, which would otherwise block their deletion. + if (!cloudtasks.isValidQueueId(endpoint.id)) { + return; + } const update = { name: cloudtasks.queueNameForEndpoint(endpoint), state: "DISABLED" as cloudtasks.State, diff --git a/src/deploy/functions/validate.spec.ts b/src/deploy/functions/validate.spec.ts index 6711eafde6f..0d355e292ff 100644 --- a/src/deploy/functions/validate.spec.ts +++ b/src/deploy/functions/validate.spec.ts @@ -115,15 +115,14 @@ describe("validate", () => { }); describe("taskQueueFunctionNamesAreValid", () => { - const ENDPOINT_BASE: backend.Endpoint = { + const ENDPOINT_BASE = { platform: "gcfv2", id: "id", region: "us-east1", project: "project", entryPoint: "id", runtime: "nodejs16", - httpsTrigger: {}, - }; + } as const; it("should not throw on hyphenated task queue function names", () => { const endpoints: backend.Endpoint[] = [ @@ -144,7 +143,9 @@ describe("validate", () => { }); it("should ignore underscores in non-task-queue function names", () => { - const endpoints: backend.Endpoint[] = [{ ...ENDPOINT_BASE, id: "dummy_function" }]; + const endpoints: backend.Endpoint[] = [ + { ...ENDPOINT_BASE, id: "dummy_function", httpsTrigger: {} }, + ]; expect(() => { validate.taskQueueFunctionNamesAreValid(endpoints); }).to.not.throw(); diff --git a/src/deploy/functions/validate.ts b/src/deploy/functions/validate.ts index 14e5523e00f..002345dd53a 100644 --- a/src/deploy/functions/validate.ts +++ b/src/deploy/functions/validate.ts @@ -3,6 +3,7 @@ import * as clc from "colorette"; import { FirebaseError } from "../../error"; import { getSecretVersion, SecretVersion } from "../../gcp/secretManager"; +import * as cloudtasks from "../../gcp/cloudtasks"; import { logger } from "../../logger"; import { EndpointFilter, endpointMatchesFilter, getFunctionLabel } from "./functionsDeployHelper"; import { serviceForEndpoint } from "./services"; @@ -338,15 +339,14 @@ export function functionIdsAreValid(functions: { id: string; platform: string }[ * @throws { FirebaseError } Task queue function names must be valid Cloud Tasks queue IDs. */ export function taskQueueFunctionNamesAreValid(endpoints: backend.Endpoint[]): void { - const queueId = /^[a-zA-Z0-9-]{1,100}$/; const invalidIds = endpoints .filter(backend.isTaskQueueTriggered) - .filter((ep) => !queueId.test(ep.id)); + .filter((ep) => !cloudtasks.isValidQueueId(ep.id)); if (invalidIds.length !== 0) { const msg = `${invalidIds.map((f) => f.id).join(", ")} task queue function name(s) can only contain ` + - `letters, numbers, and hyphens (no underscores), and not exceed 100 characters in length. ` + - `This is because the function's name is used as the Cloud Tasks queue ID.`; + `letters, numbers, and hyphens (no underscores). This is because the function's name is ` + + `used as the Cloud Tasks queue ID.`; throw new FirebaseError(msg); } } diff --git a/src/gcp/cloudtasks.spec.ts b/src/gcp/cloudtasks.spec.ts index 70c817d7d0c..de243375bd0 100644 --- a/src/gcp/cloudtasks.spec.ts +++ b/src/gcp/cloudtasks.spec.ts @@ -21,6 +21,7 @@ describe("CloudTasks", () => { beforeEach(() => { ct = sinon.stub(cloudtasks); ct.queueNameForEndpoint.restore(); + ct.isValidQueueId.restore(); ct.queueFromEndpoint.restore(); ct.triggerFromQueue.restore(); ct.setEnqueuer.restore(); @@ -31,6 +32,21 @@ describe("CloudTasks", () => { sinon.verifyAndRestore(); }); + describe("isValidQueueId", () => { + it("accepts letters, numbers and hyphens", () => { + expect(cloudtasks.isValidQueueId("my-queue-2")).to.be.true; + }); + + it("rejects underscores", () => { + expect(cloudtasks.isValidQueueId("dummy_function")).to.be.false; + }); + + it("rejects empty and over-long ids", () => { + expect(cloudtasks.isValidQueueId("")).to.be.false; + expect(cloudtasks.isValidQueueId("a".repeat(101))).to.be.false; + }); + }); + describe("queueFromEndpoint", () => { it("handles minimal endpoints", () => { expect(cloudtasks.queueFromEndpoint(ENDPOINT)).to.deep.equal({ diff --git a/src/gcp/cloudtasks.ts b/src/gcp/cloudtasks.ts index dae41d9a9aa..ffbad96cfb8 100644 --- a/src/gcp/cloudtasks.ts +++ b/src/gcp/cloudtasks.ts @@ -217,6 +217,15 @@ export async function setEnqueuer( } } +/** + * Whether a string is a legal Cloud Tasks queue ID. + * Notably narrower than a Cloud Functions function name, which also permits underscores. + * https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues#Queue + */ +export function isValidQueueId(id: string): boolean { + return /^[a-zA-Z0-9-]{1,100}$/.test(id); +} + /** The name of the Task Queue we will use for this endpoint. */ export function queueNameForEndpoint( endpoint: backend.Endpoint & backend.TaskQueueTriggered, From 3651778e9dddb989687c3841c9d1fcdcfee83393 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 13 Aug 2026 15:30:28 +0100 Subject: [PATCH 3/8] fix: make the task queue name error actionable Python function names cannot contain hyphens, so telling a Python user to use one has no remedy behind it. Name the real fix, and say that the queue was never created so the message explains why an existing deploy now fails. --- CHANGELOG.md | 2 +- src/deploy/functions/validate.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 472eb8dddb8..71137bb9e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -- Validate task queue function names at deploy time, and allow deletion of task queue functions whose name is not a legal Cloud Tasks queue ID (#7365). +- Deploying a task queue function whose name is not a legal Cloud Tasks queue ID (for example one containing an underscore) now fails validation instead of silently leaving the function without a queue, and such functions can now be deleted (#10834). - Configured OneMCP server tools to require a Firebase project by default, with options to opt-out specific tools (such as Developer Knowledge document search). - Fixed a bug where deploying functions with the `dartfunctions` experiment enabled could incorrectly prompt to delete existing GCF v2 functions. - Added `outputSchema` support for local MCP tools. diff --git a/src/deploy/functions/validate.ts b/src/deploy/functions/validate.ts index 002345dd53a..bfbbce9ee63 100644 --- a/src/deploy/functions/validate.ts +++ b/src/deploy/functions/validate.ts @@ -344,9 +344,10 @@ export function taskQueueFunctionNamesAreValid(endpoints: backend.Endpoint[]): v .filter((ep) => !cloudtasks.isValidQueueId(ep.id)); if (invalidIds.length !== 0) { const msg = - `${invalidIds.map((f) => f.id).join(", ")} task queue function name(s) can only contain ` + - `letters, numbers, and hyphens (no underscores). This is because the function's name is ` + - `used as the Cloud Tasks queue ID.`; + `Task queue function name(s) ${invalidIds.map((f) => f.id).join(", ")} cannot be used as ` + + `Cloud Tasks queue IDs, so their queues were never created. Rename each function to use ` + + `only letters, numbers, and hyphens. Python function names cannot contain hyphens, so use ` + + `a name with no separator at all.`; throw new FirebaseError(msg); } } From bad399c4c43b1235e159f54f1c7bfa1ee14500bb Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 13 Aug 2026 15:30:38 +0100 Subject: [PATCH 4/8] fix: log when a task queue disable is skipped --- src/deploy/functions/release/fabricator.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts index 89381b52b0d..2215e09759b 100644 --- a/src/deploy/functions/release/fabricator.ts +++ b/src/deploy/functions/release/fabricator.ts @@ -1102,6 +1102,10 @@ export class Fabricator { // queue ID cannot have a queue to disable. Older CLI versions let such functions deploy, // and Cloud Tasks rejects the name outright, which would otherwise block their deletion. if (!cloudtasks.isValidQueueId(endpoint.id)) { + logger.debug( + `Skipping disable of task queue for ${endpoint.id}: not a legal Cloud Tasks queue ID, ` + + `so no queue can exist.`, + ); return; } const update = { From 8d480ca10c376f9f46bca211e5dca48b11c1d664 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Thu, 13 Aug 2026 15:31:28 +0100 Subject: [PATCH 5/8] test: pin the task queue name validation and tidy fixtures Cover the call from endpointsAreValid, which nothing exercised, so removing it now fails the suite. Use the endpoint() helper's base override instead of mutating the id, and add uppercase and 100 character boundary cases. --- src/deploy/functions/release/fabricator.spec.ts | 6 ++---- src/deploy/functions/validate.spec.ts | 12 ++++++++++++ src/gcp/cloudtasks.spec.ts | 5 +++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/deploy/functions/release/fabricator.spec.ts b/src/deploy/functions/release/fabricator.spec.ts index 8a9ecbd6276..935e7ef96d9 100644 --- a/src/deploy/functions/release/fabricator.spec.ts +++ b/src/deploy/functions/release/fabricator.spec.ts @@ -1226,10 +1226,8 @@ describe("Fabricator", () => { }); it("skips ids that cannot be queue ids", async () => { - const ep = endpoint({ - taskQueueTrigger: {}, - }) as backend.Endpoint & backend.TaskQueueTriggered; - ep.id = "dummy_function"; + const ep = endpoint({ taskQueueTrigger: {} }, { id: "dummy_function" }) as backend.Endpoint & + backend.TaskQueueTriggered; await fab.disableTaskQueue(ep); expect(tasks.updateQueue).to.not.have.been.called; }); diff --git a/src/deploy/functions/validate.spec.ts b/src/deploy/functions/validate.spec.ts index 0d355e292ff..fd137816875 100644 --- a/src/deploy/functions/validate.spec.ts +++ b/src/deploy/functions/validate.spec.ts @@ -173,6 +173,18 @@ describe("validate", () => { expect(() => validate.endpointsAreValid(backend.of(ep))).to.throw(/GCF gen 1/); }); + it("rejects task queue function names that are not legal queue ids", () => { + const ep: backend.Endpoint = { + ...ENDPOINT_BASE, + id: "dummy_function", + taskQueueTrigger: {}, + }; + expect(() => validate.endpointsAreValid(backend.of(ep))).to.throw( + FirebaseError, + /dummy_function/, + ); + }); + it("Disallows concurrency for low-CPU gen 2", () => { const ep: backend.Endpoint = { ...ENDPOINT_BASE, diff --git a/src/gcp/cloudtasks.spec.ts b/src/gcp/cloudtasks.spec.ts index de243375bd0..c30cd5f21f0 100644 --- a/src/gcp/cloudtasks.spec.ts +++ b/src/gcp/cloudtasks.spec.ts @@ -41,8 +41,13 @@ describe("CloudTasks", () => { expect(cloudtasks.isValidQueueId("dummy_function")).to.be.false; }); + it("accepts uppercase", () => { + expect(cloudtasks.isValidQueueId("MyQueue")).to.be.true; + }); + it("rejects empty and over-long ids", () => { expect(cloudtasks.isValidQueueId("")).to.be.false; + expect(cloudtasks.isValidQueueId("a".repeat(100))).to.be.true; expect(cloudtasks.isValidQueueId("a".repeat(101))).to.be.false; }); }); From 11fec1d6b5a5be944c6e3d647d703d8fb2be183c Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 18 Aug 2026 10:45:10 +0100 Subject: [PATCH 6/8] Update src/gcp/cloudtasks.spec.ts Co-authored-by: Corie Watson --- src/gcp/cloudtasks.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gcp/cloudtasks.spec.ts b/src/gcp/cloudtasks.spec.ts index c30cd5f21f0..48c6fb8f155 100644 --- a/src/gcp/cloudtasks.spec.ts +++ b/src/gcp/cloudtasks.spec.ts @@ -45,7 +45,7 @@ describe("CloudTasks", () => { expect(cloudtasks.isValidQueueId("MyQueue")).to.be.true; }); - it("rejects empty and over-long ids", () => { + it("rejects ids that are empty or too long", () => { expect(cloudtasks.isValidQueueId("")).to.be.false; expect(cloudtasks.isValidQueueId("a".repeat(100))).to.be.true; expect(cloudtasks.isValidQueueId("a".repeat(101))).to.be.false; From 3a9ada4aefc0c1da007d0e5d46e970fc76cd011c Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 18 Aug 2026 10:45:58 +0100 Subject: [PATCH 7/8] Update src/deploy/functions/validate.ts Co-authored-by: Corie Watson --- src/deploy/functions/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deploy/functions/validate.ts b/src/deploy/functions/validate.ts index bfbbce9ee63..fb30a57a56c 100644 --- a/src/deploy/functions/validate.ts +++ b/src/deploy/functions/validate.ts @@ -344,7 +344,7 @@ export function taskQueueFunctionNamesAreValid(endpoints: backend.Endpoint[]): v .filter((ep) => !cloudtasks.isValidQueueId(ep.id)); if (invalidIds.length !== 0) { const msg = - `Task queue function name(s) ${invalidIds.map((f) => f.id).join(", ")} cannot be used as ` + + `Task queue function name(s) ${invalidIds.map((ep) => ep.id).join(", ")} cannot be used as ` + `Cloud Tasks queue IDs, so their queues were never created. Rename each function to use ` + `only letters, numbers, and hyphens. Python function names cannot contain hyphens, so use ` + `a name with no separator at all.`; From 0af4e02ff7823ed7432b57180a9b898080a19a23 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 18 Aug 2026 10:46:08 +0100 Subject: [PATCH 8/8] Update src/deploy/functions/validate.spec.ts Co-authored-by: Corie Watson --- src/deploy/functions/validate.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/deploy/functions/validate.spec.ts b/src/deploy/functions/validate.spec.ts index fd137816875..65cecf47feb 100644 --- a/src/deploy/functions/validate.spec.ts +++ b/src/deploy/functions/validate.spec.ts @@ -115,14 +115,14 @@ describe("validate", () => { }); describe("taskQueueFunctionNamesAreValid", () => { - const ENDPOINT_BASE = { + const ENDPOINT_BASE: Omit = { platform: "gcfv2", id: "id", region: "us-east1", project: "project", entryPoint: "id", runtime: "nodejs16", - } as const; + }; it("should not throw on hyphenated task queue function names", () => { const endpoints: backend.Endpoint[] = [