Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ pscale database throttler update <database> --org <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 <database> --org <org> --format json
Expand All @@ -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 <database> --org <org> --format json # database deploy queue (first page)
Expand All @@ -447,8 +447,13 @@ Alias: `pscale dr …` works the same. Vitess only. `--ratio` is 0–95 (0 disab
```bash
pscale deploy-request update <database> <number> --org <org> --format json --enable-auto-apply
pscale deploy-request update <database> <number> --org <org> --format json --auto-delete-branch=false
pscale deploy-request create <database> <branch> --org <org> --aggressive-cutover --format json
pscale deploy-request update <database> <number> --org <org> --format json --aggressive-cutover
pscale deploy-request update <database> <number> --org <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": <bool> }` 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
Expand Down
6 changes: 6 additions & 0 deletions internal/cmd/deployrequest/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand All @@ -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.")

Expand Down
56 changes: 56 additions & 0 deletions internal/cmd/deployrequest/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/deployrequest/dr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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),
Expand Down
34 changes: 34 additions & 0 deletions internal/cmd/deployrequest/edge_ops_read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
89 changes: 88 additions & 1 deletion internal/cmd/deployrequest/edit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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")
}
2 changes: 2 additions & 0 deletions internal/cmd/deployrequest/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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),
Expand Down
35 changes: 35 additions & 0 deletions internal/cmd/deployrequest/show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading
Loading