-
Notifications
You must be signed in to change notification settings - Fork 19
Add branchFormat support for Jira issues #937
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,21 @@ | ||
| import type { BranchNameStrategy, BranchGenerationOptions } from '../types/branch-naming.js' | ||
| import { getLogger } from '../utils/logger-context.js' | ||
|
|
||
| // ============================================ | ||
| // Shared Utilities | ||
| // ============================================ | ||
|
|
||
| /** | ||
| * Create a URL-safe slug from a title string | ||
| */ | ||
| export function slugify(title: string, maxLength = 20): string { | ||
| return title | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, '-') | ||
| .replace(/^-|-$/g, '') | ||
| .substring(0, maxLength) | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Strategy Classes | ||
| // ============================================ | ||
|
|
@@ -11,13 +26,7 @@ import { getLogger } from '../utils/logger-context.js' | |
| */ | ||
| export class SimpleBranchNameStrategy implements BranchNameStrategy { | ||
| async generate(issueNumber: string | number, title: string): Promise<string> { | ||
| // Create a simple slug from the title | ||
| const slug = title | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, '-') | ||
| .replace(/^-|-$/g, '') | ||
| .substring(0, 20) // Keep it short for the simple strategy | ||
|
|
||
| const slug = slugify(title) | ||
| return `feat/issue-${issueNumber}__${slug}` | ||
| } | ||
| } | ||
|
|
@@ -36,6 +45,32 @@ export class ClaudeBranchNameStrategy implements BranchNameStrategy { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Template-based branch naming strategy | ||
| * Uses a user-defined template with variable substitution | ||
| * | ||
| * Supported variables: | ||
| * {ticketId} - Full issue identifier (e.g., "PRINT-1234") | ||
| * {slug} - Slugified title (lowercase, hyphens, max 40 chars) | ||
| * | ||
| * Example: "{ticketId}-{slug}" → "PRINT-1234-fix-deps-bug" | ||
| */ | ||
| export class TemplateBranchNameStrategy implements BranchNameStrategy { | ||
| constructor(private template: string) {} | ||
|
|
||
| async generate(issueNumber: string | number, title: string): Promise<string> { | ||
| const slug = slugify(title, 40) | ||
| const ticketId = String(issueNumber) | ||
|
|
||
| const branchName = this.template | ||
| .replace(/\{ticketId\}/g, ticketId) | ||
| .replace(/\{slug\}/g, slug) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Warning] The template is a free-form string from |
||
|
|
||
| // Normalize: lowercase, remove trailing hyphens | ||
| return branchName.toLowerCase().replace(/-+$/g, '') | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Bug] Lowercasing the full template output folds the Jira ticket ID, so The return branchName.replace(/-+$/g, '')If we want a safety net for weird casing in user templates, a |
||
| } | ||
| } | ||
|
|
||
| // ============================================ | ||
| // Service Interface and Implementation | ||
| // ============================================ | ||
|
|
@@ -73,15 +108,23 @@ export class DefaultBranchNamingService implements BranchNamingService { | |
| } | ||
|
|
||
| async generateBranchName(options: BranchGenerationOptions): Promise<string> { | ||
| const { issueNumber, title, strategy } = options | ||
| const { issueNumber, title, strategy, branchFormat } = options | ||
|
|
||
| // Use provided strategy or fall back to default | ||
| const nameStrategy = strategy ?? this.defaultStrategy | ||
| // Priority: explicit strategy > branchFormat template > default strategy | ||
| let nameStrategy: BranchNameStrategy | ||
| if (strategy) { | ||
| nameStrategy = strategy | ||
| } else if (branchFormat) { | ||
| nameStrategy = new TemplateBranchNameStrategy(branchFormat) | ||
| } else { | ||
| nameStrategy = this.defaultStrategy | ||
| } | ||
|
|
||
| getLogger().debug('Generating branch name', { | ||
| issueNumber, | ||
| title, | ||
| strategy: nameStrategy.constructor.name, | ||
| branchFormat, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Small thing: this debug log now includes |
||
| }) | ||
|
|
||
| return nameStrategy.generate(issueNumber, title) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ export class LinearService implements IssueTracker { | |
| // IssueTracker interface implementation | ||
| readonly providerName = 'linear' | ||
| readonly supportsPullRequests = false // Linear doesn't have pull requests | ||
| readonly branchFormat?: string | undefined | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Thanks for wiring |
||
|
|
||
| private config: LinearServiceConfig | ||
| private prompter: (message: string) => Promise<boolean> | ||
|
|
@@ -44,6 +45,7 @@ export class LinearService implements IssueTracker { | |
| options?: { prompter?: (message: string) => Promise<boolean> }, | ||
| ) { | ||
| this.config = config ?? {} | ||
| this.branchFormat = this.config.branchFormat | ||
| this.prompter = options?.prompter ?? promptConfirmation | ||
|
|
||
| // Set API token from config if provided (follows mcp.ts pattern) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -616,6 +616,10 @@ export const IloomSettingsSchema = z.object({ | |
| .optional() | ||
| .default(['Done']) | ||
| .describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])'), | ||
| branchFormat: z | ||
| .string() | ||
| .optional() | ||
| .describe('Branch naming template for Jira issues. Variables: {ticketId} (e.g., "PROJ-123"), {slug} (slugified title). Example: "{ticketId}-{slug}" → "proj-123-fix-bug"'), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Warning] A user can set branchFormat: z
.string()
.min(1)
.refine(
v => v.includes('{ticketId}') || v.includes('{slug}'),
{ message: 'branchFormat must contain at least one of {ticketId} or {slug}' },
)
.optional()
.describe('...')Same change applies to the |
||
| }) | ||
| .optional(), | ||
| }) | ||
|
|
@@ -905,6 +909,10 @@ export const IloomSettingsSchemaNoDefaults = z.object({ | |
| .optional() | ||
| .default(['Done']) | ||
| .describe('Status names to exclude from issue lists (e.g., ["Done", "Closed", "Verify"])'), | ||
| branchFormat: z | ||
| .string() | ||
| .optional() | ||
| .describe('Branch naming template for Jira issues. Variables: {ticketId} (e.g., "PROJ-123"), {slug} (slugified title). Example: "{ticketId}-{slug}" → "proj-123-fix-bug"'), | ||
| }) | ||
| .optional(), | ||
| }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -669,7 +669,7 @@ Generate a git branch name for the following issue: | |
| * Check format: {prefix}/issue-{number}__{description} | ||
| * Uses case-insensitive matching for issue number (Linear uses uppercase like MARK-1) | ||
| */ | ||
| function isValidBranchName(name: string, issueNumber: string | number): boolean { | ||
| export function isValidBranchName(name: string, issueNumber: string | number): boolean { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Tiny thing: |
||
| const pattern = new RegExp(`^(feat|fix|docs|refactor|test|chore)/issue-${issueNumber}__[a-z0-9-]+$`, 'i') | ||
| return pattern.test(name) && name.length <= 50 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Warning]
substring(0, maxLength)runs afterreplace(/^-|-$/g, ''), so a truncation that lands on a hyphen can still leave a trailing-. For exampleslugify('Add-User', 4)yieldsadd-. Reordering to trim hyphens last fixes it:Worth adding a test that asserts the output never ends in
-.