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
99 changes: 77 additions & 22 deletions internal/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,43 @@ func resolveServerName(input string, servers []sdk.Server) (string, []sdk.Server
return "", servers
}

// resolveGroupName matches a user-provided name to a server-group identifier.
// Mirrors resolveServerName's exact / normalized / contains tiers. Returns the
// identifier on a unique match, or "" when ambiguous or no match. The group's
// display name is also returned for user-facing status messages.
func resolveGroupName(input string, groups []sdk.ServerGroup) (identifier, name string) {
normalized := normalize(input)

for _, g := range groups {
if strings.EqualFold(g.Name, input) {
return g.Identifier, g.Name
}
}

var normalizedMatches []sdk.ServerGroup
for _, g := range groups {
if normalize(g.Name) == normalized {
normalizedMatches = append(normalizedMatches, g)
}
}
if len(normalizedMatches) == 1 {
return normalizedMatches[0].Identifier, normalizedMatches[0].Name
}

lower := strings.ToLower(input)
var containsMatches []sdk.ServerGroup
for _, g := range groups {
if strings.Contains(strings.ToLower(g.Name), lower) {
containsMatches = append(containsMatches, g)
}
}
if len(containsMatches) == 1 {
return containsMatches[0].Identifier, containsMatches[0].Name
}

return "", ""
}

// normalize lowercases and collapses all non-alphanumeric chars.
func normalize(s string) string {
var b strings.Builder
Expand Down Expand Up @@ -284,6 +321,7 @@ func newDeployCmd() *cobra.Command {
servers, err := client.ListServers(cliCtx.Background(), projectID, nil)
if err == nil {
resolved, candidates := resolveServerName(server, servers)
matched := false
if resolved != "" {
server = resolved
for i := range servers {
Expand All @@ -292,29 +330,46 @@ func newDeployCmd() *cobra.Command {
break
}
}
} else if len(candidates) > 0 && !env.NonInteractive {
items := make([]string, len(candidates))
for i, s := range candidates {
items[i] = fmt.Sprintf("%s (%s)", s.Name, s.ProtocolType)
}
prompt := promptui.Select{
Label: fmt.Sprintf("Multiple servers match %q", server),
Items: items,
}
idx, _, err := prompt.Run()
if err != nil {
return &output.UserError{Message: "Server selection cancelled"}
}
server = candidates[idx].Identifier
resolvedServer = &candidates[idx]
} else if len(candidates) > 0 {
names := make([]string, len(candidates))
for i, s := range candidates {
names[i] = fmt.Sprintf("%s (%s)", s.Identifier, s.Name)
matched = true
}
// Server groups are deployable targets too. Fall back to group-name
// resolution before the per-server picker so `-s "My Group"` works
// (documented at deployhq.com/support/cli/cli-deploying).
if !matched {
if groups, gerr := client.ListServerGroups(cliCtx.Background(), projectID, nil); gerr == nil {
if groupID, groupName := resolveGroupName(server, groups); groupID != "" {
server = groupID
resolvedServer = nil // groups don't map to a single Server
env.Status("Resolved to server group: %s", groupName)
matched = true
}
}
return &output.UserError{
Message: fmt.Sprintf("Multiple servers match %q — specify which one", server),
Hint: fmt.Sprintf("Use the full identifier. Matches: %s", strings.Join(names, ", ")),
}
if !matched {
if len(candidates) > 0 && !env.NonInteractive {
items := make([]string, len(candidates))
for i, s := range candidates {
items[i] = fmt.Sprintf("%s (%s)", s.Name, s.ProtocolType)
}
prompt := promptui.Select{
Label: fmt.Sprintf("Multiple servers match %q", server),
Items: items,
}
idx, _, err := prompt.Run()
if err != nil {
return &output.UserError{Message: "Server selection cancelled"}
}
server = candidates[idx].Identifier
resolvedServer = &candidates[idx]
} else if len(candidates) > 0 {
names := make([]string, len(candidates))
for i, s := range candidates {
names[i] = fmt.Sprintf("%s (%s)", s.Identifier, s.Name)
}
return &output.UserError{
Message: fmt.Sprintf("Multiple servers match %q — specify which one", server),
Hint: fmt.Sprintf("Use the full identifier. Matches: %s", strings.Join(names, ", ")),
}
}
}
}
Expand Down
54 changes: 54 additions & 0 deletions internal/commands/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,60 @@ func TestDeployStartRevisionFlagsRegistered(t *testing.T) {
assert.NotNil(t, deployCmd.Flags().Lookup("full"))
}

func TestResolveGroupName_ExactMatch(t *testing.T) {
// DHQ-586: passing the group's display name to -s must resolve to its identifier.
groups := []sdk.ServerGroup{
{Identifier: "grp-prod", Name: "Production"},
{Identifier: "grp-stag", Name: "Staging"},
}
id, name := resolveGroupName("Production", groups)
assert.Equal(t, "grp-prod", id)
assert.Equal(t, "Production", name)
}

func TestResolveGroupName_CaseInsensitive(t *testing.T) {
groups := []sdk.ServerGroup{{Identifier: "grp-prod", Name: "Production"}}
id, _ := resolveGroupName("production", groups)
assert.Equal(t, "grp-prod", id)
}

func TestResolveGroupName_NormalizedMatch(t *testing.T) {
// "us-prod" should match "US Prod" via the normalize tier.
groups := []sdk.ServerGroup{
{Identifier: "grp-us", Name: "US Prod"},
{Identifier: "grp-eu", Name: "EU Prod"},
}
id, name := resolveGroupName("us-prod", groups)
assert.Equal(t, "grp-us", id)
assert.Equal(t, "US Prod", name)
}

func TestResolveGroupName_ContainsMatch(t *testing.T) {
groups := []sdk.ServerGroup{
{Identifier: "grp-prod", Name: "Production Cluster"},
{Identifier: "grp-stag", Name: "Staging Cluster"},
}
id, _ := resolveGroupName("Production", groups)
assert.Equal(t, "grp-prod", id)
}

func TestResolveGroupName_AmbiguousReturnsEmpty(t *testing.T) {
// Multiple contains matches → don't auto-pick. Caller falls back to the
// existing server picker / "specify which one" error.
groups := []sdk.ServerGroup{
{Identifier: "grp-1", Name: "Production US"},
{Identifier: "grp-2", Name: "Production EU"},
}
id, _ := resolveGroupName("Production", groups)
assert.Equal(t, "", id)
}

func TestResolveGroupName_NoMatch(t *testing.T) {
groups := []sdk.ServerGroup{{Identifier: "grp-prod", Name: "Production"}}
id, _ := resolveGroupName("Nonexistent", groups)
assert.Equal(t, "", id)
}

func TestResolveBranchAndRevision_PreferredBranchEmptyFallsToBranchField(t *testing.T) {
// Some servers populate Branch but not PreferredBranch. Treat both as the
// same source of truth.
Expand Down
Loading