Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/lucky-hornets-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": minor
---

Support long branch names in generation of branch aliases in WCI.
87 changes: 74 additions & 13 deletions packages/wrangler/src/__tests__/versions/versions.upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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);
});
});
117 changes: 82 additions & 35 deletions packages/wrangler/src/versions/upload.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand All @@ -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 <alias>-<workerName>
// 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();
}
Loading