diff --git a/cli/azd/pkg/azdo/pipeline.go b/cli/azd/pkg/azdo/pipeline.go index 18419759d83..b31d4a2a1a6 100644 --- a/cli/azd/pkg/azdo/pipeline.go +++ b/cli/azd/pkg/azdo/pipeline.go @@ -27,29 +27,81 @@ func createBuildDefinitionVariable(value string, isSecret bool, allowOverride bo } } -// returns the default agent queue. This is used to associate a Pipeline with a default agent pool queue +// selectAgentQueue picks the agent queue to use from the provided list. +// Auto-selects if only one queue exists, prompts the user if multiple. +// Queues with nil IDs or nil/empty names are filtered out. +func selectAgentQueue( + ctx context.Context, + projectId string, + queues []taskagent.TaskAgentQueue, + console input.Console, +) (*taskagent.TaskAgentQueue, error) { + // Filter out queues with nil IDs or nil/empty names + valid := make([]taskagent.TaskAgentQueue, 0, len(queues)) + for _, q := range queues { + if q.Id != nil && q.Name != nil && *q.Name != "" { + valid = append(valid, q) + } + } + + if len(valid) == 0 { + return nil, fmt.Errorf("no agent queues available in project %s", projectId) + } + + if len(valid) == 1 { + console.Message(ctx, fmt.Sprintf("Using agent queue: %s", *valid[0].Name)) + return &valid[0], nil + } + + options := make([]string, 0, len(valid)) + for _, q := range valid { + options = append(options, *q.Name) + } + + idx, err := console.Select(ctx, input.ConsoleOptions{ + Message: "Choose an agent queue for the pipeline", + Options: options, + DefaultValue: options[0], + }) + if err != nil { + return nil, fmt.Errorf("selecting agent queue: %w", err) + } + + if idx < 0 || idx >= len(valid) { + return nil, fmt.Errorf("selecting agent queue: invalid queue index %d for %d queues", idx, len(valid)) + } + + return &valid[idx], nil +} + +// getAgentQueue returns the agent queue to associate with the pipeline. +// It queries all usable queues and auto-selects if only one is available, +// or prompts the user to choose if multiple queues exist. func getAgentQueue( ctx context.Context, projectId string, connection *azuredevops.Connection, + console input.Console, ) (*taskagent.TaskAgentQueue, error) { client, err := taskagent.NewClient(ctx, connection) if err != nil { return nil, err } - getAgentQueuesArgs := taskagent.GetAgentQueuesArgs{ - Project: &projectId, - } - queues, err := client.GetAgentQueues(ctx, getAgentQueuesArgs) + + actionFilter := taskagent.TaskAgentQueueActionFilterValues.Use + queues, err := client.GetAgentQueues(ctx, taskagent.GetAgentQueuesArgs{ + Project: &projectId, + ActionFilter: &actionFilter, + }) if err != nil { - return nil, err + return nil, fmt.Errorf("listing agent queues: %w", err) } - for _, queue := range *queues { - if *queue.Name == "Default" { - return &queue, nil - } + + if queues == nil { + return nil, fmt.Errorf("no agent queues available in project %s", projectId) } - return nil, fmt.Errorf("could not find a default agent queue in project %s", projectId) + + return selectAgentQueue(ctx, projectId, *queues, console) } // find pipeline by name @@ -128,7 +180,7 @@ func CreatePipeline( return definition, nil } - queue, err := getAgentQueue(ctx, projectId, connection) + queue, err := getAgentQueue(ctx, projectId, connection, console) if err != nil { return nil, err } diff --git a/cli/azd/pkg/azdo/pipeline_test.go b/cli/azd/pkg/azdo/pipeline_test.go new file mode 100644 index 00000000000..c212bef43af --- /dev/null +++ b/cli/azd/pkg/azdo/pipeline_test.go @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdo + +import ( + "fmt" + "strings" + "testing" + + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/taskagent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSelectAgentQueue(t *testing.T) { + t.Run("no queues returns error", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + + queue, err := selectAgentQueue(t.Context(), "project-1", nil, mockConsole) + assert.Nil(t, queue) + assert.ErrorContains(t, err, "no agent queues available in project project-1") + }) + + t.Run("empty queues returns error", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + + queue, err := selectAgentQueue(t.Context(), "project-1", []taskagent.TaskAgentQueue{}, mockConsole) + assert.Nil(t, queue) + assert.ErrorContains(t, err, "no agent queues available in project project-1") + }) + + t.Run("queues with nil names are filtered out", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + queueId := 1 + queues := []taskagent.TaskAgentQueue{ + {Id: &queueId, Name: nil}, + } + + queue, err := selectAgentQueue(t.Context(), "project-1", queues, mockConsole) + assert.Nil(t, queue) + assert.ErrorContains(t, err, "no agent queues available in project project-1") + }) + + t.Run("queues with nil id are filtered out", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + name := "Default" + queues := []taskagent.TaskAgentQueue{ + {Id: nil, Name: &name}, + } + + queue, err := selectAgentQueue(t.Context(), "project-1", queues, mockConsole) + assert.Nil(t, queue) + assert.ErrorContains(t, err, "no agent queues available in project project-1") + }) + + t.Run("nil name queues filtered leaving one valid", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + id1 := 1 + id2 := 2 + validName := "Azure Pipelines" + queues := []taskagent.TaskAgentQueue{ + {Id: &id1, Name: nil}, + {Id: &id2, Name: &validName}, + } + + queue, err := selectAgentQueue(t.Context(), "project-1", queues, mockConsole) + require.NoError(t, err) + require.NotNil(t, queue) + assert.Equal(t, "Azure Pipelines", *queue.Name) + }) + + t.Run("single queue auto-selects", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + queueName := "Azure Pipelines" + queueId := 1 + queues := []taskagent.TaskAgentQueue{ + {Id: &queueId, Name: &queueName}, + } + + queue, err := selectAgentQueue(t.Context(), "project-1", queues, mockConsole) + require.NoError(t, err) + require.NotNil(t, queue) + assert.Equal(t, "Azure Pipelines", *queue.Name) + assert.Equal(t, 1, *queue.Id) + }) + + t.Run("multiple queues prompts user", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + mockConsole.WhenSelect(func(options input.ConsoleOptions) bool { + return strings.Contains(options.Message, "agent queue") + }).Respond(1) // select second queue + + name1 := "Default" + id1 := 1 + name2 := "Azure Pipelines" + id2 := 2 + queues := []taskagent.TaskAgentQueue{ + {Id: &id1, Name: &name1}, + {Id: &id2, Name: &name2}, + } + + queue, err := selectAgentQueue(t.Context(), "project-1", queues, mockConsole) + require.NoError(t, err) + require.NotNil(t, queue) + assert.Equal(t, "Azure Pipelines", *queue.Name) + assert.Equal(t, 2, *queue.Id) + }) + + t.Run("multiple queues selects first", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + mockConsole.WhenSelect(func(options input.ConsoleOptions) bool { + return strings.Contains(options.Message, "agent queue") + }).Respond(0) // select first queue + + name1 := "Default" + id1 := 1 + name2 := "Azure Pipelines" + id2 := 2 + queues := []taskagent.TaskAgentQueue{ + {Id: &id1, Name: &name1}, + {Id: &id2, Name: &name2}, + } + + queue, err := selectAgentQueue(t.Context(), "project-1", queues, mockConsole) + require.NoError(t, err) + require.NotNil(t, queue) + assert.Equal(t, "Default", *queue.Name) + assert.Equal(t, 1, *queue.Id) + }) + + t.Run("select error is wrapped", func(t *testing.T) { + mockConsole := mockinput.NewMockConsole() + mockConsole.WhenSelect(func(options input.ConsoleOptions) bool { + return strings.Contains(options.Message, "agent queue") + }).RespondFn(func(_ input.ConsoleOptions) (any, error) { + return 0, fmt.Errorf("user cancelled") + }) + + name1 := "Default" + id1 := 1 + name2 := "Azure Pipelines" + id2 := 2 + queues := []taskagent.TaskAgentQueue{ + {Id: &id1, Name: &name1}, + {Id: &id2, Name: &name2}, + } + + queue, err := selectAgentQueue(t.Context(), "project-1", queues, mockConsole) + assert.Nil(t, queue) + assert.ErrorContains(t, err, "selecting agent queue") + assert.ErrorContains(t, err, "user cancelled") + }) +}