diff --git a/AGENTS.md b/AGENTS.md index 7b41345e..8f390a08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -292,7 +292,7 @@ Vitess only. See https://planetscale.com/docs/vitess/schema-changes/aggressive-c ## Vitess deploy requests (inspect + throttler) -Core lifecycle is already covered (`list/create/show/diff/review/deploy/apply/unblock/edit/cancel/close/revert/skip-revert`). `unblock` clears the queue after a failed deploy or revert (dashboard “Unblock deploy queue”); it is not `apply`. These inspect commands are read-only: +Core lifecycle is already covered (`list/create/show/diff/review/deploy/apply/unblock/update/cancel/close/revert/skip-revert`). `update` (`edit` is an alias) sets auto-apply and auto-delete-branch. `unblock` clears the queue after a failed deploy or revert (dashboard “Unblock deploy queue”); it is not `apply`. These inspect commands are read-only: ```bash pscale deploy-request queue --org --format json # database deploy queue (first page) @@ -313,12 +313,18 @@ pscale deploy-request throttler update --org --format Alias: `pscale dr …` works the same. Vitess only. `--ratio` is 0–95 (0 disables throttling; 95 is slowest). Use either `--ratio` or `--configuration keyspace=ratio`, not both. +```bash +pscale deploy-request update --org --format json --enable-auto-apply +pscale deploy-request update --org --format json --auto-delete-branch=false +``` + After a failed deploy or revert (`complete_error` / `complete_revert_error`), unblock the queue. This is not `apply` (gated cutover) and it cannot fix a deploy-check `error`: ```bash pscale deploy-request unblock --org --format json ``` + ## Maintenance schedules (Vitess Enterprise) Read-only visibility into planned maintenance windows for a Vitess database (Enterprise plans): diff --git a/internal/cmd/deployrequest/dr.go b/internal/cmd/deployrequest/dr.go index 0def4608..89a8114c 100644 --- a/internal/cmd/deployrequest/dr.go +++ b/internal/cmd/deployrequest/dr.go @@ -31,7 +31,7 @@ func DeployRequestCmd(ch *cmdutil.Helper) *cobra.Command { cmd.AddCommand(DeployCmd(ch)) cmd.AddCommand(DeploymentCmd(ch)) cmd.AddCommand(DiffCmd(ch)) - cmd.AddCommand(EditCmd(ch)) + cmd.AddCommand(UpdateCmd(ch)) cmd.AddCommand(ForceCutoverCmd(ch)) cmd.AddCommand(ListCmd(ch)) cmd.AddCommand(OperationsCmd(ch)) diff --git a/internal/cmd/deployrequest/edit_test.go b/internal/cmd/deployrequest/edit_test.go index bc2663f8..9528c2e3 100644 --- a/internal/cmd/deployrequest/edit_test.go +++ b/internal/cmd/deployrequest/edit_test.go @@ -135,7 +135,7 @@ func TestDeployRequest_EditCmdNoFlags(t *testing.T) { cmd.SetArgs([]string{db, strconv.FormatUint(number, 10)}) err := cmd.Execute() - c.Assert(err, qt.ErrorMatches, "must specify either --enable-auto-apply, --disable-auto-apply, or --auto-apply") + c.Assert(err, qt.ErrorMatches, "must specify at least one of --enable-auto-apply, --disable-auto-apply, --auto-delete-branch, or --auto-apply") } func TestDeployRequest_EditCmdBothFlags(t *testing.T) { @@ -307,3 +307,116 @@ func TestDeployRequest_EditCmdMixedFlags(t *testing.T) { res := &ps.DeployRequest{Number: number} c.Assert(buf.String(), qt.JSONEquals, res) } + +func TestDeployRequest_UpdateCmdEnableAutoDeleteBranch(t *testing.T) { + c := qt.New(t) + + var buf bytes.Buffer + format := printer.JSON + p := printer.NewPrinter(&format) + p.SetResourceOutput(&buf) + + org := "planetscale" + db := "planetscale" + number := uint64(10) + + svc := &mock.DeployRequestsService{ + AutoDeleteBranchFn: func(ctx context.Context, req *ps.AutoDeleteBranchRequest) (*ps.DeployRequest, error) { + c.Assert(req.Number, qt.Equals, number) + c.Assert(req.Database, qt.Equals, db) + c.Assert(req.Organization, qt.Equals, org) + c.Assert(req.Enable, qt.IsTrue) + return &ps.DeployRequest{Number: number}, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{Organization: org}, + Client: func() (*ps.Client, error) { + return &ps.Client{DeployRequests: svc}, nil + }, + } + + cmd := UpdateCmd(ch) + cmd.SetArgs([]string{db, strconv.FormatUint(number, 10), "--auto-delete-branch"}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.AutoDeleteBranchFnInvoked, qt.IsTrue) + c.Assert(svc.AutoApplyFnInvoked, qt.IsFalse) + c.Assert(buf.String(), qt.JSONEquals, &ps.DeployRequest{Number: number}) +} + +func TestDeployRequest_UpdateCmdBothSettings(t *testing.T) { + c := qt.New(t) + + var buf bytes.Buffer + format := printer.JSON + p := printer.NewPrinter(&format) + p.SetResourceOutput(&buf) + + org := "planetscale" + db := "planetscale" + number := uint64(10) + + svc := &mock.DeployRequestsService{ + AutoApplyFn: func(ctx context.Context, req *ps.AutoApplyDeployRequestRequest) (*ps.DeployRequest, error) { + c.Assert(req.Enable, qt.IsTrue) + return &ps.DeployRequest{Number: number}, nil + }, + AutoDeleteBranchFn: func(ctx context.Context, req *ps.AutoDeleteBranchRequest) (*ps.DeployRequest, error) { + c.Assert(req.Enable, qt.IsFalse) + return &ps.DeployRequest{Number: number}, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{Organization: org}, + Client: func() (*ps.Client, error) { + return &ps.Client{DeployRequests: svc}, nil + }, + } + + cmd := UpdateCmd(ch) + cmd.SetArgs([]string{db, strconv.FormatUint(number, 10), "--enable-auto-apply", "--auto-delete-branch=false"}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.AutoApplyFnInvoked, qt.IsTrue) + c.Assert(svc.AutoDeleteBranchFnInvoked, qt.IsTrue) + c.Assert(buf.String(), qt.JSONEquals, &ps.DeployRequest{Number: number}) +} + +func TestDeployRequest_UpdateCmdDisableAutoDeleteBranch(t *testing.T) { + c := qt.New(t) + + var buf bytes.Buffer + format := printer.JSON + p := printer.NewPrinter(&format) + p.SetResourceOutput(&buf) + + svc := &mock.DeployRequestsService{ + AutoDeleteBranchFn: func(ctx context.Context, req *ps.AutoDeleteBranchRequest) (*ps.DeployRequest, error) { + c.Assert(req.Enable, qt.IsFalse) + return &ps.DeployRequest{Number: 10}, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{Organization: "planetscale"}, + Client: func() (*ps.Client, error) { + return &ps.Client{DeployRequests: svc}, nil + }, + } + + cmd := UpdateCmd(ch) + cmd.SetArgs([]string{"planetscale", "10", "--auto-delete-branch=false"}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.AutoDeleteBranchFnInvoked, qt.IsTrue) + c.Assert(buf.String(), qt.JSONEquals, &ps.DeployRequest{Number: 10}) +} diff --git a/internal/cmd/deployrequest/edit.go b/internal/cmd/deployrequest/update.go similarity index 50% rename from internal/cmd/deployrequest/edit.go rename to internal/cmd/deployrequest/update.go index c9ff66c0..302275a6 100644 --- a/internal/cmd/deployrequest/edit.go +++ b/internal/cmd/deployrequest/update.go @@ -3,6 +3,7 @@ package deployrequest import ( "fmt" "strconv" + "strings" "github.com/planetscale/cli/internal/cmdutil" "github.com/planetscale/cli/internal/planetscale" @@ -11,18 +12,26 @@ import ( "github.com/spf13/cobra" ) -// EditCmd is the command for editing preferences on deploy requests. -func EditCmd(ch *cmdutil.Helper) *cobra.Command { +// UpdateCmd updates deploy-request settings. `edit` is kept as an alias. +func UpdateCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { enable_auto_apply bool disable_auto_apply bool autoApply string // deprecated + auto_delete_branch bool } cmd := &cobra.Command{ - Use: "edit [flags]", - Short: "Edit a deploy request", - Args: cmdutil.RequiredArgs("database", "number"), + Use: "update [flags]", + Aliases: []string{"edit"}, + Short: "Update a deploy request", + Long: `Update settings on a deploy request. + +Use --enable-auto-apply / --disable-auto-apply to control gated cutover, and +--auto-delete-branch to control whether the source branch is deleted after a +successful deploy (same flag as 'deploy-request create'). At least one setting +must be passed; unset flags are not sent.`, + Args: cmdutil.RequiredArgs("database", "number"), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() database := args[0] @@ -42,11 +51,13 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command { return fmt.Errorf("cannot use both --enable-auto-apply and --disable-auto-apply flags together") } - hasNewFlags := flags.enable_auto_apply || flags.disable_auto_apply + hasNewAutoApply := flags.enable_auto_apply || flags.disable_auto_apply hasDeprecatedFlag := flags.autoApply != "" + hasAutoApply := hasNewAutoApply || hasDeprecatedFlag + hasAutoDelete := cmd.Flags().Changed("auto-delete-branch") - if !hasNewFlags && !hasDeprecatedFlag { - return fmt.Errorf("must specify either --enable-auto-apply, --disable-auto-apply, or --auto-apply") + if !hasAutoApply && !hasAutoDelete { + return fmt.Errorf("must specify at least one of --enable-auto-apply, --disable-auto-apply, --auto-delete-branch, or --auto-apply") } if hasDeprecatedFlag { @@ -57,20 +68,7 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command { } } - var enable bool - if hasNewFlags { - enable = flags.enable_auto_apply - } else { - enable = flags.autoApply == "enable" - } - - dr, err := client.DeployRequests.AutoApplyDeploy(ctx, &planetscale.AutoApplyDeployRequestRequest{ - Organization: ch.Config.Organization, - Database: database, - Number: n, - Enable: enable, - }) - if err != nil { + handleDRErr := func(err error) error { switch cmdutil.ErrCode(err) { case planetscale.ErrNotFound: return fmt.Errorf("deploy request '%s/%s' does not exist in organization %s", @@ -80,8 +78,49 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command { } } + var dr *planetscale.DeployRequest + + if hasAutoApply { + var enable bool + if hasNewAutoApply { + enable = flags.enable_auto_apply + } else { + enable = flags.autoApply == "enable" + } + + dr, err = client.DeployRequests.AutoApplyDeploy(ctx, &planetscale.AutoApplyDeployRequestRequest{ + Organization: ch.Config.Organization, + Database: database, + Number: n, + Enable: enable, + }) + if err != nil { + return handleDRErr(err) + } + } + + if hasAutoDelete { + dr, err = client.DeployRequests.AutoDeleteBranch(ctx, &planetscale.AutoDeleteBranchRequest{ + Organization: ch.Config.Organization, + Database: database, + Number: n, + Enable: flags.auto_delete_branch, + }) + if err != nil { + return handleDRErr(err) + } + } + if ch.Printer.Format() == printer.Human { - ch.Printer.Printf("Successfully updated auto-apply changes for '%s/%s'.\n", + updated := make([]string, 0, 2) + if hasAutoApply { + updated = append(updated, "auto-apply") + } + if hasAutoDelete { + updated = append(updated, "auto-delete-branch") + } + ch.Printer.Printf("Successfully updated %s for '%s/%s'.\n", + strings.Join(updated, " and "), printer.BoldBlue(database), printer.BoldBlue(dr.Number)) return nil @@ -93,9 +132,15 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command { cmd.Flags().BoolVar(&flags.enable_auto_apply, "enable-auto-apply", false, "Enable auto-apply. The deploy request will automatically swap over to the new schema once ready.") cmd.Flags().BoolVar(&flags.disable_auto_apply, "disable-auto-apply", false, "Disable auto-apply. The deploy request will wait for your confirmation before swapping to the new schema. Use 'deploy-request apply' to apply the changes manually.") + cmd.Flags().BoolVar(&flags.auto_delete_branch, "auto-delete-branch", false, "Delete the branch after the deploy request completes. Pass --auto-delete-branch=false to keep it.") cmd.Flags().StringVar(&flags.autoApply, "auto-apply", "", "Update the auto apply setting for a deploy request. Possible values: [enable,disable]") cmd.Flags().MarkDeprecated("auto-apply", "use --enable-auto-apply or --disable-auto-apply instead") return cmd } + +// EditCmd is an alias of UpdateCmd so existing tests keep compiling. +func EditCmd(ch *cmdutil.Helper) *cobra.Command { + return UpdateCmd(ch) +} diff --git a/internal/mock/dr.go b/internal/mock/dr.go index a1a0f75e..0c13297e 100644 --- a/internal/mock/dr.go +++ b/internal/mock/dr.go @@ -19,6 +19,9 @@ type DeployRequestsService struct { AutoApplyFn func(context.Context, *ps.AutoApplyDeployRequestRequest) (*ps.DeployRequest, error) AutoApplyFnInvoked bool + AutoDeleteBranchFn func(context.Context, *ps.AutoDeleteBranchRequest) (*ps.DeployRequest, error) + AutoDeleteBranchFnInvoked bool + CancelFn func(context.Context, *ps.CancelDeployRequestRequest) (*ps.DeployRequest, error) CancelFnInvoked bool @@ -94,6 +97,11 @@ func (d *DeployRequestsService) AutoApplyDeploy(ctx context.Context, req *ps.Aut return d.AutoApplyFn(ctx, req) } +func (d *DeployRequestsService) AutoDeleteBranch(ctx context.Context, req *ps.AutoDeleteBranchRequest) (*ps.DeployRequest, error) { + d.AutoDeleteBranchFnInvoked = true + return d.AutoDeleteBranchFn(ctx, req) +} + func (d *DeployRequestsService) CancelDeploy(ctx context.Context, req *ps.CancelDeployRequestRequest) (*ps.DeployRequest, error) { d.CancelFnInvoked = true return d.CancelFn(ctx, req) diff --git a/internal/planetscale/deploy_requests.go b/internal/planetscale/deploy_requests.go index e447ea71..979ed519 100644 --- a/internal/planetscale/deploy_requests.go +++ b/internal/planetscale/deploy_requests.go @@ -20,6 +20,7 @@ var _ DeployRequestsService = (*deployRequestsService)(nil) type DeployRequestsService interface { ApplyDeploy(context.Context, *ApplyDeployRequestRequest) (*DeployRequest, error) AutoApplyDeploy(context.Context, *AutoApplyDeployRequestRequest) (*DeployRequest, error) + AutoDeleteBranch(context.Context, *AutoDeleteBranchRequest) (*DeployRequest, error) CancelDeploy(context.Context, *CancelDeployRequestRequest) (*DeployRequest, error) CloseDeploy(context.Context, *CloseDeployRequestRequest) (*DeployRequest, error) Create(context.Context, *CreateDeployRequestRequest) (*DeployRequest, error) @@ -306,6 +307,13 @@ type AutoApplyDeployRequestRequest struct { Enable bool `json:"-"` } +type AutoDeleteBranchRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Number uint64 `json:"-"` + Enable bool `json:"-"` +} + type CancelDeployRequestRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -536,6 +544,27 @@ func (d *deployRequestsService) AutoApplyDeploy(ctx context.Context, autoApplyRe return drr, nil } +func (d *deployRequestsService) AutoDeleteBranch(ctx context.Context, autoDeleteReq *AutoDeleteBranchRequest) (*DeployRequest, error) { + reqBody := struct { + Enable bool `json:"enable"` + }{ + Enable: autoDeleteReq.Enable, + } + + path := deployRequestActionAPIPath(autoDeleteReq.Organization, autoDeleteReq.Database, autoDeleteReq.Number, "auto-delete-branch") + req, err := d.client.newRequest(http.MethodPut, path, reqBody) + if err != nil { + return nil, fmt.Errorf("error creating http request: %w", err) + } + + drr := &DeployRequest{} + if err := d.client.do(ctx, req, &drr); err != nil { + return nil, err + } + + return drr, nil +} + // SkipRevert skips a pending revert of a completed deploy request func (d *deployRequestsService) SkipRevertDeploy(ctx context.Context, deployReq *SkipRevertDeployRequestRequest) (*DeployRequest, error) { path := deployRequestActionAPIPath(deployReq.Organization, deployReq.Database, deployReq.Number, "skip-revert") diff --git a/internal/planetscale/deploy_requests_test.go b/internal/planetscale/deploy_requests_test.go index abecbdb9..b88ac307 100644 --- a/internal/planetscale/deploy_requests_test.go +++ b/internal/planetscale/deploy_requests_test.go @@ -323,6 +323,51 @@ func TestDeployRequests_ForceCutover(t *testing.T) { c.Assert(dr, qt.DeepEquals, want) } +func TestDeployRequests_AutoDeleteBranch(t *testing.T) { + c := qt.New(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, qt.Equals, http.MethodPut) + c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/test-organization/databases/test-database/deploy-requests/1337/auto-delete-branch") + var body struct { + Enable bool `json:"enable"` + } + c.Assert(json.NewDecoder(r.Body).Decode(&body), qt.IsNil) + c.Assert(body.Enable, qt.IsTrue) + w.WriteHeader(200) + out := `{"id": "test-deploy-request-id", "branch": "development", "into_branch": "some-branch", "notes": "", "created_at": "2021-01-14T10:19:23.000Z", "updated_at": "2021-01-14T10:19:23.000Z", "closed_at": null, "deployment": { "state": "pending" }, "number": 1337}` + _, err := w.Write([]byte(out)) + c.Assert(err, qt.IsNil) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + dr, err := client.DeployRequests.AutoDeleteBranch(context.Background(), &AutoDeleteBranchRequest{ + Organization: "test-organization", + Database: "test-database", + Number: 1337, + Enable: true, + }) + + testTime := time.Date(2021, time.January, 14, 10, 19, 23, 0, time.UTC) + want := &DeployRequest{ + ID: "test-deploy-request-id", + Branch: "development", + Deployment: &Deployment{ + State: "pending", + }, + IntoBranch: "some-branch", + Number: 1337, + Notes: "", + CreatedAt: testTime, + UpdatedAt: testTime, + } + + c.Assert(err, qt.IsNil) + c.Assert(dr, qt.DeepEquals, want) +} + func TestDeployRequests_Close(t *testing.T) { c := qt.New(t)