diff --git a/AGENTS.md b/AGENTS.md index d264df73..68214935 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -411,7 +411,7 @@ pscale database throttler update --org --format json \ ## Vitess aggressive cutover -Database-level setting for future deploy requests (not the same as `deploy-request force-cutover`): +Database-level setting for future deploy requests (not the same as `deploy-request force-cutover`, and not the same as `deploy-request create --aggressive-cutover`): ```bash pscale database aggressive-cutover show --org --format json @@ -423,7 +423,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/retry/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: +Core lifecycle is already covered (`list/create/show/diff/review/deploy/apply/retry/unblock/update/cancel/close/revert/skip-revert`). `update` (`edit` is an alias) sets auto-apply, auto-delete-branch, and per-request aggressive cutover. `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) @@ -447,8 +447,13 @@ Alias: `pscale dr …` works the same. Vitess only. `--ratio` is 0–95 (0 disab ```bash pscale deploy-request update --org --format json --enable-auto-apply pscale deploy-request update --org --format json --auto-delete-branch=false +pscale deploy-request create --org --aggressive-cutover --format json +pscale deploy-request update --org --format json --aggressive-cutover +pscale deploy-request update --org --format json --aggressive-cutover=false ``` +`--aggressive-cutover` opts this deploy request into aggressive cutover: Vitess kills blocking queries and transactions on the first cutover attempt instead of retrying for up to an hour. It does not change the database setting (`pscale database aggressive-cutover`) and other deploy requests are unaffected. Create sends `aggressive_cutover: true` only when the flag is set. Update sends `{ "enable": }` to `PUT .../deploy-requests/:number/aggressive-cutover`, including `enable: false` when `--aggressive-cutover=false` is passed. Unset flags are not sent. The opt-in can be changed while the deployment is `pending`, `ready`, `error`, `no_changes`, or `queued`. After submission the API rejects the update. `show`, `deployment`, and `queue` JSON include `deployment.aggressive_cutover`. Human `show` and `queue` tables include an `aggressive_cutover` column. This is not `deploy-request force-cutover`. + 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 diff --git a/internal/cmd/deployrequest/create.go b/internal/cmd/deployrequest/create.go index cc4bdf35..42194164 100644 --- a/internal/cmd/deployrequest/create.go +++ b/internal/cmd/deployrequest/create.go @@ -18,6 +18,7 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command { auto_delete_branch bool enable_auto_apply bool disable_auto_apply bool + aggressive_cutover bool } cmd := &cobra.Command{ @@ -59,6 +60,10 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command { request.AutoDeleteBranch = true } + if flags.aggressive_cutover { + request.AggressiveCutover = true + } + dr, err := client.DeployRequests.Create(ctx, request) if err != nil { @@ -85,6 +90,7 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command { cmd.PersistentFlags().StringVar(&flags.into, "into", "", "Branch to deploy into. By default, it's the parent branch (if present) or the database's default branch.") cmd.PersistentFlags().StringVar(&flags.notes, "notes", "", "Notes to include with the deploy request.") cmd.Flags().BoolVar(&flags.auto_delete_branch, "auto-delete-branch", false, "Delete the branch after the deploy request completes.") + cmd.Flags().BoolVar(&flags.aggressive_cutover, "aggressive-cutover", false, "Cut this deploy request over aggressively: Vitess kills blocking queries and transactions on the first cutover attempt instead of retrying for up to an hour. Does not change the database setting. Other deploy requests are unaffected.") 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.") diff --git a/internal/cmd/deployrequest/create_test.go b/internal/cmd/deployrequest/create_test.go index eaa06efa..01610a74 100644 --- a/internal/cmd/deployrequest/create_test.go +++ b/internal/cmd/deployrequest/create_test.go @@ -34,6 +34,7 @@ func TestDeployRequest_CreateCmd(t *testing.T) { c.Assert(req.Database, qt.Equals, db) c.Assert(req.Branch, qt.Equals, branch) c.Assert(req.AutoCutover, qt.Equals, false) + c.Assert(req.AggressiveCutover, qt.Equals, false) c.Assert(req.Notes, qt.Equals, notes) c.Assert(req.AutoDeleteBranch, qt.Equals, true) c.Assert(req.IntoBranch, qt.Equals, "", qt.Commentf("default value of the '--into' flag has changed")) @@ -188,6 +189,61 @@ func TestDeployRequest_CreateCmdBothAutoApplyFlags(t *testing.T) { c.Assert(err, qt.ErrorMatches, "cannot use both --enable-auto-apply and --disable-auto-apply flags together") } +func TestDeployRequest_CreateCmdAggressiveCutover(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" + branch := "development" + var number uint64 = 10 + + svc := &mock.DeployRequestsService{ + CreateFn: func(ctx context.Context, req *ps.CreateDeployRequestRequest) (*ps.DeployRequest, error) { + c.Assert(req.Organization, qt.Equals, org) + c.Assert(req.Database, qt.Equals, db) + c.Assert(req.Branch, qt.Equals, branch) + c.Assert(req.AggressiveCutover, qt.IsTrue) + + return &ps.DeployRequest{ + Number: number, + Deployment: &ps.Deployment{ + AggressiveCutover: true, + }, + }, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{ + Organization: org, + }, + Client: func() (*ps.Client, error) { + return &ps.Client{ + DeployRequests: svc, + }, nil + }, + } + + cmd := CreateCmd(ch) + cmd.SetArgs([]string{db, branch, "--aggressive-cutover"}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.CreateFnInvoked, qt.IsTrue) + c.Assert(buf.String(), qt.JSONEquals, &ps.DeployRequest{ + Number: number, + Deployment: &ps.Deployment{ + AggressiveCutover: true, + }, + }) +} + func TestDeployRequest_CreateCmdIntoFlag(t *testing.T) { c := qt.New(t) diff --git a/internal/cmd/deployrequest/dr.go b/internal/cmd/deployrequest/dr.go index 62b56503..de72e141 100644 --- a/internal/cmd/deployrequest/dr.go +++ b/internal/cmd/deployrequest/dr.go @@ -72,6 +72,7 @@ type inlineDeployment struct { State string `header:"deploy state" json:"state"` Deployable bool `header:"deployable" json:"deployable"` InstantDDLEligible bool `header:"instant ddl eligible" json:"instant_ddl_eligible"` + AggressiveCutover bool `header:"aggressive_cutover" json:"aggressive_cutover"` QueuedAt string `header:"queued_at" json:"queued_at"` StartedAt string `header:"started_at" json:"started_at"` @@ -133,6 +134,7 @@ func toInlineDeployment(d *planetscale.Deployment) inlineDeployment { State: d.State, Deployable: d.Deployable, InstantDDLEligible: d.InstantDDLEligible, + AggressiveCutover: d.AggressiveCutover, QueuedAt: formatTimestamp(d.QueuedAt), StartedAt: formatTimestamp(d.StartedAt), FinishedAt: formatTimestamp(d.FinishedAt), diff --git a/internal/cmd/deployrequest/edge_ops_read_test.go b/internal/cmd/deployrequest/edge_ops_read_test.go index d0bca2ce..b6c7d869 100644 --- a/internal/cmd/deployrequest/edge_ops_read_test.go +++ b/internal/cmd/deployrequest/edge_ops_read_test.go @@ -49,6 +49,40 @@ func TestDeployRequest_QueueCmd(t *testing.T) { c.Assert(buf.String(), qt.JSONEquals, []*ps.Deployment{deployment}) } +func TestDeployRequest_QueueHumanAggressiveCutoverColumn(t *testing.T) { + c := qt.New(t) + + var buf bytes.Buffer + format := printer.Human + p := printer.NewPrinter(&format) + p.SetResourceOutput(&buf) + + svc := &mock.DeployRequestsService{ + GetDeployQueueFn: func(ctx context.Context, req *ps.GetDeployQueueRequest) ([]*ps.Deployment, error) { + return []*ps.Deployment{{ + ID: "dep-1", + DeployRequestNumber: 1284, + State: "queued", + AggressiveCutover: true, + }}, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{Organization: "planetscale"}, + Client: func() (*ps.Client, error) { + return &ps.Client{DeployRequests: svc}, nil + }, + } + + cmd := QueueCmd(ch) + cmd.SetArgs([]string{"planetscale"}) + c.Assert(cmd.Execute(), qt.IsNil) + c.Assert(buf.String(), qt.Contains, "AGGRESSIVE CUTOVER") + c.Assert(buf.String(), qt.Contains, "Yes") +} + func TestDeployRequest_OperationsCmd(t *testing.T) { c := qt.New(t) diff --git a/internal/cmd/deployrequest/edit_test.go b/internal/cmd/deployrequest/edit_test.go index 9528c2e3..f156b3fb 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 at least one of --enable-auto-apply, --disable-auto-apply, --auto-delete-branch, or --auto-apply") + c.Assert(err, qt.ErrorMatches, "must specify at least one of --enable-auto-apply, --disable-auto-apply, --auto-delete-branch, --aggressive-cutover, or --auto-apply") } func TestDeployRequest_EditCmdBothFlags(t *testing.T) { @@ -420,3 +420,90 @@ func TestDeployRequest_UpdateCmdDisableAutoDeleteBranch(t *testing.T) { c.Assert(svc.AutoDeleteBranchFnInvoked, qt.IsTrue) c.Assert(buf.String(), qt.JSONEquals, &ps.DeployRequest{Number: 10}) } + +func TestDeployRequest_UpdateCmdAggressiveCutoverFalse(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{ + AggressiveCutoverFn: func(ctx context.Context, req *ps.DeployRequestAggressiveCutoverRequest) (*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.IsFalse) + return &ps.DeployRequest{ + Number: number, + Deployment: &ps.Deployment{ + AggressiveCutover: false, + }, + }, 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), "--aggressive-cutover=false"}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.AggressiveCutoverFnInvoked, qt.IsTrue) + c.Assert(svc.AutoApplyFnInvoked, qt.IsFalse) + c.Assert(svc.AutoDeleteBranchFnInvoked, qt.IsFalse) + c.Assert(buf.String(), qt.JSONEquals, &ps.DeployRequest{ + Number: number, + Deployment: &ps.Deployment{ + AggressiveCutover: false, + }, + }) +} + +func TestDeployRequest_UpdateCmdAggressiveCutover(t *testing.T) { + c := qt.New(t) + + var human bytes.Buffer + format := printer.Human + p := printer.NewPrinter(&format) + p.SetHumanOutput(&human) + + org := "planetscale" + db := "planetscale" + number := uint64(1284) + + svc := &mock.DeployRequestsService{ + AggressiveCutoverFn: func(ctx context.Context, req *ps.DeployRequestAggressiveCutoverRequest) (*ps.DeployRequest, error) { + 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), "--aggressive-cutover"}) + err := cmd.Execute() + + c.Assert(err, qt.IsNil) + c.Assert(svc.AggressiveCutoverFnInvoked, qt.IsTrue) + c.Assert(human.String(), qt.Contains, "Successfully updated aggressive-cutover for") +} diff --git a/internal/cmd/deployrequest/queue.go b/internal/cmd/deployrequest/queue.go index f9076153..c5744aad 100644 --- a/internal/cmd/deployrequest/queue.go +++ b/internal/cmd/deployrequest/queue.go @@ -64,6 +64,7 @@ type DeploymentRow struct { Deployable bool `header:"deployable" json:"deployable"` AutoCutover bool `header:"auto_cutover" json:"auto_cutover"` AutoDeleteBranch bool `header:"auto_delete_branch" json:"auto_delete_branch"` + AggressiveCutover bool `header:"aggressive_cutover" json:"aggressive_cutover"` InstantDDLEligible bool `header:"instant_ddl_eligible" json:"instant_ddl_eligible"` QueuePaused bool `header:"queue_paused" json:"queue_paused"` CreatedAt string `header:"created_at" json:"created_at"` @@ -94,6 +95,7 @@ func toDeployment(d *planetscale.Deployment) *DeploymentRow { Deployable: d.Deployable, AutoCutover: d.AutoCutover, AutoDeleteBranch: d.AutoDeleteBranch, + AggressiveCutover: d.AggressiveCutover, InstantDDLEligible: d.InstantDDLEligible, QueuePaused: d.QueuePaused, CreatedAt: formatTimestampRequired(d.CreatedAt), diff --git a/internal/cmd/deployrequest/show_test.go b/internal/cmd/deployrequest/show_test.go index 76add824..cc37da42 100644 --- a/internal/cmd/deployrequest/show_test.go +++ b/internal/cmd/deployrequest/show_test.go @@ -119,6 +119,41 @@ func TestDeployRequest_ShowBranchName(t *testing.T) { c.Assert(buf.String(), qt.JSONEquals, res) } +func TestDeployRequest_ShowHumanAggressiveCutoverColumn(t *testing.T) { + c := qt.New(t) + + var buf bytes.Buffer + format := printer.Human + p := printer.NewPrinter(&format) + p.SetResourceOutput(&buf) + + svc := &mock.DeployRequestsService{ + GetFn: func(ctx context.Context, req *ps.GetDeployRequestRequest) (*ps.DeployRequest, error) { + return &ps.DeployRequest{ + Number: 1284, + Deployment: &ps.Deployment{ + State: "ready", + AggressiveCutover: true, + }, + }, nil + }, + } + + ch := &cmdutil.Helper{ + Printer: p, + Config: &config.Config{Organization: "planetscale"}, + Client: func() (*ps.Client, error) { + return &ps.Client{DeployRequests: svc}, nil + }, + } + + cmd := ShowCmd(ch) + cmd.SetArgs([]string{"planetscale", "1284"}) + c.Assert(cmd.Execute(), qt.IsNil) + c.Assert(buf.String(), qt.Contains, "AGGRESSIVE CUTOVER") + c.Assert(buf.String(), qt.Contains, "Yes") +} + func TestDeployRequest_ShowTimestampBug(t *testing.T) { c := qt.New(t) diff --git a/internal/cmd/deployrequest/update.go b/internal/cmd/deployrequest/update.go index 302275a6..a39f2e51 100644 --- a/internal/cmd/deployrequest/update.go +++ b/internal/cmd/deployrequest/update.go @@ -19,6 +19,7 @@ func UpdateCmd(ch *cmdutil.Helper) *cobra.Command { disable_auto_apply bool autoApply string // deprecated auto_delete_branch bool + aggressive_cutover bool } cmd := &cobra.Command{ @@ -27,10 +28,11 @@ func UpdateCmd(ch *cmdutil.Helper) *cobra.Command { Short: "Update a deploy request", Long: `Update settings on a deploy request. -Use --enable-auto-apply / --disable-auto-apply to control gated cutover, and +Use --enable-auto-apply / --disable-auto-apply to control gated cutover, --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.`, +successful deploy (same flag as 'deploy-request create'), and +--aggressive-cutover to opt this deploy request into aggressive cutover. +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() @@ -55,9 +57,10 @@ must be passed; unset flags are not sent.`, hasDeprecatedFlag := flags.autoApply != "" hasAutoApply := hasNewAutoApply || hasDeprecatedFlag hasAutoDelete := cmd.Flags().Changed("auto-delete-branch") + hasAggressiveCutover := cmd.Flags().Changed("aggressive-cutover") - 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 !hasAutoApply && !hasAutoDelete && !hasAggressiveCutover { + return fmt.Errorf("must specify at least one of --enable-auto-apply, --disable-auto-apply, --auto-delete-branch, --aggressive-cutover, or --auto-apply") } if hasDeprecatedFlag { @@ -111,14 +114,29 @@ must be passed; unset flags are not sent.`, } } + if hasAggressiveCutover { + dr, err = client.DeployRequests.AggressiveCutover(ctx, &planetscale.DeployRequestAggressiveCutoverRequest{ + Organization: ch.Config.Organization, + Database: database, + Number: n, + Enable: flags.aggressive_cutover, + }) + if err != nil { + return handleDRErr(err) + } + } + if ch.Printer.Format() == printer.Human { - updated := make([]string, 0, 2) + updated := make([]string, 0, 3) if hasAutoApply { updated = append(updated, "auto-apply") } if hasAutoDelete { updated = append(updated, "auto-delete-branch") } + if hasAggressiveCutover { + updated = append(updated, "aggressive-cutover") + } ch.Printer.Printf("Successfully updated %s for '%s/%s'.\n", strings.Join(updated, " and "), printer.BoldBlue(database), @@ -133,6 +151,7 @@ must be passed; unset flags are not sent.`, 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().BoolVar(&flags.aggressive_cutover, "aggressive-cutover", false, "Cut this deploy request over aggressively: Vitess kills blocking queries and transactions on the first cutover attempt instead of retrying for up to an hour. Does not change the database setting. Other deploy requests are unaffected. Pass --aggressive-cutover=false to clear the opt-in.") 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") diff --git a/internal/mock/dr.go b/internal/mock/dr.go index 2c658ce0..e69365f9 100644 --- a/internal/mock/dr.go +++ b/internal/mock/dr.go @@ -22,6 +22,9 @@ type DeployRequestsService struct { AutoDeleteBranchFn func(context.Context, *ps.AutoDeleteBranchRequest) (*ps.DeployRequest, error) AutoDeleteBranchFnInvoked bool + AggressiveCutoverFn func(context.Context, *ps.DeployRequestAggressiveCutoverRequest) (*ps.DeployRequest, error) + AggressiveCutoverFnInvoked bool + CancelFn func(context.Context, *ps.CancelDeployRequestRequest) (*ps.DeployRequest, error) CancelFnInvoked bool @@ -105,6 +108,11 @@ func (d *DeployRequestsService) AutoDeleteBranch(ctx context.Context, req *ps.Au return d.AutoDeleteBranchFn(ctx, req) } +func (d *DeployRequestsService) AggressiveCutover(ctx context.Context, req *ps.DeployRequestAggressiveCutoverRequest) (*ps.DeployRequest, error) { + d.AggressiveCutoverFnInvoked = true + return d.AggressiveCutoverFn(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 15f71324..8cc3434c 100644 --- a/internal/planetscale/deploy_requests.go +++ b/internal/planetscale/deploy_requests.go @@ -21,6 +21,7 @@ type DeployRequestsService interface { ApplyDeploy(context.Context, *ApplyDeployRequestRequest) (*DeployRequest, error) AutoApplyDeploy(context.Context, *AutoApplyDeployRequestRequest) (*DeployRequest, error) AutoDeleteBranch(context.Context, *AutoDeleteBranchRequest) (*DeployRequest, error) + AggressiveCutover(context.Context, *DeployRequestAggressiveCutoverRequest) (*DeployRequest, error) CancelDeploy(context.Context, *CancelDeployRequestRequest) (*DeployRequest, error) CloseDeploy(context.Context, *CloseDeployRequestRequest) (*DeployRequest, error) Create(context.Context, *CreateDeployRequestRequest) (*DeployRequest, error) @@ -146,6 +147,7 @@ type Deployment struct { AutoCutover bool `json:"auto_cutover"` AutoDeleteBranch bool `json:"auto_delete_branch"` + AggressiveCutover bool `json:"aggressive_cutover"` CutoverExpiring bool `json:"cutover_expiring"` TableLocked bool `json:"table_locked"` QueuePaused bool `json:"queue_paused"` @@ -315,6 +317,15 @@ type AutoDeleteBranchRequest struct { Enable bool `json:"-"` } +// DeployRequestAggressiveCutoverRequest sets or clears the per-request aggressive cutover opt-in. +// Distinct from the database-level AggressiveCutoverRequest. +type DeployRequestAggressiveCutoverRequest struct { + Organization string `json:"-"` + Database string `json:"-"` + Number uint64 `json:"-"` + Enable bool `json:"-"` +} + type CancelDeployRequestRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -329,13 +340,14 @@ type RetryFailedOperationsRequest struct { } type CreateDeployRequestRequest struct { - Organization string `json:"-"` - Database string `json:"-"` - Branch string `json:"branch"` - IntoBranch string `json:"into_branch,omitempty"` - Notes string `json:"notes"` - AutoCutover bool `json:"auto_cutover,omitempty"` - AutoDeleteBranch bool `json:"auto_delete_branch,omitempty"` + Organization string `json:"-"` + Database string `json:"-"` + Branch string `json:"branch"` + IntoBranch string `json:"into_branch,omitempty"` + Notes string `json:"notes"` + AutoCutover bool `json:"auto_cutover,omitempty"` + AutoDeleteBranch bool `json:"auto_delete_branch,omitempty"` + AggressiveCutover bool `json:"aggressive_cutover,omitempty"` } type SkipRevertDeployRequestRequest struct { @@ -590,6 +602,29 @@ func (d *deployRequestsService) AutoDeleteBranch(ctx context.Context, autoDelete return drr, nil } +// AggressiveCutover sets whether this deploy request cuts over aggressively. +// enable false clears the opt-in; it does not change the database setting. +func (d *deployRequestsService) AggressiveCutover(ctx context.Context, cutoverReq *DeployRequestAggressiveCutoverRequest) (*DeployRequest, error) { + reqBody := struct { + Enable bool `json:"enable"` + }{ + Enable: cutoverReq.Enable, + } + + path := deployRequestActionAPIPath(cutoverReq.Organization, cutoverReq.Database, cutoverReq.Number, "aggressive-cutover") + 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 171cc8ed..deaf1d70 100644 --- a/internal/planetscale/deploy_requests_test.go +++ b/internal/planetscale/deploy_requests_test.go @@ -515,6 +515,96 @@ func TestDeployRequests_Create(t *testing.T) { c.Assert(requests, qt.DeepEquals, want) } +func TestDeployRequests_CreateOmitsAggressiveCutoverUnlessSet(t *testing.T) { + c := qt.New(t) + + var gotBody string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, qt.Equals, http.MethodPost) + c.Assert(r.URL.Path, qt.Equals, "/v1/organizations/test-organization/databases/test-database/deploy-requests") + body, err := io.ReadAll(r.Body) + c.Assert(err, qt.IsNil) + gotBody = string(body) + w.WriteHeader(201) + _, err = w.Write([]byte(`{"id":"test-deploy-request-id","number":1,"branch":"development"}`)) + c.Assert(err, qt.IsNil) + })) + + client, err := NewClient(WithBaseURL(ts.URL)) + c.Assert(err, qt.IsNil) + + _, err = client.DeployRequests.Create(context.Background(), &CreateDeployRequestRequest{ + Organization: "test-organization", + Database: "test-database", + Branch: "development", + AggressiveCutover: false, + }) + c.Assert(err, qt.IsNil) + c.Assert(strings.Contains(gotBody, "aggressive_cutover"), qt.IsFalse) + + gotBody = "" + _, err = client.DeployRequests.Create(context.Background(), &CreateDeployRequestRequest{ + Organization: "test-organization", + Database: "test-database", + Branch: "development", + AggressiveCutover: true, + }) + c.Assert(err, qt.IsNil) + + var sent struct { + AggressiveCutover *bool `json:"aggressive_cutover"` + } + c.Assert(json.Unmarshal([]byte(gotBody), &sent), qt.IsNil) + c.Assert(sent.AggressiveCutover, qt.IsNotNil) + c.Assert(*sent.AggressiveCutover, qt.IsTrue) +} + +func TestDeployRequests_AggressiveCutover(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/1284/aggressive-cutover") + var body struct { + Enable bool `json:"enable"` + } + c.Assert(json.NewDecoder(r.Body).Decode(&body), qt.IsNil) + c.Assert(body.Enable, qt.IsFalse) + w.WriteHeader(200) + out := `{"id": "test-deploy-request-id", "branch": "urgent-branch", "into_branch": "main", "notes": "", "created_at": "2021-01-14T10:19:23.000Z", "updated_at": "2021-01-14T10:19:23.000Z", "closed_at": null, "deployment": { "state": "ready", "aggressive_cutover": false }, "number": 1284}` + _, 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.AggressiveCutover(context.Background(), &DeployRequestAggressiveCutoverRequest{ + Organization: "test-organization", + Database: "test-database", + Number: 1284, + Enable: false, + }) + + testTime := time.Date(2021, time.January, 14, 10, 19, 23, 0, time.UTC) + want := &DeployRequest{ + ID: "test-deploy-request-id", + Branch: "urgent-branch", + Deployment: &Deployment{ + State: "ready", + AggressiveCutover: false, + }, + IntoBranch: "main", + Number: 1284, + Notes: "", + CreatedAt: testTime, + UpdatedAt: testTime, + } + + c.Assert(err, qt.IsNil) + c.Assert(dr, qt.DeepEquals, want) +} + func TestDeployRequests_Review(t *testing.T) { c := qt.New(t) @@ -821,6 +911,7 @@ func TestDeployRequests_GetDeployment(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(deployment.ID, qt.Equals, "dep-1") c.Assert(deployment.AutoDeleteBranch, qt.IsTrue) + c.Assert(deployment.AggressiveCutover, qt.IsFalse) } func TestDeployRequests_ListReviews(t *testing.T) {