-
Notifications
You must be signed in to change notification settings - Fork 7
[AGENT-130] Delete and Roll Back deployments #313
Changes from all commits
5d06122
936b3ba
397820f
d1c0725
6ddcf9f
02db0d5
9bc7451
970e7fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -672,6 +672,218 @@ 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 { | ||||||||||||||||||||||
| errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("project not found")).ShowErrorAndExit() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| 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).ShowErrorAndExit() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| for _, p := range projects { | ||||||||||||||||||||||
| if p.ID == projectId { | ||||||||||||||||||||||
| selectedProject = p.ID | ||||||||||||||||||||||
| break | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if selectedProject == "" { | ||||||||||||||||||||||
| // this will never happen because we've already checked the project id | ||||||||||||||||||||||
| errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("project not found")).ShowErrorAndExit() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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 == "" { | ||||||||||||||||||||||
| errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no deployment found with tag '%s'", tag)).ShowErrorAndExit() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } 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 == "" { | ||||||||||||||||||||||
| // this will never happen because we've already checked the deployment id | ||||||||||||||||||||||
| errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no deployment selected")).ShowErrorAndExit() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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) { | ||||||||||||||||||||||
| fmt.Println() | ||||||||||||||||||||||
| tui.ShowWarning("Canceled") | ||||||||||||||||||||||
| return | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| fmt.Println() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if deleteFlag { | ||||||||||||||||||||||
| err := project.DeleteDeployment(ctx, logger, apiUrl, apikey, selectedProject, selectedDeployment) | ||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||
| 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 { | ||||||||||||||||||||||
| errsystem.New(errsystem.ErrDeployProject, err, errsystem.WithContextMessage("Failed to rollback deployment")).ShowErrorAndExit() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| 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 { | ||||||||||||||||||||||
| 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 for this project") | ||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+852
to
+853
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use errsystem instead of direct os.Exit Direct calls to - tui.ShowWarning("no deployments found for this project")
- os.Exit(1)
+ errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no deployments found for this project")).ShowErrorAndExit()📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this isn't a unexpected system error which is when we use errsystem vs an expected one.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jhaynie I appreciate the clarification. You're absolutely right - this is an expected condition rather than an unexpected system error. I misunderstood the error handling pattern in the codebase. The current implementation with (_/) ✏️ Learnings added
|
||||||||||||||||||||||
| 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(tags) > 50 { | ||||||||||||||||||||||
| tags = tags[:50] + "..." | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if d.Active { | ||||||||||||||||||||||
| deploymentOptions = append(deploymentOptions, tui.Option{ | ||||||||||||||||||||||
| ID: d.ID, | ||||||||||||||||||||||
| 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 %-50s %s", tui.Title(date.Format(time.Stamp)), tui.Bold(tags), tui.Muted(msg)), | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i don't really love the way this looks... i might make some suggestions /changes here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, not sure what the best way is or what info to show of each deployment |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| selectedDeployment := tui.Select(logger, prompt, "", deploymentOptions) | ||||||||||||||||||||||
| return selectedDeployment | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| func init() { | ||||||||||||||||||||||
| rootCmd.AddCommand(cloudCmd) | ||||||||||||||||||||||
| rootCmd.AddCommand(cloudDeployCmd) | ||||||||||||||||||||||
|
|
@@ -701,4 +913,12 @@ 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("force", false, "Force the rollback or delete") | ||||||||||||||||||||||
| cloudRollbackCmd.Flags().Bool("delete", false, "Delete the deployment instead of rolling back") | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.