Skip to content

Commit 80960b9

Browse files
feat(wrangler): support long branch names in preview alias generation (#10283)
Updates generatePreviewAlias to truncate long branch names with hash suffixes when they exceed DNS label constraints, instead of returning undefined. This allows preview deployments for branches with longer descriptive names.
1 parent 1479fd0 commit 80960b9

File tree

3 files changed

+161
-48
lines changed

3 files changed

+161
-48
lines changed

.changeset/lucky-hornets-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Support long branch names in generation of branch aliases in WCI.

packages/wrangler/src/__tests__/versions/versions.upload.test.ts

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -368,71 +368,103 @@ describe("generatePreviewAlias", () => {
368368
});
369369

370370
it("sanitizes branch names correctly", () => {
371+
const scriptName = "worker";
371372
mockExecSync
372373
.mockImplementationOnce(() => {}) // is-inside-work-tree
373374
.mockImplementationOnce(() => Buffer.from("feat/awesome-feature"));
374375

375-
const result = generatePreviewAlias("worker");
376+
const result = generatePreviewAlias(scriptName);
376377
expect(result).toBe("feat-awesome-feature");
378+
expect(result).not.toBeUndefined();
379+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
377380
});
378381

379-
it("returns undefined for long branch names which don't fit within DNS label constraints", () => {
380-
const longBranch = "a".repeat(70);
382+
it("truncates and hashes long branch names that don't fit within DNS label constraints", () => {
383+
const scriptName = "very-long-worker-name";
384+
const longBranch = "a".repeat(62);
381385
mockExecSync
382386
.mockImplementationOnce(() => {}) // is-inside-work-tree
383387
.mockImplementationOnce(() => Buffer.from(longBranch));
384388

385-
const result = generatePreviewAlias("worker");
386-
expect(result).toBeUndefined();
389+
const result = generatePreviewAlias(scriptName);
390+
391+
// Should be truncated to fit: max 63 - 21 - 1 = 41 chars
392+
// With 4-char hash + hyphen, we have 41 - 4 - 1 = 36 chars for the prefix
393+
expect(result).toBeDefined();
394+
expect(result).toMatch(/^a{36}-[a-f0-9]{4}$/);
395+
expect(result?.length).toBe(41);
396+
expect(result).not.toBeUndefined();
397+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
387398
});
388399

389400
it("handles multiple, leading, and trailing dashes", () => {
401+
const scriptName = "testscript";
390402
mockExecSync
391403
.mockImplementationOnce(() => {}) // is-inside-work-tree
392404
.mockImplementationOnce(() => Buffer.from("--some--branch--name--"));
393405

394-
const result = generatePreviewAlias("testscript");
406+
const result = generatePreviewAlias(scriptName);
395407
expect(result).toBe("some-branch-name");
408+
expect(result).not.toBeUndefined();
409+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
396410
});
397411

398412
it("lowercases branch names", () => {
413+
const scriptName = "testscript";
399414
mockExecSync
400415
.mockImplementationOnce(() => {}) // is-inside-work-tree
401416
.mockImplementationOnce(() => Buffer.from("HEAD/feature/work"));
402417

403-
const result = generatePreviewAlias("testscript");
418+
const result = generatePreviewAlias(scriptName);
404419
expect(result).toBe("head-feature-work");
420+
expect(result).not.toBeUndefined();
421+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
405422
});
406423

407424
it("Generates from workers ci branch", () => {
425+
const scriptName = "testscript";
408426
vi.stubEnv("WORKERS_CI_BRANCH", "some/debug-branch");
409427

410-
const result = generatePreviewAlias("testscript");
428+
const result = generatePreviewAlias(scriptName);
411429
expect(result).toBe("some-debug-branch");
430+
expect(result).not.toBeUndefined();
431+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
412432
});
413433

414-
it("Does not produce an alias from long workers ci branch name", () => {
434+
it("Truncates and hashes long workers ci branch names", () => {
435+
const scriptName = "testscript";
415436
vi.stubEnv(
416437
"WORKERS_CI_BRANCH",
417438
"some/really-really-really-really-really-long-branch-name"
418439
);
419440

420-
const result = generatePreviewAlias("testscript");
421-
expect(result).toBeUndefined();
441+
const result = generatePreviewAlias(scriptName);
442+
expect(result).toMatch(
443+
/^some-really-really-really-really-really-long-br-[a-f0-9]{4}$/
444+
);
445+
expect(result?.length).toBe(52);
446+
expect(result).not.toBeUndefined();
447+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
422448
});
423449

424450
it("Strips leading dashes from branch name", () => {
451+
const scriptName = "testscript";
425452
vi.stubEnv("WORKERS_CI_BRANCH", "-some-branch-name");
426453

427-
const result = generatePreviewAlias("testscript");
454+
const result = generatePreviewAlias(scriptName);
428455
expect(result).toBe("some-branch-name");
456+
expect(result).not.toBeUndefined();
457+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
429458
});
430459

431460
it("Removes concurrent dashes from branch name", () => {
461+
const scriptName = "testscript";
432462
vi.stubEnv("WORKERS_CI_BRANCH", "some----branch-----name");
433463

434-
const result = generatePreviewAlias("testscript");
464+
const result = generatePreviewAlias(scriptName);
435465
expect(result).toBe("some-branch-name");
466+
expect(result).not.toBeUndefined();
467+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
436468
});
437469

438470
it("Does not produce an alias with leading numbers", () => {
@@ -441,4 +473,33 @@ describe("generatePreviewAlias", () => {
441473
const result = generatePreviewAlias("testscript");
442474
expect(result).toBeUndefined();
443475
});
476+
477+
it("returns undefined when script name is too long to allow any alias", () => {
478+
const scriptName = "a".repeat(60);
479+
mockExecSync
480+
.mockImplementationOnce(() => {}) // is-inside-work-tree
481+
.mockImplementationOnce(() => Buffer.from("short-branch"));
482+
483+
const result = generatePreviewAlias(scriptName);
484+
expect(result).toBeUndefined();
485+
});
486+
487+
it("handles complex branch names with truncation", () => {
488+
const scriptName = "myworker";
489+
const complexBranch =
490+
"feat/JIRA-12345/implement-awesome-new-feature-with-detail";
491+
mockExecSync
492+
.mockImplementationOnce(() => {}) // is-inside-work-tree
493+
.mockImplementationOnce(() => Buffer.from(complexBranch));
494+
495+
const result = generatePreviewAlias(scriptName);
496+
497+
expect(result).toBeDefined();
498+
expect(result).toMatch(
499+
/^feat-jira-12345-implement-awesome-new-feature-wit-[a-f0-9]{4}$/
500+
);
501+
expect(result?.length).toBe(54);
502+
expect(result).not.toBeUndefined();
503+
expect((scriptName + "-" + result).length).toBeLessThanOrEqual(63);
504+
});
444505
});

packages/wrangler/src/versions/upload.ts

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "node:assert";
22
import { execSync } from "node:child_process";
3+
import { createHash } from "node:crypto";
34
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
45
import path from "node:path";
56
import { blue, gray } from "@cloudflare/cli/colors";
@@ -901,9 +902,71 @@ function formatTime(duration: number) {
901902
return `(${(duration / 1000).toFixed(2)} sec)`;
902903
}
903904

905+
// Constants for DNS label constraints and hash configuration
906+
const MAX_DNS_LABEL_LENGTH = 63;
907+
const HASH_LENGTH = 4;
908+
const ALIAS_VALIDATION_REGEX = /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
909+
910+
/**
911+
* Sanitizes a branch name to create a valid DNS label alias.
912+
* Converts to lowercase, replaces invalid chars with dashes, removes consecutive dashes.
913+
*/
914+
function sanitizeBranchName(branchName: string): string {
915+
return branchName
916+
.replace(/[^a-zA-Z0-9-]/g, "-")
917+
.replace(/-+/g, "-")
918+
.replace(/^-+|-+$/g, "")
919+
.toLowerCase();
920+
}
921+
922+
/**
923+
* Gets the current branch name from CI environment or git.
924+
*/
925+
function getBranchName(): string | undefined {
926+
// Try CI environment variable first
927+
const ciBranchName = getWorkersCIBranchName();
928+
if (ciBranchName) {
929+
return ciBranchName;
930+
}
931+
932+
// Fall back to git commands
933+
try {
934+
execSync(`git rev-parse --is-inside-work-tree`, { stdio: "ignore" });
935+
return execSync(`git rev-parse --abbrev-ref HEAD`).toString().trim();
936+
} catch {
937+
return undefined;
938+
}
939+
}
940+
941+
/**
942+
* Creates a truncated alias with hash suffix when the branch name is too long.
943+
* Hash from original branch name to preserve uniqueness.
944+
*/
945+
function createTruncatedAlias(
946+
branchName: string,
947+
sanitizedAlias: string,
948+
availableSpace: number
949+
): string | undefined {
950+
const spaceForHash = HASH_LENGTH + 1; // +1 for hyphen separator
951+
const maxPrefixLength = availableSpace - spaceForHash;
952+
953+
if (maxPrefixLength < 1) {
954+
// Not enough space even with truncation
955+
return undefined;
956+
}
957+
958+
const hash = createHash("sha256")
959+
.update(branchName)
960+
.digest("hex")
961+
.slice(0, HASH_LENGTH);
962+
963+
const truncatedPrefix = sanitizedAlias.slice(0, maxPrefixLength);
964+
return `${truncatedPrefix}-${hash}`;
965+
}
966+
904967
/**
905968
* Generates a preview alias based on the current git branch.
906-
* Alias must be <= 63 characters, alphanumeric + dashes only.
969+
* Alias must be <= 63 characters, alphanumeric + dashes only, and start with a letter.
907970
* Returns undefined if not in a git directory or requirements cannot be met.
908971
*/
909972
export function generatePreviewAlias(scriptName: string): string | undefined {
@@ -914,47 +977,31 @@ export function generatePreviewAlias(scriptName: string): string | undefined {
914977
return undefined;
915978
};
916979

917-
let branchName = getWorkersCIBranchName();
918-
if (!branchName) {
919-
try {
920-
execSync(`git rev-parse --is-inside-work-tree`, { stdio: "ignore" });
921-
branchName = execSync(`git rev-parse --abbrev-ref HEAD`)
922-
.toString()
923-
.trim();
924-
} catch {
925-
return warnAndExit();
926-
}
927-
}
928-
980+
const branchName = getBranchName();
929981
if (!branchName) {
930982
return warnAndExit();
931983
}
932984

933-
const sanitizedAlias = branchName
934-
.replace(/[^a-zA-Z0-9-]/g, "-") // Replace all non-alphanumeric characters
935-
.replace(/-+/g, "-") // replace multiple dashes
936-
.replace(/^-+|-+$/g, "") // trim dashes
937-
.toLowerCase(); // lowercase the name
938-
939-
// Ensure the alias name meets requirements:
940-
// - only alphanumeric or hyphen characters
941-
// - no trailing slashes
942-
// - begins with letter
943-
// - no longer than 63 total characters
944-
const isValidAlias = /^[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(
945-
sanitizedAlias
946-
);
947-
if (!isValidAlias) {
985+
const sanitizedAlias = sanitizeBranchName(branchName);
986+
987+
// Validate the sanitized alias meets DNS label requirements
988+
if (!ALIAS_VALIDATION_REGEX.test(sanitizedAlias)) {
948989
return warnAndExit();
949990
}
950991

951-
// Dns labels can only have a max of 63 chars. We use preview urls in the form of <alias>-<workerName>
952-
// which means our alias must be shorter than 63-scriptNameLen-1
953-
const maxDnsLabelLength = 63;
954-
const available = maxDnsLabelLength - scriptName.length - 1;
955-
if (sanitizedAlias.length > available) {
956-
return warnAndExit();
992+
const availableSpace = MAX_DNS_LABEL_LENGTH - scriptName.length - 1;
993+
994+
// If the sanitized alias fits within the remaining space, return it,
995+
// otherwise otherwise try truncation with hash suffixed
996+
if (sanitizedAlias.length <= availableSpace) {
997+
return sanitizedAlias;
957998
}
958999

959-
return sanitizedAlias;
1000+
const truncatedAlias = createTruncatedAlias(
1001+
branchName,
1002+
sanitizedAlias,
1003+
availableSpace
1004+
);
1005+
1006+
return truncatedAlias || warnAndExit();
9601007
}

0 commit comments

Comments
 (0)