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
119 changes: 62 additions & 57 deletions internal/service/queue_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ func (q *QueueManager) EnqueueNextIssue(ctx context.Context, issues []github.Iss
// 3. 優先度順で最適なIssueを選択
targetIssue := q.selectPriorityIssue(todoIssues)

// 4. ラベル変更(soba:todo → soba:queued)
// 4. ラベル変更(soba:todo系 → soba:queued)
q.logger.Info(ctx, "Enqueueing issue", logging.Field{Key: "issue", Value: targetIssue.Number})
err := q.updateLabels(ctx, targetIssue.Number, "soba:todo", "soba:queued")
err := q.updateTodoLabelsToQueued(ctx, targetIssue)
if err != nil {
q.logger.Info(ctx, "Queue management completed",
logging.Field{Key: "result", Value: "failed"},
Expand All @@ -77,8 +77,11 @@ func (q *QueueManager) EnqueueNextIssue(ctx context.Context, issues []github.Iss
// hasActiveTask はアクティブなタスクがあるかチェック
func (q *QueueManager) hasActiveTask(issues []github.Issue) bool {
for _, issue := range issues {
if q.hasSobaLabel(issue) && !q.hasLabel(issue, "soba:todo") {
return true // soba:todo以外のsobaラベルがある
if q.hasSobaLabel(issue) &&
!q.hasLabel(issue, "soba:todo") &&
!q.hasLabel(issue, "soba:todo:high") &&
!q.hasLabel(issue, "soba:todo:low") {
return true // soba:todo系以外のsobaラベルがある
}
}
return false
Expand All @@ -88,7 +91,10 @@ func (q *QueueManager) hasActiveTask(issues []github.Issue) bool {
func (q *QueueManager) collectTodoIssues(issues []github.Issue) []github.Issue {
var todoIssues []github.Issue
for _, issue := range issues {
if q.hasLabel(issue, "soba:todo") {
// soba:todo, soba:todo:high, soba:todo:low のいずれかがあれば収集
if q.hasLabel(issue, "soba:todo") ||
q.hasLabel(issue, "soba:todo:high") ||
q.hasLabel(issue, "soba:todo:low") {
todoIssues = append(todoIssues, issue)
}
}
Expand All @@ -110,69 +116,68 @@ func (q *QueueManager) selectMinimumIssue(issues []github.Issue) *github.Issue {
return &minIssue
}

// updateLabels はラベルを更新する(削除→追加)
func (q *QueueManager) updateLabels(ctx context.Context, issueNumber int, removeLabel, addLabel string) error {
q.logger.Info(ctx, "Updating labels for queue management",
logging.Field{Key: "issue", Value: issueNumber},
logging.Field{Key: "remove", Value: removeLabel},
logging.Field{Key: "add", Value: addLabel},
// updateTodoLabelsToQueued は todo系ラベルを削除して soba:queued を追加する
func (q *QueueManager) updateTodoLabelsToQueued(ctx context.Context, issue *github.Issue) error {
q.logger.Info(ctx, "Updating todo labels to queued",
logging.Field{Key: "issue", Value: issue.Number},
)

// 古いラベルを削除
if removeLabel != "" {
if err := q.client.RemoveLabelFromIssue(ctx, q.owner, q.repo, issueNumber, removeLabel); err != nil {
// エラーメッセージを解析して、ラベルが存在しない場合は警告として扱う
errMsg := err.Error()
if strings.Contains(strings.ToLower(errMsg), "not found") ||
strings.Contains(strings.ToLower(errMsg), "404") ||
strings.Contains(strings.ToLower(errMsg), "label does not exist") {
q.logger.Warn(ctx, "Label not found on issue, skipping removal",
logging.Field{Key: "issue", Value: issueNumber},
logging.Field{Key: "label", Value: removeLabel},
)
// ラベルが存在しない場合はエラーとせず、処理を続行
// todo系のラベルを削除
todoLabels := []string{"soba:todo", "soba:todo:high", "soba:todo:low"}
for _, label := range todoLabels {
if q.hasLabel(*issue, label) {
if err := q.client.RemoveLabelFromIssue(ctx, q.owner, q.repo, issue.Number, label); err != nil {
// エラーメッセージを解析して、ラベルが存在しない場合は警告として扱う
errMsg := err.Error()
if strings.Contains(strings.ToLower(errMsg), "not found") ||
strings.Contains(strings.ToLower(errMsg), "404") ||
strings.Contains(strings.ToLower(errMsg), "label does not exist") {
q.logger.Warn(ctx, "Label not found on issue, skipping removal",
logging.Field{Key: "issue", Value: issue.Number},
logging.Field{Key: "label", Value: label},
)
// ラベルが存在しない場合はエラーとせず、処理を続行
} else {
q.logger.Error(ctx, "Failed to remove label",
logging.Field{Key: "error", Value: err.Error()},
logging.Field{Key: "issue", Value: issue.Number},
logging.Field{Key: "label", Value: label},
)
return errors.WrapInternal(err, "failed to remove label")
}
} else {
q.logger.Error(ctx, "Failed to remove label",
logging.Field{Key: "error", Value: err.Error()},
logging.Field{Key: "issue", Value: issueNumber},
logging.Field{Key: "label", Value: removeLabel},
q.logger.Info(ctx, "Successfully removed label from issue",
logging.Field{Key: "issue", Value: issue.Number},
logging.Field{Key: "label", Value: label},
)
return errors.WrapInternal(err, "failed to remove label")
}
} else {
q.logger.Info(ctx, "Successfully removed label from issue",
logging.Field{Key: "issue", Value: issueNumber},
logging.Field{Key: "label", Value: removeLabel},
)
}
}

// 新しいラベルを追加
if addLabel != "" {
if err := q.client.AddLabelToIssue(ctx, q.owner, q.repo, issueNumber, addLabel); err != nil {
// エラーメッセージを解析して、既にラベルが存在する場合は警告として扱う
errMsg := err.Error()
if strings.Contains(strings.ToLower(errMsg), "already exists") ||
strings.Contains(strings.ToLower(errMsg), "label already added") {
q.logger.Warn(ctx, "Label already exists on issue",
logging.Field{Key: "issue", Value: issueNumber},
logging.Field{Key: "label", Value: addLabel},
)
// 既にラベルが存在する場合もエラーとせず、成功として扱う
} else {
q.logger.Error(ctx, "Failed to add label",
logging.Field{Key: "error", Value: err.Error()},
logging.Field{Key: "issue", Value: issueNumber},
logging.Field{Key: "label", Value: addLabel},
)
return errors.WrapInternal(err, "failed to add label")
}
// soba:queued を追加
if err := q.client.AddLabelToIssue(ctx, q.owner, q.repo, issue.Number, "soba:queued"); err != nil {
// エラーメッセージを解析して、既にラベルが存在する場合は警告として扱う
errMsg := err.Error()
if strings.Contains(strings.ToLower(errMsg), "already exists") ||
strings.Contains(strings.ToLower(errMsg), "label already added") {
q.logger.Warn(ctx, "Label already exists on issue",
logging.Field{Key: "issue", Value: issue.Number},
logging.Field{Key: "label", Value: "soba:queued"},
)
// 既にラベルが存在する場合もエラーとせず、成功として扱う
} else {
q.logger.Info(ctx, "Successfully added label to issue",
logging.Field{Key: "issue", Value: issueNumber},
logging.Field{Key: "label", Value: addLabel},
q.logger.Error(ctx, "Failed to add label",
logging.Field{Key: "error", Value: err.Error()},
logging.Field{Key: "issue", Value: issue.Number},
logging.Field{Key: "label", Value: "soba:queued"},
)
return errors.WrapInternal(err, "failed to add label")
}
} else {
q.logger.Info(ctx, "Successfully added label to issue",
logging.Field{Key: "issue", Value: issue.Number},
logging.Field{Key: "label", Value: "soba:queued"},
)
}

return nil
Expand Down
142 changes: 142 additions & 0 deletions internal/service/queue_manager_collect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package service

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/douhashi/soba/internal/infra/github"
"github.com/douhashi/soba/pkg/logging"
)

func TestQueueManager_collectTodoIssues_WithPriority(t *testing.T) {
qm := &QueueManager{
logger: logging.NewMockLogger(),
}

tests := []struct {
name string
issues []github.Issue
expected []int // Expected issue numbers
}{
{
name: "soba:todo:highラベルを持つIssueも収集される",
issues: []github.Issue{
{Number: 1, Labels: []github.Label{{Name: "soba:todo:high"}}},
{Number: 2, Labels: []github.Label{{Name: "soba:todo"}}},
{Number: 3, Labels: []github.Label{{Name: "soba:planning"}}},
},
expected: []int{1, 2},
},
{
name: "soba:todo:lowラベルを持つIssueも収集される",
issues: []github.Issue{
{Number: 1, Labels: []github.Label{{Name: "soba:todo:low"}}},
{Number: 2, Labels: []github.Label{{Name: "soba:todo"}}},
{Number: 3, Labels: []github.Label{{Name: "bug"}}},
},
expected: []int{1, 2},
},
{
name: "soba:todo:highとsoba:todoの両方を持つIssueは1回だけ収集される",
issues: []github.Issue{
{Number: 1, Labels: []github.Label{{Name: "soba:todo:high"}, {Name: "soba:todo"}}},
{Number: 2, Labels: []github.Label{{Name: "soba:todo"}}},
},
expected: []int{1, 2},
},
{
name: "優先度ラベルのみ(soba:todoなし)でも収集される",
issues: []github.Issue{
{Number: 1, Labels: []github.Label{{Name: "soba:todo:high"}}},
{Number: 2, Labels: []github.Label{{Name: "soba:todo:low"}}},
{Number: 3, Labels: []github.Label{{Name: "soba:todo"}}},
},
expected: []int{1, 2, 3},
},
{
name: "優先度ラベル付きIssueが全て収集される",
issues: []github.Issue{
{Number: 1, Labels: []github.Label{{Name: "soba:todo:high"}}},
{Number: 2, Labels: []github.Label{{Name: "soba:todo"}}},
{Number: 3, Labels: []github.Label{{Name: "soba:todo:low"}}},
{Number: 4, Labels: []github.Label{{Name: "soba:planning"}}},
{Number: 5, Labels: []github.Label{{Name: "soba:todo:high"}, {Name: "soba:todo"}}},
},
expected: []int{1, 2, 3, 5},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := qm.collectTodoIssues(tt.issues)

// Issue番号のリストを作成
var resultNumbers []int
for _, issue := range result {
resultNumbers = append(resultNumbers, issue.Number)
}

assert.Equal(t, tt.expected, resultNumbers)
})
}
}

func TestQueueManager_hasActiveTask_WithPriority(t *testing.T) {
qm := &QueueManager{
logger: logging.NewMockLogger(),
}

tests := []struct {
name string
issues []github.Issue
expected bool
}{
{
name: "soba:todo:highのみはアクティブタスクではない",
issues: []github.Issue{
{Labels: []github.Label{{Name: "soba:todo:high"}}},
},
expected: false,
},
{
name: "soba:todo:lowのみはアクティブタスクではない",
issues: []github.Issue{
{Labels: []github.Label{{Name: "soba:todo:low"}}},
},
expected: false,
},
{
name: "soba:todo:highとsoba:planningが混在(アクティブタスクあり)",
issues: []github.Issue{
{Labels: []github.Label{{Name: "soba:todo:high"}}},
{Labels: []github.Label{{Name: "soba:planning"}}},
},
expected: true,
},
{
name: "優先度ラベルのみの場合はアクティブタスクではない",
issues: []github.Issue{
{Labels: []github.Label{{Name: "soba:todo:high"}}},
{Labels: []github.Label{{Name: "soba:todo"}}},
{Labels: []github.Label{{Name: "soba:todo:low"}}},
},
expected: false,
},
{
name: "soba:queuedがある場合はアクティブタスクあり",
issues: []github.Issue{
{Labels: []github.Label{{Name: "soba:todo:high"}}},
{Labels: []github.Label{{Name: "soba:queued"}}},
},
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := qm.hasActiveTask(tt.issues)
assert.Equal(t, tt.expected, result)
})
}
}
3 changes: 3 additions & 0 deletions internal/service/queue_manager_priority_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ func TestQueueManager_EnqueueNextIssue_WithPriority(t *testing.T) {
expectedIssueNum: 2, // 高優先度のIssue
setupMock: func(m *MockQueueGitHubClient, issueNum int) {
m.On("RemoveLabelFromIssue", mock.Anything, "owner", "repo", issueNum, "soba:todo").Return(nil)
m.On("RemoveLabelFromIssue", mock.Anything, "owner", "repo", issueNum, "soba:todo:high").Return(nil)
m.On("AddLabelToIssue", mock.Anything, "owner", "repo", issueNum, "soba:queued").Return(nil)
},
shouldEnqueue: true,
Expand All @@ -272,6 +273,7 @@ func TestQueueManager_EnqueueNextIssue_WithPriority(t *testing.T) {
expectedIssueNum: 3, // 高優先度の中で最小番号
setupMock: func(m *MockQueueGitHubClient, issueNum int) {
m.On("RemoveLabelFromIssue", mock.Anything, "owner", "repo", issueNum, "soba:todo").Return(nil)
m.On("RemoveLabelFromIssue", mock.Anything, "owner", "repo", issueNum, "soba:todo:high").Return(nil)
m.On("AddLabelToIssue", mock.Anything, "owner", "repo", issueNum, "soba:queued").Return(nil)
},
shouldEnqueue: true,
Expand All @@ -291,6 +293,7 @@ func TestQueueManager_EnqueueNextIssue_WithPriority(t *testing.T) {
expectedIssueNum: 5, // 低優先度の中で最小番号
setupMock: func(m *MockQueueGitHubClient, issueNum int) {
m.On("RemoveLabelFromIssue", mock.Anything, "owner", "repo", issueNum, "soba:todo").Return(nil)
m.On("RemoveLabelFromIssue", mock.Anything, "owner", "repo", issueNum, "soba:todo:low").Return(nil)
m.On("AddLabelToIssue", mock.Anything, "owner", "repo", issueNum, "soba:queued").Return(nil)
},
shouldEnqueue: true,
Expand Down
Loading
Loading