From 5d061224f963b4bf8495e7bc7b288d0ec95e08d5 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Mon, 19 May 2025 22:30:50 +0200 Subject: [PATCH 1/8] [AGENT-130] Delete and Roll Back deployments --- cmd/project.go | 161 ++++++++++++++++++++++++++++++++++++ internal/project/project.go | 44 ++++++++++ 2 files changed, 205 insertions(+) diff --git a/cmd/project.go b/cmd/project.go index dd1c23c8..b1d9c9ae 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -9,7 +9,9 @@ import ( "os/signal" "path/filepath" "sort" + "strings" "syscall" + "time" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/mcp" @@ -804,6 +806,81 @@ Examples: }, } +var projectRollbackDeploymentCmd = &cobra.Command{ + Use: "rollback-deployment", + Short: "Rollback a deployment for a project", + Long: `Rollback a specific deployment for a project by selecting a project and deployment. + +Examples: + agentuity project rollback-deployment +`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + logger := env.NewLogger(cmd) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) + apiUrl, _, _ := util.GetURLs(logger) + + selected := selectProject(ctx, logger, apiUrl, apikey, "Select a project to rollback a deployment") + if selected == "" { + return + } + selectedDeployment := selectDeployment(ctx, logger, apiUrl, apikey, selected, "Select a deployment to rollback") + if selectedDeployment == "" { + tui.ShowWarning("no deployment selected") + return + } + err := project.RollbackDeployment(ctx, logger, apiUrl, apikey, selected, selectedDeployment) + if err != nil { + tui.ShowError(err.Error()) + return + } + tui.ShowSuccess("Deployment rolled back successfully") + }, +} + +var projectDeleteDeploymentCmd = &cobra.Command{ + Use: "delete-deployment", + Short: "Delete a deployment for a project", + Long: `Delete a specific deployment for a project by selecting a project and deployment. + +Examples: + agentuity project delete-deployment +`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + logger := env.NewLogger(cmd) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) + apiUrl, _, _ := util.GetURLs(logger) + + selected := selectProject(ctx, logger, apiUrl, apikey, "Select a project to delete a deployment") + if selected == "" { + return + } + selectedDeployment := selectDeployment(ctx, logger, apiUrl, apikey, selected, "Select a deployment to delete") + if selectedDeployment == "" { + tui.ShowWarning("no deployment selected") + return + } + + if !tui.Ask(logger, "Are you sure you want to delete the selected deployment?", true) { + tui.ShowWarning("cancelled") + return + } + + err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selected, selectedDeployment) + if err != nil { + tui.ShowError(err.Error()) + return + } + + tui.ShowSuccess("Deployment deleted successfully") + }, +} + func getConfigTemplateDir(cmd *cobra.Command) (string, bool, error) { if cmd.Flags().Changed("templates-dir") { dir, _ := cmd.Flags().GetString("templates-dir") @@ -821,6 +898,88 @@ func getConfigTemplateDir(cmd *cobra.Command) (string, bool, error) { return dir, false, nil } +// Helper to fetch projects and prompt user to select one. Returns selected project ID or empty string. +func selectProject(ctx context.Context, logger logger.Logger, apiUrl, apikey string, prompt string) string { + var projects []project.ProjectListData + action := func() { + var err error + projects, err = project.ListProjects(ctx, logger, apiUrl, apikey) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list projects")).ShowErrorAndExit() + } + } + tui.ShowSpinner("fetching projects ...", action) + if len(projects) == 0 { + showNoProjects() + return "" + } + var options []tui.Option + for _, p := range projects { + desc := p.Description + if desc == "" { + desc = emptyProjectDescription + } + options = append(options, tui.Option{ + ID: p.ID, + Text: tui.Bold(tui.PadRight(p.Name, 20, " ")) + tui.Muted(p.ID), + }) + } + selected := tui.Select(logger, prompt, "", options) + if selected == "" { + tui.ShowWarning("no project selected") + } + return selected +} + +func selectDeployment(ctx context.Context, logger logger.Logger, apiUrl, apikey, projectId string, prompt string) string { + var deployments []project.DeploymentListData + fetchDeploymentsAction := func() { + var err error + deployments, err = project.ListDeployments(ctx, logger, apiUrl, apikey, projectId) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list deployments")).ShowErrorAndExit() + } + } + tui.ShowSpinner("fetching deployments ...", fetchDeploymentsAction) + if len(deployments) == 0 { + tui.ShowWarning("no deployments found") + return "" + } + var deploymentOptions []tui.Option + for _, d := range deployments { + date, err := time.Parse(time.RFC3339, d.CreatedAt) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to parse deployment date")).ShowErrorAndExit() + } + var msg string + if len(d.Message) > 60 { + msg = d.Message[:57] + "..." + } else { + msg = d.Message + } + tags := strings.Join(d.Tags, ", ") + if len(d.Tags) > 50 { + tags = strings.Join(d.Tags[:50], ", ") + "..." + } else { + tags = strings.Join(d.Tags, ", ") + } + + if d.Active { + deploymentOptions = append(deploymentOptions, tui.Option{ + ID: d.ID, + Text: fmt.Sprintf("%s %s, tags: [%-50s], msg: [%s]", "✅", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), + }) + } else { + deploymentOptions = append(deploymentOptions, tui.Option{ + ID: d.ID, + Text: fmt.Sprintf(" %s, tags: [%-50s], msg: [%s]", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), + }) + } + } + selectedDeployment := tui.Select(logger, "Select a deployment to rollback", "", deploymentOptions) + return selectedDeployment +} + func init() { rootCmd.AddCommand(projectCmd) rootCmd.AddCommand(projectNewCmd) @@ -828,6 +987,8 @@ func init() { projectCmd.AddCommand(projectListCmd) projectCmd.AddCommand(projectDeleteCmd) projectCmd.AddCommand(projectImportCmd) + projectCmd.AddCommand(projectRollbackDeploymentCmd) + projectCmd.AddCommand(projectDeleteDeploymentCmd) for _, cmd := range []*cobra.Command{projectNewCmd, projectImportCmd} { cmd.Flags().StringP("dir", "d", "", "The directory for the project") diff --git a/internal/project/project.go b/internal/project/project.go index 2e981dc4..4061d3ac 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -299,6 +299,50 @@ func ListProjects(ctx context.Context, logger logger.Logger, baseUrl string, tok return resp.Data, nil } +type DeploymentListData struct { + ID string `json:"id"` + Message string `json:"message"` + Tags []string `json:"tags"` + Active bool `json:"active"` + CreatedAt string `json:"createdAt"` +} + +func ListDeployments(ctx context.Context, logger logger.Logger, baseUrl string, token string, projectId string) ([]DeploymentListData, error) { + client := util.NewAPIClient(ctx, logger, baseUrl, token) + + var resp Response[[]DeploymentListData] + if err := client.Do("GET", fmt.Sprintf("/cli/project/%s/deployments", projectId), nil, &resp); err != nil { + return nil, fmt.Errorf("error listing deployments: %w", err) + } + return resp.Data, nil +} + +func DeleteDeployment(ctx context.Context, logger logger.Logger, baseUrl string, token string, projectId string, deploymentId string) error { + client := util.NewAPIClient(ctx, logger, baseUrl, token) + + var resp Response[string] + if err := client.Do("DELETE", fmt.Sprintf("/cli/project/%s/deployments/%s", projectId, deploymentId), nil, &resp); err != nil { + return fmt.Errorf("error deleting deployment: %w", err) + } + if !resp.Success { + return errors.New(resp.Message) + } + return nil +} + +func RollbackDeployment(ctx context.Context, logger logger.Logger, baseUrl string, token string, projectId string, deploymentId string) error { + client := util.NewAPIClient(ctx, logger, baseUrl, token) + + var resp Response[string] + if err := client.Do("POST", fmt.Sprintf("/cli/project/%s/deployments/%s/rollback", projectId, deploymentId), nil, &resp); err != nil { + return fmt.Errorf("error rolling back deployment: %w", err) + } + if !resp.Success { + return errors.New(resp.Message) + } + return nil +} + func DeleteProjects(ctx context.Context, logger logger.Logger, baseUrl string, token string, ids []string) ([]string, error) { client := util.NewAPIClient(ctx, logger, baseUrl, token) From 936b3bac54b8159271df9a8465f29b86f0819f30 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Mon, 19 May 2025 22:37:32 +0200 Subject: [PATCH 2/8] Make the type checker happy --- cmd/project.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/project.go b/cmd/project.go index b1d9c9ae..fcbb76d1 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -833,7 +833,7 @@ Examples: } err := project.RollbackDeployment(ctx, logger, apiUrl, apikey, selected, selectedDeployment) if err != nil { - tui.ShowError(err.Error()) + tui.ShowError("%s", err.Error()) return } tui.ShowSuccess("Deployment rolled back successfully") @@ -873,7 +873,7 @@ Examples: err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selected, selectedDeployment) if err != nil { - tui.ShowError(err.Error()) + tui.ShowError("%s", err.Error()) return } From 397820f2bd87aed4300257e9ebb5c0602f44b410 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Tue, 20 May 2025 15:38:16 +0200 Subject: [PATCH 3/8] Refactor --- cmd/cloud.go | 218 +++++++++++++++++++++++++++++++++++++++++++++++++ cmd/project.go | 77 ----------------- 2 files changed, 218 insertions(+), 77 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index a6e21d1a..30508161 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -672,6 +672,217 @@ func updateDeploymentStatusCompleted(logger logger.Logger, apiUrl, token, deploy return client.Do("PUT", fmt.Sprintf("/cli/deploy/upload/%s", deploymentId), payload, nil) } +var cloudRollbackCmd = &cobra.Command{ + Use: "rollback", + Short: "Rollback (undeploy) or delete a deployment from the cloud", + Long: `Rollback (undeploy) or delete a specific deployment for a project by selecting a project and deployment. + +Examples: + agentuity rollback + agentuity cloud rollback + agentuity rollback --tag name + agentuity rollback --delete +`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + logger := env.NewLogger(cmd) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) + apiUrl, _, _ := util.GetURLs(logger) + deleteFlag, _ := cmd.Flags().GetBool("delete") + dir, _ := cmd.Flags().GetString("dir") + + var selectedProject string + if dir != "" { + proj := project.EnsureProject(ctx, cmd) + if proj.Project == nil { + tui.ShowWarning("Project not found") + return + } + selectedProject = proj.Project.ProjectId + } else { + projectId, _ := cmd.Flags().GetString("project") + if projectId != "" { + // look up the project by id + projects, err := project.ListProjects(ctx, logger, apiUrl, apikey) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list projects")).ShowErrorAndExit() + } + for _, p := range projects { + if p.ID == projectId { + selectedProject = p.ID + break + } + } + if selectedProject == "" { + tui.ShowWarning("Project not found") + return + } + } + } + + question := "Select a project to rollback a deployment" + if deleteFlag { + question = "Select a project to delete a deployment" + } + + if selectedProject == "" { + selectedProject = cloudSelectProject(ctx, logger, apiUrl, apikey, question) + } + + if selectedProject == "" { + return + } + + // Try to get tag flag + tag, _ := cmd.Flags().GetString("tag") + var selectedDeployment string + if tag != "" { + // List deployments and match by tag + var deployments []project.DeploymentListData + action := func() { + var err error + deployments, err = project.ListDeployments(ctx, logger, apiUrl, apikey, selectedProject) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list deployments")).ShowErrorAndExit() + } + } + tui.ShowSpinner("fetching deployments ...", action) + for _, d := range deployments { + for _, t := range d.Tags { + if t == tag { + selectedDeployment = d.ID + break + } + } + if selectedDeployment != "" { + break + } + } + if selectedDeployment == "" { + tui.ShowWarning("No deployment found with tag '%s'", tag) + return + } + } else { + question = "Select a deployment to rollback" + if deleteFlag { + question = "Select a deployment to delete" + } + selectedDeployment = cloudSelectDeployment(ctx, logger, apiUrl, apikey, selectedProject, question) + if selectedDeployment == "" { + tui.ShowWarning("no deployment selected") + return + } + } + + if deleteFlag { + if !tui.Ask(logger, "Are you sure you want to "+tui.Bold("delete")+" the selected deployment?", true) { + tui.ShowWarning("cancelled") + return + } + err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) + if err != nil { + tui.ShowError("%s", err.Error()) + return + } + tui.ShowSuccess("Deployment deleted successfully") + } else { + err := project.RollbackDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) + if err != nil { + tui.ShowError("%s", err.Error()) + os.Exit(1) + return + } + tui.ShowSuccess("Deployment rolled back successfully") + } + }, +} + +// Helper to fetch projects and prompt user to select one. Returns selected project ID or empty string. +func cloudSelectProject(ctx context.Context, logger logger.Logger, apiUrl, apikey string, prompt string) string { + var projects []project.ProjectListData + action := func() { + var err error + projects, err = project.ListProjects(ctx, logger, apiUrl, apikey) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list projects")).ShowErrorAndExit() + } + } + tui.ShowSpinner("fetching projects ...", action) + if len(projects) == 0 { + fmt.Println() + tui.ShowWarning("no projects found") + tui.ShowBanner("Create a new project", tui.Text("Use the ")+tui.Command("new")+tui.Text(" command to create a new project"), false) + return "" + } + var options []tui.Option + for _, p := range projects { + desc := p.Description + if desc == "" { + desc = "No description provided." + } + options = append(options, tui.Option{ + ID: p.ID, + Text: tui.Bold(tui.PadRight(p.Name, 20, " ")) + tui.Muted(p.ID), + }) + } + selected := tui.Select(logger, prompt, "", options) + if selected == "" { + tui.ShowWarning("no project selected") + } + return selected +} + +func cloudSelectDeployment(ctx context.Context, logger logger.Logger, apiUrl, apikey, projectId string, prompt string) string { + var deployments []project.DeploymentListData + fetchDeploymentsAction := func() { + var err error + deployments, err = project.ListDeployments(ctx, logger, apiUrl, apikey, projectId) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list deployments")).ShowErrorAndExit() + } + } + tui.ShowSpinner("fetching deployments ...", fetchDeploymentsAction) + if len(deployments) == 0 { + tui.ShowWarning("no deployments found") + return "" + } + var deploymentOptions []tui.Option + for _, d := range deployments { + date, err := time.Parse(time.RFC3339, d.CreatedAt) + if err != nil { + errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to parse deployment date")).ShowErrorAndExit() + } + var msg string + if len(d.Message) > 60 { + msg = d.Message[:57] + "..." + } else { + msg = d.Message + } + tags := strings.Join(d.Tags, ", ") + if len(d.Tags) > 50 { + tags = strings.Join(d.Tags[:50], ", ") + "..." + } else { + tags = strings.Join(d.Tags, ", ") + } + + if d.Active { + deploymentOptions = append(deploymentOptions, tui.Option{ + ID: d.ID, + Text: fmt.Sprintf("%s %s, tags: [%-50s], msg: [%s]", "✅", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), + }) + } else { + deploymentOptions = append(deploymentOptions, tui.Option{ + ID: d.ID, + Text: fmt.Sprintf(" %s, tags: [%-50s], msg: [%s]", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), + }) + } + } + selectedDeployment := tui.Select(logger, prompt, "", deploymentOptions) + return selectedDeployment +} + func init() { rootCmd.AddCommand(cloudCmd) rootCmd.AddCommand(cloudDeployCmd) @@ -701,4 +912,11 @@ func init() { cloudDeployCmd.Flags().String("format", "text", "The output format to use for results which can be either 'text' or 'json'") cloudDeployCmd.Flags().String("org-id", "", "The organization to create the project in") cloudDeployCmd.Flags().String("templates-dir", "", "The directory to load the templates. Defaults to loading them from the github.com/agentuity/templates repository") + + rootCmd.AddCommand(cloudRollbackCmd) + cloudCmd.AddCommand(cloudRollbackCmd) + cloudRollbackCmd.Flags().String("tag", "", "Tag of the deployment to rollback") + cloudRollbackCmd.Flags().String("project", "", "Project to rollback a deployment") + cloudRollbackCmd.Flags().String("dir", "", "The directory to the project to rollback if project is not specified") + cloudRollbackCmd.Flags().Bool("delete", false, "Delete the deployment instead of rolling back") } diff --git a/cmd/project.go b/cmd/project.go index fcbb76d1..ffd7c3fb 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -806,81 +806,6 @@ Examples: }, } -var projectRollbackDeploymentCmd = &cobra.Command{ - Use: "rollback-deployment", - Short: "Rollback a deployment for a project", - Long: `Rollback a specific deployment for a project by selecting a project and deployment. - -Examples: - agentuity project rollback-deployment -`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - logger := env.NewLogger(cmd) - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - defer cancel() - apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) - apiUrl, _, _ := util.GetURLs(logger) - - selected := selectProject(ctx, logger, apiUrl, apikey, "Select a project to rollback a deployment") - if selected == "" { - return - } - selectedDeployment := selectDeployment(ctx, logger, apiUrl, apikey, selected, "Select a deployment to rollback") - if selectedDeployment == "" { - tui.ShowWarning("no deployment selected") - return - } - err := project.RollbackDeployment(ctx, logger, apiUrl, apikey, selected, selectedDeployment) - if err != nil { - tui.ShowError("%s", err.Error()) - return - } - tui.ShowSuccess("Deployment rolled back successfully") - }, -} - -var projectDeleteDeploymentCmd = &cobra.Command{ - Use: "delete-deployment", - Short: "Delete a deployment for a project", - Long: `Delete a specific deployment for a project by selecting a project and deployment. - -Examples: - agentuity project delete-deployment -`, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - logger := env.NewLogger(cmd) - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - defer cancel() - apikey, _ := util.EnsureLoggedIn(ctx, logger, cmd) - apiUrl, _, _ := util.GetURLs(logger) - - selected := selectProject(ctx, logger, apiUrl, apikey, "Select a project to delete a deployment") - if selected == "" { - return - } - selectedDeployment := selectDeployment(ctx, logger, apiUrl, apikey, selected, "Select a deployment to delete") - if selectedDeployment == "" { - tui.ShowWarning("no deployment selected") - return - } - - if !tui.Ask(logger, "Are you sure you want to delete the selected deployment?", true) { - tui.ShowWarning("cancelled") - return - } - - err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selected, selectedDeployment) - if err != nil { - tui.ShowError("%s", err.Error()) - return - } - - tui.ShowSuccess("Deployment deleted successfully") - }, -} - func getConfigTemplateDir(cmd *cobra.Command) (string, bool, error) { if cmd.Flags().Changed("templates-dir") { dir, _ := cmd.Flags().GetString("templates-dir") @@ -987,8 +912,6 @@ func init() { projectCmd.AddCommand(projectListCmd) projectCmd.AddCommand(projectDeleteCmd) projectCmd.AddCommand(projectImportCmd) - projectCmd.AddCommand(projectRollbackDeploymentCmd) - projectCmd.AddCommand(projectDeleteDeploymentCmd) for _, cmd := range []*cobra.Command{projectNewCmd, projectImportCmd} { cmd.Flags().StringP("dir", "d", "", "The directory for the project") From d1c072594713dcf1ac6346cf02cc2dc309a94d63 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Tue, 20 May 2025 15:59:56 +0200 Subject: [PATCH 4/8] odd --- cmd/cloud.go | 6 ++---- cmd/project.go | 51 -------------------------------------------------- 2 files changed, 2 insertions(+), 55 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 30508161..516ff954 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -861,10 +861,8 @@ func cloudSelectDeployment(ctx context.Context, logger logger.Logger, apiUrl, ap msg = d.Message } tags := strings.Join(d.Tags, ", ") - if len(d.Tags) > 50 { - tags = strings.Join(d.Tags[:50], ", ") + "..." - } else { - tags = strings.Join(d.Tags, ", ") + if len(tags) > 50 { + tags = tags[:50] + "..." } if d.Active { diff --git a/cmd/project.go b/cmd/project.go index ffd7c3fb..862986e5 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -9,9 +9,7 @@ import ( "os/signal" "path/filepath" "sort" - "strings" "syscall" - "time" "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/mcp" @@ -856,55 +854,6 @@ func selectProject(ctx context.Context, logger logger.Logger, apiUrl, apikey str return selected } -func selectDeployment(ctx context.Context, logger logger.Logger, apiUrl, apikey, projectId string, prompt string) string { - var deployments []project.DeploymentListData - fetchDeploymentsAction := func() { - var err error - deployments, err = project.ListDeployments(ctx, logger, apiUrl, apikey, projectId) - if err != nil { - errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list deployments")).ShowErrorAndExit() - } - } - tui.ShowSpinner("fetching deployments ...", fetchDeploymentsAction) - if len(deployments) == 0 { - tui.ShowWarning("no deployments found") - return "" - } - var deploymentOptions []tui.Option - for _, d := range deployments { - date, err := time.Parse(time.RFC3339, d.CreatedAt) - if err != nil { - errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to parse deployment date")).ShowErrorAndExit() - } - var msg string - if len(d.Message) > 60 { - msg = d.Message[:57] + "..." - } else { - msg = d.Message - } - tags := strings.Join(d.Tags, ", ") - if len(d.Tags) > 50 { - tags = strings.Join(d.Tags[:50], ", ") + "..." - } else { - tags = strings.Join(d.Tags, ", ") - } - - if d.Active { - deploymentOptions = append(deploymentOptions, tui.Option{ - ID: d.ID, - Text: fmt.Sprintf("%s %s, tags: [%-50s], msg: [%s]", "✅", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), - }) - } else { - deploymentOptions = append(deploymentOptions, tui.Option{ - ID: d.ID, - Text: fmt.Sprintf(" %s, tags: [%-50s], msg: [%s]", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), - }) - } - } - selectedDeployment := tui.Select(logger, "Select a deployment to rollback", "", deploymentOptions) - return selectedDeployment -} - func init() { rootCmd.AddCommand(projectCmd) rootCmd.AddCommand(projectNewCmd) From 6ddcf9f8a8a49868630f09126cd9ae6765cc8458 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Tue, 20 May 2025 16:10:24 +0200 Subject: [PATCH 5/8] couple more fixes --- cmd/cloud.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 516ff954..be09b81e 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -776,11 +776,21 @@ Examples: } } - if deleteFlag { - if !tui.Ask(logger, "Are you sure you want to "+tui.Bold("delete")+" the selected deployment?", true) { + forceFlag, _ := cmd.Flags().GetBool("force") + + if !forceFlag { + what := "rollback" + if deleteFlag { + what = "delete" + } + + if !tui.Ask(logger, "Are you sure you want to "+tui.Bold(what)+" the selected deployment?", true) { tui.ShowWarning("cancelled") return } + } + + if deleteFlag { err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) if err != nil { tui.ShowError("%s", err.Error()) @@ -916,5 +926,6 @@ func init() { cloudRollbackCmd.Flags().String("tag", "", "Tag of the deployment to rollback") cloudRollbackCmd.Flags().String("project", "", "Project to rollback a deployment") cloudRollbackCmd.Flags().String("dir", "", "The directory to the project to rollback if project is not specified") + cloudRollbackCmd.Flags().Bool("force", false, "Force the rollback or delete") cloudRollbackCmd.Flags().Bool("delete", false, "Delete the deployment instead of rolling back") } From 02db0d5fed57e254bfa75e59a8ad795146875fd9 Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Tue, 20 May 2025 16:25:32 +0200 Subject: [PATCH 6/8] unused code, thanks coderabbit --- cmd/cloud.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index be09b81e..4dd0e70d 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -828,10 +828,6 @@ func cloudSelectProject(ctx context.Context, logger logger.Logger, apiUrl, apike } var options []tui.Option for _, p := range projects { - desc := p.Description - if desc == "" { - desc = "No description provided." - } options = append(options, tui.Option{ ID: p.ID, Text: tui.Bold(tui.PadRight(p.Name, 20, " ")) + tui.Muted(p.ID), From 9bc74510838a1e67a80decdd68db056bf1497cdf Mon Sep 17 00:00:00 2001 From: Pedro Enrique Date: Tue, 20 May 2025 21:45:58 +0200 Subject: [PATCH 7/8] error handling --- cmd/cloud.go | 25 ++++++++++--------------- cmd/project.go | 33 --------------------------------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 4dd0e70d..08c4a083 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -697,8 +697,7 @@ Examples: if dir != "" { proj := project.EnsureProject(ctx, cmd) if proj.Project == nil { - tui.ShowWarning("Project not found") - return + errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("project not found")).ShowErrorAndExit() } selectedProject = proj.Project.ProjectId } else { @@ -707,7 +706,7 @@ Examples: // look up the project by id projects, err := project.ListProjects(ctx, logger, apiUrl, apikey) if err != nil { - errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list projects")).ShowErrorAndExit() + errsystem.New(errsystem.ErrApiRequest, err).ShowErrorAndExit() } for _, p := range projects { if p.ID == projectId { @@ -716,8 +715,8 @@ Examples: } } if selectedProject == "" { - tui.ShowWarning("Project not found") - return + // this will never happen because we've already checked the project id + errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("project not found")).ShowErrorAndExit() } } } @@ -761,8 +760,7 @@ Examples: } } if selectedDeployment == "" { - tui.ShowWarning("No deployment found with tag '%s'", tag) - return + errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no deployment found with tag '%s'", tag)).ShowErrorAndExit() } } else { question = "Select a deployment to rollback" @@ -771,8 +769,8 @@ Examples: } selectedDeployment = cloudSelectDeployment(ctx, logger, apiUrl, apikey, selectedProject, question) if selectedDeployment == "" { - tui.ShowWarning("no deployment selected") - return + // this will never happen because we've already checked the deployment id + errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no deployment selected")).ShowErrorAndExit() } } @@ -785,7 +783,7 @@ Examples: } if !tui.Ask(logger, "Are you sure you want to "+tui.Bold(what)+" the selected deployment?", true) { - tui.ShowWarning("cancelled") + tui.ShowWarning("Canceled") return } } @@ -793,16 +791,13 @@ Examples: if deleteFlag { err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) if err != nil { - tui.ShowError("%s", err.Error()) - return + errsystem.New(errsystem.ErrDeleteApiKey, err, errsystem.WithContextMessage("Failed to delete deployment")).ShowErrorAndExit() } tui.ShowSuccess("Deployment deleted successfully") } else { err := project.RollbackDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) if err != nil { - tui.ShowError("%s", err.Error()) - os.Exit(1) - return + errsystem.New(errsystem.ErrDeployProject, err, errsystem.WithContextMessage("Failed to rollback deployment")).ShowErrorAndExit() } tui.ShowSuccess("Deployment rolled back successfully") } diff --git a/cmd/project.go b/cmd/project.go index 862986e5..dd1c23c8 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -821,39 +821,6 @@ func getConfigTemplateDir(cmd *cobra.Command) (string, bool, error) { return dir, false, nil } -// Helper to fetch projects and prompt user to select one. Returns selected project ID or empty string. -func selectProject(ctx context.Context, logger logger.Logger, apiUrl, apikey string, prompt string) string { - var projects []project.ProjectListData - action := func() { - var err error - projects, err = project.ListProjects(ctx, logger, apiUrl, apikey) - if err != nil { - errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to list projects")).ShowErrorAndExit() - } - } - tui.ShowSpinner("fetching projects ...", action) - if len(projects) == 0 { - showNoProjects() - return "" - } - var options []tui.Option - for _, p := range projects { - desc := p.Description - if desc == "" { - desc = emptyProjectDescription - } - options = append(options, tui.Option{ - ID: p.ID, - Text: tui.Bold(tui.PadRight(p.Name, 20, " ")) + tui.Muted(p.ID), - }) - } - selected := tui.Select(logger, prompt, "", options) - if selected == "" { - tui.ShowWarning("no project selected") - } - return selected -} - func init() { rootCmd.AddCommand(projectCmd) rootCmd.AddCommand(projectNewCmd) From 970e7fd77dcf98a24a1c4c7076aa4a509c19dac4 Mon Sep 17 00:00:00 2001 From: Jeff Haynie Date: Thu, 22 May 2025 13:22:06 -0500 Subject: [PATCH 8/8] more testing, some UI formatting --- cmd/cloud.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 08c4a083..e0b3df54 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -783,9 +783,11 @@ Examples: } if !tui.Ask(logger, "Are you sure you want to "+tui.Bold(what)+" the selected deployment?", true) { + fmt.Println() tui.ShowWarning("Canceled") return } + fmt.Println() } if deleteFlag { @@ -846,8 +848,8 @@ func cloudSelectDeployment(ctx context.Context, logger logger.Logger, apiUrl, ap } tui.ShowSpinner("fetching deployments ...", fetchDeploymentsAction) if len(deployments) == 0 { - tui.ShowWarning("no deployments found") - return "" + tui.ShowWarning("no deployments found for this project") + os.Exit(1) } var deploymentOptions []tui.Option for _, d := range deployments { @@ -869,12 +871,12 @@ func cloudSelectDeployment(ctx context.Context, logger logger.Logger, apiUrl, ap if d.Active { deploymentOptions = append(deploymentOptions, tui.Option{ ID: d.ID, - Text: fmt.Sprintf("%s %s, tags: [%-50s], msg: [%s]", "✅", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), + Text: fmt.Sprintf("%s %s %-50s %s", "✅", tui.Title(date.Format(time.Stamp)), tui.Bold(tags), tui.Muted(msg)), }) } else { deploymentOptions = append(deploymentOptions, tui.Option{ ID: d.ID, - Text: fmt.Sprintf(" %s, tags: [%-50s], msg: [%s]", tui.Title(date.Format("2006-01-02 15:04:05")), tui.Bold(tags), tui.Bold(msg)), + Text: fmt.Sprintf(" %s %-50s %s", tui.Title(date.Format(time.Stamp)), tui.Bold(tags), tui.Muted(msg)), }) } }