Use branch name template

This commit is contained in:
Cole Davis
2025-09-12 14:55:21 -04:00
committed by Cole D
parent 4991c0c947
commit b5de2b9913
3 changed files with 270 additions and 14 deletions

View File

@@ -12,6 +12,7 @@ import type { ParsedGitHubContext } from "../context";
import type { GitHubPullRequest } from "../types"; import type { GitHubPullRequest } from "../types";
import type { Octokits } from "../api/client"; import type { Octokits } from "../api/client";
import type { FetchDataResult } from "../data/fetcher"; import type { FetchDataResult } from "../data/fetcher";
import { generateBranchName } from "../../utils/branch-template";
export type BranchInfo = { export type BranchInfo = {
baseBranch: string; baseBranch: string;
@@ -87,18 +88,8 @@ export async function setupBranch(
// Generate branch name for either an issue or closed/merged PR // Generate branch name for either an issue or closed/merged PR
const entityType = isPR ? "pr" : "issue"; const entityType = isPR ? "pr" : "issue";
// Create Kubernetes-compatible timestamp: lowercase, hyphens only, shorter format // Get the SHA of the source branch to use in template
const now = new Date(); let sourceSHA: string | undefined;
const 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")}`;
// Ensure branch name is Kubernetes-compatible:
// - Lowercase only
// - Alphanumeric with hyphens
// - No underscores
// - Max 50 chars (to allow for prefixes)
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
console.log(`Provided branch name template: ${branchNameTemplate}`);
const newBranch = branchName.toLowerCase().substring(0, 50);
try { try {
// Get the SHA of the source branch to verify it exists // Get the SHA of the source branch to verify it exists
@@ -108,8 +99,17 @@ export async function setupBranch(
ref: `heads/${sourceBranch}`, ref: `heads/${sourceBranch}`,
}); });
const currentSHA = sourceBranchRef.data.object.sha; sourceSHA = sourceBranchRef.data.object.sha;
console.log(`Source branch SHA: ${currentSHA}`); console.log(`Source branch SHA: ${sourceSHA}`);
// Generate branch name using template or default format
const newBranch = generateBranchName(
branchNameTemplate,
branchPrefix,
entityType,
entityNumber,
sourceSHA,
);
// For commit signing, defer branch creation to the file ops server // For commit signing, defer branch creation to the file ops server
if (context.inputs.useCommitSigning) { if (context.inputs.useCommitSigning) {

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bun
/**
* Branch name template parsing and variable substitution utilities
*/
export interface BranchTemplateVariables {
prefix: string;
entityType: string;
entityNumber: number;
timestamp: string;
year: string;
month: string;
day: string;
hour: string;
minute: string;
sha?: 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]) => {
if (value !== undefined) {
const placeholder = `{{${key}}}`;
result = result.replaceAll(placeholder, String(value));
}
});
return result;
}
/**
* Generates template variables from current context
*/
export function createBranchTemplateVariables(
branchPrefix: string,
entityType: string,
entityNumber: number,
sha?: string,
): BranchTemplateVariables {
const now = new Date();
return {
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")}`,
year: String(now.getFullYear()),
month: String(now.getMonth() + 1).padStart(2, "0"),
day: String(now.getDate()).padStart(2, "0"),
hour: String(now.getHours()).padStart(2, "0"),
minute: String(now.getMinutes()).padStart(2, "0"),
sha: sha?.substring(0, 8), // First 8 characters of SHA
};
}
/**
* Generates a branch name using template or falls back to default format
*/
export function generateBranchName(
template: string | undefined,
branchPrefix: string,
entityType: string,
entityNumber: number,
sha?: string,
): string {
const variables = createBranchTemplateVariables(
branchPrefix,
entityType,
entityNumber,
sha,
);
let branchName: string;
if (template && template.trim()) {
// Use custom template
branchName = applyBranchTemplate(template, variables);
} else {
// Use default format (backward compatibility)
branchName = `${branchPrefix}${entityType}-${entityNumber}-${variables.timestamp}`;
}
// Ensure branch name is Kubernetes-compatible:
// - Lowercase only
// - Alphanumeric with hyphens
// - No underscores
// - Max 50 chars (to allow for prefixes)
return branchName.toLowerCase().substring(0, 50);
}

View File

@@ -0,0 +1,157 @@
#!/usr/bin/env bun
import { describe, it, expect } from "bun:test";
import {
applyBranchTemplate,
createBranchTemplateVariables,
generateBranchName,
} from "../src/utils/branch-template";
describe("branch template utilities", () => {
describe("applyBranchTemplate", () => {
it("should replace all template variables", () => {
const template =
"{{prefix}}{{entityType}}-{{entityNumber}}-{{timestamp}}";
const variables = {
prefix: "feat/",
entityType: "issue",
entityNumber: 123,
timestamp: "20240301-1430",
year: "2024",
month: "03",
day: "01",
hour: "14",
minute: "30",
sha: "abcd1234",
};
const result = applyBranchTemplate(template, variables);
expect(result).toBe("feat/issue-123-20240301-1430");
});
it("should handle custom templates with multiple variables", () => {
const template =
"{{prefix}}fix/{{entityType}}_{{entityNumber}}_{{year}}{{month}}{{day}}_{{sha}}";
const variables = {
prefix: "claude-",
entityType: "pr",
entityNumber: 456,
timestamp: "20240301-1430",
year: "2024",
month: "03",
day: "01",
hour: "14",
minute: "30",
sha: "abcd1234",
};
const result = applyBranchTemplate(template, variables);
expect(result).toBe("claude-fix/pr_456_20240301_abcd1234");
});
it("should handle templates with missing variables gracefully", () => {
const template = "{{prefix}}{{entityType}}-{{missing}}-{{entityNumber}}";
const variables = {
prefix: "feat/",
entityType: "issue",
entityNumber: 123,
timestamp: "20240301-1430",
year: "2024",
month: "03",
day: "01",
hour: "14",
minute: "30",
};
const result = applyBranchTemplate(template, variables);
expect(result).toBe("feat/issue-{{missing}}-123");
});
});
describe("createBranchTemplateVariables", () => {
it("should create all required variables", () => {
const result = createBranchTemplateVariables(
"claude/",
"issue",
123,
"abcdef123456",
);
expect(result.prefix).toBe("claude/");
expect(result.entityType).toBe("issue");
expect(result.entityNumber).toBe(123);
expect(result.sha).toBe("abcdef12");
expect(result.timestamp).toMatch(/^\d{8}-\d{4}$/);
expect(result.year).toMatch(/^\d{4}$/);
expect(result.month).toMatch(/^\d{2}$/);
expect(result.day).toMatch(/^\d{2}$/);
expect(result.hour).toMatch(/^\d{2}$/);
expect(result.minute).toMatch(/^\d{2}$/);
});
it("should handle SHA truncation", () => {
const result = createBranchTemplateVariables(
"test/",
"pr",
456,
"abcdef123456789",
);
expect(result.sha).toBe("abcdef12");
});
it("should handle missing SHA", () => {
const result = createBranchTemplateVariables("test/", "pr", 456);
expect(result.sha).toBeUndefined();
});
});
describe("generateBranchName", () => {
it("should use custom template when provided", () => {
const template = "{{prefix}}custom-{{entityType}}_{{entityNumber}}";
const result = generateBranchName(template, "feature/", "issue", 123);
expect(result).toBe("feature/custom-issue_123");
});
it("should use default format when template is empty", () => {
const result = generateBranchName("", "claude/", "issue", 123);
expect(result).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
});
it("should use default format when template is undefined", () => {
const result = generateBranchName(undefined, "claude/", "pr", 456);
expect(result).toMatch(/^claude\/pr-456-\d{8}-\d{4}$/);
});
it("should apply Kubernetes-compatible transformations", () => {
const template = "{{prefix}}UPPERCASE_Branch-Name_{{entityNumber}}";
const result = generateBranchName(template, "Feature/", "issue", 123);
expect(result).toBe("feature/uppercase_branch-name_123");
});
it("should truncate long branch names to 50 characters", () => {
const template =
"{{prefix}}very-long-branch-name-that-exceeds-the-maximum-allowed-length-{{entityNumber}}";
const result = generateBranchName(template, "feature/", "issue", 123);
expect(result.length).toBe(50);
expect(result).toBe("feature/very-long-branch-name-that-exceeds-the-max");
});
it("should handle SHA in template", () => {
const template = "{{prefix}}{{entityType}}-{{entityNumber}}-{{sha}}";
const result = generateBranchName(
template,
"fix/",
"pr",
789,
"abcdef123456",
);
expect(result).toBe("fix/pr-789-abcdef12");
});
});
});