diff --git a/.changeset/lucky-hornets-listen.md b/.changeset/lucky-hornets-listen.md new file mode 100644 index 000000000000..a3ebbcad2fdc --- /dev/null +++ b/.changeset/lucky-hornets-listen.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Support long branch names in generation of branch aliases in WCI. diff --git a/packages/wrangler/src/__tests__/versions/versions.upload.test.ts b/packages/wrangler/src/__tests__/versions/versions.upload.test.ts index 0e4ad098ca00..6276b9f97da8 100644 --- a/packages/wrangler/src/__tests__/versions/versions.upload.test.ts +++ b/packages/wrangler/src/__tests__/versions/versions.upload.test.ts @@ -368,71 +368,103 @@ describe("generatePreviewAlias", () => { }); it("sanitizes branch names correctly", () => { + const scriptName = "worker"; mockExecSync .mockImplementationOnce(() => {}) // is-inside-work-tree .mockImplementationOnce(() => Buffer.from("feat/awesome-feature")); - const result = generatePreviewAlias("worker"); + const result = generatePreviewAlias(scriptName); expect(result).toBe("feat-awesome-feature"); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); - it("returns undefined for long branch names which don't fit within DNS label constraints", () => { - const longBranch = "a".repeat(70); + it("truncates and hashes long branch names that don't fit within DNS label constraints", () => { + const scriptName = "very-long-worker-name"; + const longBranch = "a".repeat(62); mockExecSync .mockImplementationOnce(() => {}) // is-inside-work-tree .mockImplementationOnce(() => Buffer.from(longBranch)); - const result = generatePreviewAlias("worker"); - expect(result).toBeUndefined(); + const result = generatePreviewAlias(scriptName); + + // Should be truncated to fit: max 63 - 21 - 1 = 41 chars + // With 4-char hash + hyphen, we have 41 - 4 - 1 = 36 chars for the prefix + expect(result).toBeDefined(); + expect(result).toMatch(/^a{36}-[a-f0-9]{4}$/); + expect(result?.length).toBe(41); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); it("handles multiple, leading, and trailing dashes", () => { + const scriptName = "testscript"; mockExecSync .mockImplementationOnce(() => {}) // is-inside-work-tree .mockImplementationOnce(() => Buffer.from("--some--branch--name--")); - const result = generatePreviewAlias("testscript"); + const result = generatePreviewAlias(scriptName); expect(result).toBe("some-branch-name"); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); it("lowercases branch names", () => { + const scriptName = "testscript"; mockExecSync .mockImplementationOnce(() => {}) // is-inside-work-tree .mockImplementationOnce(() => Buffer.from("HEAD/feature/work")); - const result = generatePreviewAlias("testscript"); + const result = generatePreviewAlias(scriptName); expect(result).toBe("head-feature-work"); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); it("Generates from workers ci branch", () => { + const scriptName = "testscript"; vi.stubEnv("WORKERS_CI_BRANCH", "some/debug-branch"); - const result = generatePreviewAlias("testscript"); + const result = generatePreviewAlias(scriptName); expect(result).toBe("some-debug-branch"); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); - it("Does not produce an alias from long workers ci branch name", () => { + it("Truncates and hashes long workers ci branch names", () => { + const scriptName = "testscript"; vi.stubEnv( "WORKERS_CI_BRANCH", "some/really-really-really-really-really-long-branch-name" ); - const result = generatePreviewAlias("testscript"); - expect(result).toBeUndefined(); + const result = generatePreviewAlias(scriptName); + expect(result).toMatch( + /^some-really-really-really-really-really-long-br-[a-f0-9]{4}$/ + ); + expect(result?.length).toBe(52); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); it("Strips leading dashes from branch name", () => { + const scriptName = "testscript"; vi.stubEnv("WORKERS_CI_BRANCH", "-some-branch-name"); - const result = generatePreviewAlias("testscript"); + const result = generatePreviewAlias(scriptName); expect(result).toBe("some-branch-name"); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); it("Removes concurrent dashes from branch name", () => { + const scriptName = "testscript"; vi.stubEnv("WORKERS_CI_BRANCH", "some----branch-----name"); - const result = generatePreviewAlias("testscript"); + const result = generatePreviewAlias(scriptName); expect(result).toBe("some-branch-name"); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); }); it("Does not produce an alias with leading numbers", () => { @@ -441,4 +473,33 @@ describe("generatePreviewAlias", () => { const result = generatePreviewAlias("testscript"); expect(result).toBeUndefined(); }); + + it("returns undefined when script name is too long to allow any alias", () => { + const scriptName = "a".repeat(60); + mockExecSync + .mockImplementationOnce(() => {}) // is-inside-work-tree + .mockImplementationOnce(() => Buffer.from("short-branch")); + + const result = generatePreviewAlias(scriptName); + expect(result).toBeUndefined(); + }); + + it("handles complex branch names with truncation", () => { + const scriptName = "myworker"; + const complexBranch = + "feat/JIRA-12345/implement-awesome-new-feature-with-detail"; + mockExecSync + .mockImplementationOnce(() => {}) // is-inside-work-tree + .mockImplementationOnce(() => Buffer.from(complexBranch)); + + const result = generatePreviewAlias(scriptName); + + expect(result).toBeDefined(); + expect(result).toMatch( + /^feat-jira-12345-implement-awesome-new-feature-wit-[a-f0-9]{4}$/ + ); + expect(result?.length).toBe(54); + expect(result).not.toBeUndefined(); + expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63); + }); }); diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index 21b29ef328ae..344110b7c5cc 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -1,5 +1,6 @@ import assert from "node:assert"; import { execSync } from "node:child_process"; +import { createHash } from "node:crypto"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { blue, gray } from "@cloudflare/cli/colors"; @@ -901,9 +902,71 @@ function formatTime(duration: number) { return `(${(duration / 1000).toFixed(2)} sec)`; } +// Constants for DNS label constraints and hash configuration +const MAX_DNS_LABEL_LENGTH = 63; +const HASH_LENGTH = 4; +const ALIAS_VALIDATION_REGEX = /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/; + +/** + * Sanitizes a branch name to create a valid DNS label alias. + * Converts to lowercase, replaces invalid chars with dashes, removes consecutive dashes. + */ +function sanitizeBranchName(branchName: string): string { + return branchName + .replace(/[^a-zA-Z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase(); +} + +/** + * Gets the current branch name from CI environment or git. + */ +function getBranchName(): string | undefined { + // Try CI environment variable first + const ciBranchName = getWorkersCIBranchName(); + if (ciBranchName) { + return ciBranchName; + } + + // Fall back to git commands + try { + execSync(`git rev-parse --is-inside-work-tree`, { stdio: "ignore" }); + return execSync(`git rev-parse --abbrev-ref HEAD`).toString().trim(); + } catch { + return undefined; + } +} + +/** + * Creates a truncated alias with hash suffix when the branch name is too long. + * Hash from original branch name to preserve uniqueness. + */ +function createTruncatedAlias( + branchName: string, + sanitizedAlias: string, + availableSpace: number +): string | undefined { + const spaceForHash = HASH_LENGTH + 1; // +1 for hyphen separator + const maxPrefixLength = availableSpace - spaceForHash; + + if (maxPrefixLength < 1) { + // Not enough space even with truncation + return undefined; + } + + const hash = createHash("sha256") + .update(branchName) + .digest("hex") + .slice(0, HASH_LENGTH); + + const truncatedPrefix = sanitizedAlias.slice(0, maxPrefixLength); + return `${truncatedPrefix}-${hash}`; +} + /** * Generates a preview alias based on the current git branch. - * Alias must be <= 63 characters, alphanumeric + dashes only. + * Alias must be <= 63 characters, alphanumeric + dashes only, and start with a letter. * Returns undefined if not in a git directory or requirements cannot be met. */ export function generatePreviewAlias(scriptName: string): string | undefined { @@ -914,47 +977,31 @@ export function generatePreviewAlias(scriptName: string): string | undefined { return undefined; }; - let branchName = getWorkersCIBranchName(); - if (!branchName) { - try { - execSync(`git rev-parse --is-inside-work-tree`, { stdio: "ignore" }); - branchName = execSync(`git rev-parse --abbrev-ref HEAD`) - .toString() - .trim(); - } catch { - return warnAndExit(); - } - } - + const branchName = getBranchName(); if (!branchName) { return warnAndExit(); } - const sanitizedAlias = branchName - .replace(/[^a-zA-Z0-9-]/g, "-") // Replace all non-alphanumeric characters - .replace(/-+/g, "-") // replace multiple dashes - .replace(/^-+|-+$/g, "") // trim dashes - .toLowerCase(); // lowercase the name - - // Ensure the alias name meets requirements: - // - only alphanumeric or hyphen characters - // - no trailing slashes - // - begins with letter - // - no longer than 63 total characters - const isValidAlias = /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test( - sanitizedAlias - ); - if (!isValidAlias) { + const sanitizedAlias = sanitizeBranchName(branchName); + + // Validate the sanitized alias meets DNS label requirements + if (!ALIAS_VALIDATION_REGEX.test(sanitizedAlias)) { return warnAndExit(); } - // Dns labels can only have a max of 63 chars. We use preview urls in the form of - - // which means our alias must be shorter than 63-scriptNameLen-1 - const maxDnsLabelLength = 63; - const available = maxDnsLabelLength - scriptName.length - 1; - if (sanitizedAlias.length > available) { - return warnAndExit(); + const availableSpace = MAX_DNS_LABEL_LENGTH - scriptName.length - 1; + + // If the sanitized alias fits within the remaining space, return it, + // otherwise otherwise try truncation with hash suffixed + if (sanitizedAlias.length <= availableSpace) { + return sanitizedAlias; } - return sanitizedAlias; + const truncatedAlias = createTruncatedAlias( + branchName, + sanitizedAlias, + availableSpace + ); + + return truncatedAlias || warnAndExit(); }