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
27 changes: 27 additions & 0 deletions internal/services/issue_generator_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,37 @@ func (s *IssueGeneratorService) GenerateFromDiff(ctx context.Context, hint strin
logger.Debug(ctx, "labels inferred",
"total_labels", len(result.Labels))
}
result.Labels = filterToAvailableLabels(result.Labels, availableLabels)

logger.Info(ctx, "issue generated from diff successfully",
"title", result.Title)

return result, nil
}

// filterToAvailableLabels keeps only labels that actually exist in the
// repo, dropping anything a template hardcodes or a heuristic guesses that
// isn't real (e.g. an issue template's own YAML "labels:" field can list a
// label that was never created on GitHub). If availableLabels is empty
// (fetch failed, or no VCS client), there's nothing reliable to validate
// against, so labels are kept as-is.
func filterToAvailableLabels(labels, availableLabels []string) []string {
if len(availableLabels) == 0 {
return labels
}
allowed := make(map[string]bool, len(availableLabels))
for _, l := range availableLabels {
allowed[strings.ToLower(l)] = true
}
filtered := make([]string, 0, len(labels))
for _, l := range labels {
if allowed[strings.ToLower(l)] {
filtered = append(filtered, l)
}
}
return filtered
}

func (s *IssueGeneratorService) fetchAvailableLabels(ctx context.Context) ([]string, error) {
if s.vcsClient == nil {
return nil, nil
Expand Down Expand Up @@ -225,6 +249,8 @@ func (s *IssueGeneratorService) GenerateFromDescription(ctx context.Context, des

if skipLabels {
result.Labels = []string{}
} else {
result.Labels = filterToAvailableLabels(result.Labels, availableLabels)
}

logger.Info(ctx, "issue generated from description successfully",
Expand Down Expand Up @@ -328,6 +354,7 @@ func (s *IssueGeneratorService) GenerateFromPR(ctx context.Context, prNumber int
logger.Debug(ctx, "labels inferred from PR",
"total_labels", len(result.Labels))
}
result.Labels = filterToAvailableLabels(result.Labels, availableLabels)

logger.Info(ctx, "issue generated from PR successfully",
"pr_number", prNumber,
Expand Down
38 changes: 38 additions & 0 deletions internal/services/issue_generator_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,44 @@ func TestIssueGeneratorService_GenerateWithTemplate(t *testing.T) {
mockAI.AssertExpectations(t)
})

t.Run("Success - drops template labels that don't exist in the repo", func(t *testing.T) {
mockAI := new(MockIssueContentGenerator)
mockTemplate := new(MockIssueTemplateService)
mockVCS := new(testutil.MockVCSClient)
service := NewIssueGeneratorService(nil, mockAI, WithIssueTemplateService(mockTemplate), WithIssueVCSClient(mockVCS), WithIssueConfig(cfg))

// The template's own YAML hardcodes "optimization", which was never
// created as a real label on the repo — only "performance" is real.
template := &models.IssueTemplate{
Name: "performance",
Title: "[PERF] ",
Labels: []string{"performance", "optimization"},
}
generated := &models.IssueGenerationResult{
Title: "Slow release preview",
Description: "The release preview command is slow",
Labels: []string{"performance", "refactor"},
}
merged := &models.IssueGenerationResult{
Title: "[PERF] Slow release preview",
Description: "The release preview command is slow\n---\n## Performance",
Labels: []string{"performance", "optimization", "refactor"},
}

mockTemplate.On("GetTemplateByName", ctx, "performance").Return(template, nil)
mockVCS.On("GetRepoLabels", ctx).Return([]string{"performance", "refactor", "release"}, nil)
mockAI.On("GenerateIssueContent", ctx, mock.Anything).Return(generated, nil)
mockTemplate.On("MergeWithGeneratedContent", template, mock.Anything).Return(merged)

result, err := service.GenerateWithTemplate(ctx, "performance", "", false, "Slow release preview", false)

assert.NoError(t, err)
assert.ElementsMatch(t, []string{"performance", "refactor"}, result.Labels)
assert.NotContains(t, result.Labels, "optimization")
mockTemplate.AssertExpectations(t)
mockAI.AssertExpectations(t)
})

t.Run("Error - Template not found", func(t *testing.T) {
mockAI := new(MockIssueContentGenerator)
mockTemplate := new(MockIssueTemplateService)
Expand Down
Loading