mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-03 09:48:31 +08:00
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.
114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
/**
|
|
* Branch name template parsing and variable substitution utilities
|
|
*/
|
|
|
|
const NUM_DESCRIPTION_WORDS = 5;
|
|
|
|
/**
|
|
* Extracts the first 5 words from a title and converts them to kebab-case
|
|
*/
|
|
function extractDescription(
|
|
title: string,
|
|
numWords: number = NUM_DESCRIPTION_WORDS,
|
|
): string {
|
|
if (!title || title.trim() === "") {
|
|
return "";
|
|
}
|
|
|
|
return title
|
|
.trim()
|
|
.split(/\s+/)
|
|
.slice(0, numWords) // Only first `numWords` words
|
|
.join("-")
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9-]/g, "") // Remove non-alphanumeric except hyphens
|
|
.replace(/-+/g, "-") // Replace multiple hyphens with single
|
|
.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 {
|
|
prefix: string;
|
|
entityType: string;
|
|
entityNumber: number;
|
|
timestamp: string;
|
|
sha?: string;
|
|
label?: string;
|
|
description?: string;
|
|
}
|
|
|
|
/**
|
|
* Replaces template variables in a branch name template
|
|
* Template format: {{variableName}}
|
|
*/
|
|
export function applyBranchTemplate(
|
|
template: string,
|
|
variables: BranchTemplateVariables,
|
|
): string {
|
|
let result = template;
|
|
|
|
// Replace each variable
|
|
Object.entries(variables).forEach(([key, value]) => {
|
|
const placeholder = `{{${key}}}`;
|
|
const replacement = value ? String(value) : "";
|
|
result = result.replaceAll(placeholder, replacement);
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generates a branch name from the provided `template` and set of `variables`. Uses a default format if the template is empty or produces an empty result.
|
|
*/
|
|
export function generateBranchName(
|
|
template: string | undefined,
|
|
branchPrefix: string,
|
|
entityType: string,
|
|
entityNumber: number,
|
|
sha?: string,
|
|
label?: string,
|
|
title?: string,
|
|
): string {
|
|
const now = new Date();
|
|
|
|
const variables: BranchTemplateVariables = {
|
|
prefix: branchPrefix,
|
|
entityType,
|
|
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")}`,
|
|
sha: sha?.substring(0, 8), // First 8 characters of SHA
|
|
label: (label && sanitizeLabel(label)) || entityType, // Sanitize; fall back to entityType if empty/no label
|
|
description: title ? extractDescription(title) : undefined,
|
|
};
|
|
|
|
if (template?.trim()) {
|
|
const branchName = applyBranchTemplate(template, variables);
|
|
|
|
// Some templates could produce empty results- validate
|
|
if (branchName.trim().length > 0) return branchName;
|
|
|
|
console.log(
|
|
`Branch template '${template}' generated empty result, falling back to default format`,
|
|
);
|
|
}
|
|
|
|
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${variables.timestamp}`;
|
|
// Kubernetes compatible: lowercase, max 50 chars, alphanumeric and hyphens only
|
|
return branchName.toLowerCase().substring(0, 50);
|
|
}
|