Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9b75579
fix: add validation for names of task queue funcs
IzaakGough Jul 21, 2026
f8a8cf7
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Jul 21, 2026
2684ed6
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Jul 23, 2026
f7b17ec
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Jul 30, 2026
eedc6f4
fix: allow deleting task queue functions with invalid queue ids
IzaakGough Aug 13, 2026
0488702
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Aug 13, 2026
3651778
fix: make the task queue name error actionable
IzaakGough Aug 13, 2026
bad399c
fix: log when a task queue disable is skipped
IzaakGough Aug 13, 2026
8d480ca
test: pin the task queue name validation and tidy fixtures
IzaakGough Aug 13, 2026
af80f6d
Merge remote-tracking branch 'origin/main' into @invertase/fix-issue-…
IzaakGough Aug 14, 2026
8b00f81
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Aug 17, 2026
11fec1d
Update src/gcp/cloudtasks.spec.ts
IzaakGough Aug 18, 2026
3a9ada4
Update src/deploy/functions/validate.ts
IzaakGough Aug 18, 2026
0af4e02
Update src/deploy/functions/validate.spec.ts
IzaakGough Aug 18, 2026
2e513ab
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Aug 20, 2026
8beec00
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Aug 24, 2026
52c5af5
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Aug 27, 2026
d184f3b
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Sep 3, 2026
d9679a5
Merge branch 'main' into @invertase/fix-issue-7365
IzaakGough Sep 10, 2026
109e25f
Merge remote-tracking branch 'origin/main' into pr10834-merge
IzaakGough Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- 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).
- [Fixed] Report the GCFv2-to-GCFv1 downgrade error during validation instead of a misleading CPU error (#5461).
8 changes: 8 additions & 0 deletions src/deploy/functions/release/fabricator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
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"));
Expand Down Expand Up @@ -452,7 +453,7 @@
it("handles topics that already exist", async () => {
pubsub.createTopic.callsFake(() => {
const err = new Error("Already exists");
(err as any).status = 409;

Check warning on line 456 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 456 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .status on an `any` value
return Promise.reject(err);
});
gcfv2.createFunction.resolves({ name: "op", done: false });
Expand Down Expand Up @@ -527,7 +528,7 @@
eventarc.createChannel.callsFake(({ name }) => {
expect(name).to.equal("channel");
const err = new Error("Already exists");
(err as any).status = 409;

Check warning on line 531 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 531 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .status on an `any` value
return Promise.reject(err);
});
gcfv2.createFunction.resolves({ name: "op", done: false });
Expand Down Expand Up @@ -593,7 +594,7 @@
eventarc.getChannel.resolves(undefined);
eventarc.createChannel.callsFake(() => {
const err = new Error("🤷‍♂️");
(err as any).status = 400;

Check warning on line 597 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 597 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe member access .status on an `any` value
return Promise.reject(err);
});

Expand Down Expand Up @@ -1318,6 +1319,13 @@
});
});

it("skips ids that cannot be queue ids", async () => {
const ep = endpoint({ taskQueueTrigger: {} }, { id: "dummy_function" }) as backend.Endpoint &
backend.TaskQueueTriggered;
await fab.disableTaskQueue(ep);
expect(tasks.updateQueue).to.not.have.been.called;
});

it("wraps errors", async () => {
const ep = endpoint({
taskQueueTrigger: {},
Expand Down Expand Up @@ -1907,7 +1915,7 @@
expect(deleteEndpoint).to.not.have.been.called;

// Resolve the create operation
resolveCreate!();

Check warning on line 1918 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Forbidden non-null assertion

await applyPlanPromise;

Expand Down Expand Up @@ -1954,7 +1962,7 @@

describe("createRunFunction", () => {
it("creates a Cloud Run service with correct configuration", async () => {
runv2.createService.resolves({ uri: "https://service", name: "service" } as any);

Check warning on line 1965 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unexpected any. Specify a different type

Check warning on line 1965 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `Service | undefined`
run.setInvokerCreate.resolves();

const ep = endpoint(
Expand Down Expand Up @@ -1995,7 +2003,7 @@
});

it("always sets callable triggers to public on creation", async () => {
runv2.createService.resolves({ uri: "https://service", name: "service" } as any);

Check warning on line 2006 in src/deploy/functions/release/fabricator.spec.ts

View workflow job for this annotation

GitHub Actions / lint (24)

Unsafe argument of type `any` assigned to a parameter of type `Service | undefined`
run.setInvokerCreate.resolves();

const ep = endpoint(
Expand Down
10 changes: 10 additions & 0 deletions src/deploy/functions/release/fabricator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,16 @@ export class Fabricator {
}

async disableTaskQueue(endpoint: backend.Endpoint & backend.TaskQueueTriggered): Promise<void> {
// 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)) {
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 = {
name: cloudtasks.queueNameForEndpoint(endpoint),
state: "DISABLED" as cloudtasks.State,
Expand Down
50 changes: 50 additions & 0 deletions src/deploy/functions/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,44 @@ describe("validate", () => {
});
});

describe("taskQueueFunctionNamesAreValid", () => {
const ENDPOINT_BASE: Omit<backend.Endpoint, "httpsTrigger"> = {
platform: "gcfv2",
id: "id",
region: "us-east1",
project: "project",
entryPoint: "id",
runtime: "nodejs16",
};

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", httpsTrigger: {} },
];
expect(() => {
validate.taskQueueFunctionNamesAreValid(endpoints);
}).to.not.throw();
});
});

describe("endpointsAreValid", () => {
const ENDPOINT_BASE: backend.Endpoint = {
platform: "gcfv2",
Expand All @@ -135,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,
Expand Down
23 changes: 23 additions & 0 deletions src/deploy/functions/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -96,6 +97,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);
Expand Down Expand Up @@ -358,6 +360,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 invalidIds = endpoints
.filter(backend.isTaskQueueTriggered)
.filter((ep) => !cloudtasks.isValidQueueId(ep.id));
if (invalidIds.length !== 0) {
const msg =
`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.`;
throw new FirebaseError(msg);
}
}

/**
* Validate secret environment variables setting, if any.
* A bad secret configuration can lead to a significant delay in function deploys.
Expand Down
21 changes: 21 additions & 0 deletions src/gcp/cloudtasks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -31,6 +32,26 @@ 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("accepts uppercase", () => {
expect(cloudtasks.isValidQueueId("MyQueue")).to.be.true;
});

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;
});
});

describe("queueFromEndpoint", () => {
it("handles minimal endpoints", () => {
expect(cloudtasks.queueFromEndpoint(ENDPOINT)).to.deep.equal({
Expand Down
9 changes: 9 additions & 0 deletions src/gcp/cloudtasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading