mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-03 09:48:31 +08:00
fix: sanitize {{label}} in branch name templates (#1492)
A scoped label like area:permissions was substituted into the branch
name verbatim, producing a ":" that validateBranchName rejects. Because
the branch setup block catches that error and calls process.exit(1), the
whole run died. {{description}} was already sanitized via extractDescription;
{{label}} was the only free-text variable that skipped it.
Add a sanitizeLabel helper (replaces invalid-char runs with a hyphen so
scoped labels stay readable) and apply it before substitution, falling back
to entityType when a label sanitizes to empty. Adds regression tests that
also assert the result passes validateBranchName.
This commit is contained in:
parent
58dc33d9ad
commit
e64308ff97
@ -28,6 +28,20 @@ function extractDescription(
|
|||||||
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
|
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a label into a git-safe branch segment. Labels are free-form and
|
||||||
|
* often scoped (e.g. "area:permissions"), so characters that are invalid in a
|
||||||
|
* branch name (":", "/", spaces, ...) are replaced with a hyphen rather than
|
||||||
|
* dropped, keeping the label readable. Returns "" if nothing usable remains.
|
||||||
|
*/
|
||||||
|
function sanitizeLabel(label: string): string {
|
||||||
|
return label
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]+/g, "-") // Replace runs of invalid chars with a hyphen
|
||||||
|
.replace(/-+/g, "-") // Collapse multiple hyphens
|
||||||
|
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
|
||||||
|
}
|
||||||
|
|
||||||
export interface BranchTemplateVariables {
|
export interface BranchTemplateVariables {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
entityType: string;
|
entityType: string;
|
||||||
@ -78,7 +92,7 @@ export function generateBranchName(
|
|||||||
entityNumber,
|
entityNumber,
|
||||||
timestamp: `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`,
|
timestamp: `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`,
|
||||||
sha: sha?.substring(0, 8), // First 8 characters of SHA
|
sha: sha?.substring(0, 8), // First 8 characters of SHA
|
||||||
label: label || entityType, // Fall back to entityType if no label
|
label: (label && sanitizeLabel(label)) || entityType, // Sanitize; fall back to entityType if empty/no label
|
||||||
description: title ? extractDescription(title) : undefined,
|
description: title ? extractDescription(title) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
applyBranchTemplate,
|
applyBranchTemplate,
|
||||||
generateBranchName,
|
generateBranchName,
|
||||||
} from "../src/utils/branch-template";
|
} from "../src/utils/branch-template";
|
||||||
|
import { validateBranchName } from "../src/github/operations/branch";
|
||||||
|
|
||||||
describe("branch template utilities", () => {
|
describe("branch template utilities", () => {
|
||||||
describe("applyBranchTemplate", () => {
|
describe("applyBranchTemplate", () => {
|
||||||
@ -144,6 +145,53 @@ describe("branch template utilities", () => {
|
|||||||
expect(result).toBe("dev/enhancement-issue_789");
|
expect(result).toBe("dev/enhancement-issue_789");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should sanitize scoped labels that contain invalid git characters", () => {
|
||||||
|
const template = "{{prefix}}{{label}}/{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"claude/",
|
||||||
|
"issue",
|
||||||
|
123,
|
||||||
|
undefined,
|
||||||
|
"area:permissions",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("claude/area-permissions/123");
|
||||||
|
// Regression: an unsanitized ":" here previously failed validateBranchName
|
||||||
|
// and crashed the run via process.exit(1).
|
||||||
|
expect(() => validateBranchName(result)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should replace spaces in labels with hyphens", () => {
|
||||||
|
const template = "{{prefix}}{{label}}-{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"fix/",
|
||||||
|
"issue",
|
||||||
|
456,
|
||||||
|
undefined,
|
||||||
|
"needs review",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("fix/needs-review-456");
|
||||||
|
expect(() => validateBranchName(result)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to entityType when a label sanitizes to empty", () => {
|
||||||
|
const template = "{{prefix}}{{label}}-{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"fix/",
|
||||||
|
"pr",
|
||||||
|
789,
|
||||||
|
undefined,
|
||||||
|
"🎉",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("fix/pr-789");
|
||||||
|
expect(() => validateBranchName(result)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("should use description in template when provided", () => {
|
it("should use description in template when provided", () => {
|
||||||
const template = "{{prefix}}{{description}}/{{entityNumber}}";
|
const template = "{{prefix}}{{description}}/{{entityNumber}}";
|
||||||
const result = generateBranchName(
|
const result = generateBranchName(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user