diff --git a/.changeset/rare-peaches-change.md b/.changeset/rare-peaches-change.md new file mode 100644 index 000000000000..886942919f49 --- /dev/null +++ b/.changeset/rare-peaches-change.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Enforce 64-character limit for Workflow binding names locally to match production validation diff --git a/packages/wrangler/src/__tests__/workflows.test.ts b/packages/wrangler/src/__tests__/workflows.test.ts index 90434e47e7d5..7d28fcf25e7e 100644 --- a/packages/wrangler/src/__tests__/workflows.test.ts +++ b/packages/wrangler/src/__tests__/workflows.test.ts @@ -6,6 +6,7 @@ import { clearDialogs } from "./helpers/mock-dialogs"; import { msw } from "./helpers/msw"; import { runInTempDir } from "./helpers/run-in-tmp"; import { runWrangler } from "./helpers/run-wrangler"; +import { writeWorkerSource } from "./helpers/write-worker-source"; import { writeWranglerConfig } from "./helpers/write-wrangler-config"; import type { Instance, Workflow } from "../workflows/types"; @@ -433,4 +434,116 @@ describe("wrangler workflows", () => { ); }); }); + + describe("workflow binding validation", () => { + it("should validate workflow binding with valid name", async () => { + writeWorkerSource({ format: "ts" }); + writeWranglerConfig({ + main: "index.ts", + workflows: [ + { + binding: "MY_WORKFLOW", + name: "valid-workflow-name", + class_name: "MyWorkflow", + script_name: "external-script", + }, + ], + }); + + await runWrangler("deploy --dry-run"); + expect(std.err).toBe(""); + }); + + it("should reject workflow binding with name exceeding 64 characters", async () => { + const longName = "a".repeat(65); // 65 characters + writeWranglerConfig({ + workflows: [ + { + binding: "MY_WORKFLOW", + name: longName, + class_name: "MyWorkflow", + }, + ], + }); + + await expect(runWrangler("deploy --dry-run")).rejects.toThrow(); + expect(std.err).toContain("must be 64 characters or less"); + expect(std.err).toContain("but got 65 characters"); + }); + + it("should accept workflow binding with name exactly 64 characters", async () => { + const maxLengthName = "a".repeat(64); // exactly 64 characters + writeWorkerSource({ format: "ts" }); + writeWranglerConfig({ + main: "index.ts", + workflows: [ + { + binding: "MY_WORKFLOW", + name: maxLengthName, + class_name: "MyWorkflow", + script_name: "external-script", + }, + ], + }); + + await runWrangler("deploy --dry-run"); + expect(std.err).toBe(""); + }); + + it("should validate required fields for workflow binding", async () => { + writeWranglerConfig({ + workflows: [ + { + binding: "MY_WORKFLOW", + } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + ], + }); + + await expect(runWrangler("deploy --dry-run")).rejects.toThrow(); + expect(std.err).toContain('should have a string "name" field'); + expect(std.err).toContain('should have a string "class_name" field'); + }); + + it("should validate optional fields for workflow binding", async () => { + writeWorkerSource({ format: "ts" }); + writeWranglerConfig({ + main: "index.ts", + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + script_name: "external-script", + }, + ], + }); + + await runWrangler("deploy --dry-run"); + expect(std.err).toBe(""); + }); + + it("should reject workflow binding with invalid field types", async () => { + writeWranglerConfig({ + workflows: [ + { + binding: 123, // should be string + name: "my-workflow", + class_name: "MyWorkflow", + } as any, // eslint-disable-line @typescript-eslint/no-explicit-any + ], + }); + + await expect(runWrangler("deploy --dry-run")).rejects.toThrow(); + expect(std.err).toContain('should have a string "binding" field'); + }); + + it("should reject workflow binding that is not an object", async () => { + writeWranglerConfig({ + workflows: ["invalid-workflow-config"] as any, // eslint-disable-line @typescript-eslint/no-explicit-any + }); + + await expect(runWrangler("deploy --dry-run")).rejects.toThrow(); + expect(std.err).toContain('"workflows" bindings should be objects'); + }); + }); }); diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index a097ec54207e..f812ce5af4a9 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -2173,10 +2173,75 @@ const validateDurableObjectBinding: ValidatorFn = ( /** * Check that the given field is a valid "workflow" binding object. */ -const validateWorkflowBinding: ValidatorFn = (_diagnostics, _field, _value) => { - // TODO +const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"workflows" bindings should be objects, but got ${JSON.stringify(value)}` + ); + return false; + } - return true; + let isValid = true; + + if (!isRequiredProperty(value, "binding", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "binding" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + if (!isRequiredProperty(value, "name", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "name" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } else if (value.name.length > 64) { + diagnostics.errors.push( + `"${field}" binding "name" field must be 64 characters or less, but got ${value.name.length} characters.` + ); + isValid = false; + } + + if (!isRequiredProperty(value, "class_name", "string")) { + diagnostics.errors.push( + `"${field}" bindings should have a string "class_name" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + if (!isOptionalProperty(value, "script_name", "string")) { + diagnostics.errors.push( + `"${field}" bindings should, optionally, have a string "script_name" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + if (!isOptionalProperty(value, "experimental_remote", "boolean")) { + diagnostics.errors.push( + `"${field}" bindings should, optionally, have a boolean "experimental_remote" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + "binding", + "name", + "class_name", + "script_name", + "experimental_remote", + ]); + + return isValid; }; const validateCflogfwdrObject: (env: string) => ValidatorFn =