Skip to content

Commit 1cf44fc

Browse files
authored
feat: support branch pause and unpause (#3979)
* feat: support branch pause and unpause * chore: drop experimental flag for branches command * chore: tidy up branch get command * chore: address linter error * chore: suggest creating first branch * fix: missing database in non-pooling url
1 parent a9cd836 commit 1cf44fc

File tree

8 files changed

+151
-36
lines changed

8 files changed

+151
-36
lines changed

cmd/branches.go

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@ import (
66
"os"
77

88
"github.com/go-errors/errors"
9-
"github.com/google/uuid"
109
"github.com/spf13/afero"
1110
"github.com/spf13/cobra"
1211
"github.com/supabase/cli/internal/branches/create"
1312
"github.com/supabase/cli/internal/branches/delete"
1413
"github.com/supabase/cli/internal/branches/disable"
1514
"github.com/supabase/cli/internal/branches/get"
1615
"github.com/supabase/cli/internal/branches/list"
16+
"github.com/supabase/cli/internal/branches/pause"
17+
"github.com/supabase/cli/internal/branches/unpause"
1718
"github.com/supabase/cli/internal/branches/update"
1819
"github.com/supabase/cli/internal/gen/keys"
1920
"github.com/supabase/cli/internal/utils"
2021
"github.com/supabase/cli/internal/utils/flags"
2122
"github.com/supabase/cli/pkg/api"
23+
"github.com/supabase/cli/pkg/cast"
2224
)
2325

2426
var (
@@ -40,7 +42,7 @@ var (
4042
Long: "Create a preview branch for the linked project.",
4143
Args: cobra.MaximumNArgs(1),
4244
RunE: func(cmd *cobra.Command, args []string) error {
43-
var body api.CreateBranchBody
45+
body := api.CreateBranchBody{IsDefault: cast.Ptr(false)}
4446
if len(args) > 0 {
4547
body.BranchName = args[0]
4648
}
@@ -74,14 +76,16 @@ var (
7476
branchId string
7577

7678
branchGetCmd = &cobra.Command{
77-
Use: "get [branch-id]",
79+
Use: "get [name]",
7880
Short: "Retrieve details of a preview branch",
7981
Long: "Retrieve details of the specified preview branch.",
8082
Args: cobra.MaximumNArgs(1),
8183
RunE: func(cmd *cobra.Command, args []string) error {
8284
ctx := cmd.Context()
8385
fsys := afero.NewOsFs()
84-
if err := promptBranchId(ctx, args, fsys); err != nil {
86+
if len(args) > 0 {
87+
branchId = args[0]
88+
} else if err := promptBranchId(ctx, fsys); err != nil {
8589
return err
8690
}
8791
return get.Run(ctx, branchId, fsys)
@@ -101,7 +105,7 @@ var (
101105
gitBranch string
102106

103107
branchUpdateCmd = &cobra.Command{
104-
Use: "update [branch-id]",
108+
Use: "update [name]",
105109
Short: "Update a preview branch",
106110
Long: "Update a preview branch by its name or ID.",
107111
Args: cobra.MaximumNArgs(1),
@@ -122,32 +126,69 @@ var (
122126
}
123127
ctx := cmd.Context()
124128
fsys := afero.NewOsFs()
125-
if err := promptBranchId(ctx, args, fsys); err != nil {
129+
if len(args) > 0 {
130+
branchId = args[0]
131+
} else if err := promptBranchId(ctx, fsys); err != nil {
126132
return err
127133
}
128134
return update.Run(cmd.Context(), branchId, body, fsys)
129135
},
130136
}
131137

138+
branchPauseCmd = &cobra.Command{
139+
Use: "pause [name]",
140+
Short: "Pause a preview branch",
141+
Args: cobra.MaximumNArgs(1),
142+
RunE: func(cmd *cobra.Command, args []string) error {
143+
ctx := cmd.Context()
144+
fsys := afero.NewOsFs()
145+
if len(args) > 0 {
146+
branchId = args[0]
147+
} else if err := promptBranchId(ctx, fsys); err != nil {
148+
return err
149+
}
150+
return pause.Run(ctx, branchId)
151+
},
152+
}
153+
154+
branchUnpauseCmd = &cobra.Command{
155+
Use: "unpause [name]",
156+
Short: "Unpause a preview branch",
157+
Args: cobra.MaximumNArgs(1),
158+
RunE: func(cmd *cobra.Command, args []string) error {
159+
ctx := cmd.Context()
160+
fsys := afero.NewOsFs()
161+
if len(args) > 0 {
162+
branchId = args[0]
163+
} else if err := promptBranchId(ctx, fsys); err != nil {
164+
return err
165+
}
166+
return unpause.Run(ctx, branchId)
167+
},
168+
}
169+
132170
branchDeleteCmd = &cobra.Command{
133-
Use: "delete [branch-id]",
171+
Use: "delete [name]",
134172
Short: "Delete a preview branch",
135173
Long: "Delete a preview branch by its name or ID.",
136174
Args: cobra.MaximumNArgs(1),
137175
RunE: func(cmd *cobra.Command, args []string) error {
138176
ctx := cmd.Context()
139177
fsys := afero.NewOsFs()
140-
if err := promptBranchId(ctx, args, fsys); err != nil {
178+
if len(args) > 0 {
179+
branchId = args[0]
180+
} else if err := promptBranchId(ctx, fsys); err != nil {
141181
return err
142182
}
143183
return delete.Run(ctx, branchId)
144184
},
145185
}
146186

147187
branchDisableCmd = &cobra.Command{
148-
Use: "disable",
149-
Short: "Disable preview branching",
150-
Long: "Disable preview branching for the linked project.",
188+
Hidden: true,
189+
Use: "disable",
190+
Short: "Disable preview branching",
191+
Long: "Disable preview branching for the linked project.",
151192
RunE: func(cmd *cobra.Command, args []string) error {
152193
return disable.Run(cmd.Context(), afero.NewOsFs())
153194
},
@@ -173,18 +214,13 @@ func init() {
173214
branchesCmd.AddCommand(branchUpdateCmd)
174215
branchesCmd.AddCommand(branchDeleteCmd)
175216
branchesCmd.AddCommand(branchDisableCmd)
217+
branchesCmd.AddCommand(branchPauseCmd)
218+
branchesCmd.AddCommand(branchUnpauseCmd)
176219
rootCmd.AddCommand(branchesCmd)
177220
}
178221

179-
func promptBranchId(ctx context.Context, args []string, fsys afero.Fs) error {
180-
var filter []list.BranchFilter
181-
if len(args) > 0 {
182-
if branchId = args[0]; uuid.Validate(branchId) == nil {
183-
return nil
184-
}
185-
// Try resolving as branch name
186-
filter = append(filter, list.FilterByName(branchId))
187-
} else if console := utils.NewConsole(); !console.IsTTY {
222+
func promptBranchId(ctx context.Context, fsys afero.Fs) error {
223+
if console := utils.NewConsole(); !console.IsTTY {
188224
// Only read from stdin if the terminal is non-interactive
189225
title := "Enter the name of your branch"
190226
if branchId = keys.GetGitBranch(fsys); len(branchId) > 0 {
@@ -199,16 +235,14 @@ func promptBranchId(ctx context.Context, args []string, fsys afero.Fs) error {
199235
if len(branchId) == 0 {
200236
return errors.New("branch name cannot be empty")
201237
}
202-
filter = append(filter, list.FilterByName(branchId))
238+
return nil
203239
}
204-
branches, err := list.ListBranch(ctx, flags.ProjectRef, filter...)
240+
branches, err := list.ListBranch(ctx, flags.ProjectRef)
205241
if err != nil {
206242
return err
207243
} else if len(branches) == 0 {
208-
return errors.Errorf("branch not found: %s", branchId)
209-
} else if len(branches) == 1 {
210-
branchId = branches[0].Id.String()
211-
return nil
244+
utils.CmdSuggestion = fmt.Sprintf("Create your first branch with: %s", utils.Aqua("supabase branches create"))
245+
return errors.Errorf("branching is disabled")
212246
}
213247
// Let user choose from a list of branches
214248
items := make([]utils.PromptItem, len(branches))

cmd/projects.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ var (
105105
}
106106

107107
projectsDeleteCmd = &cobra.Command{
108-
Use: "delete <ref>",
108+
Use: "delete [ref]",
109109
Short: "Delete a Supabase project",
110110
Args: cobra.MaximumNArgs(1),
111111
PreRunE: func(cmd *cobra.Command, args []string) error {

cmd/root.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ var experimental = []*cobra.Command{
5858
sslEnforcementCmd,
5959
genKeysCmd,
6060
postgresCmd,
61-
branchesCmd,
6261
storageCmd,
6362
}
6463

internal/branches/delete/delete.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import (
66
"net/http"
77

88
"github.com/go-errors/errors"
9-
"github.com/google/uuid"
9+
"github.com/supabase/cli/internal/branches/get"
1010
"github.com/supabase/cli/internal/utils"
1111
)
1212

1313
func Run(ctx context.Context, branchId string) error {
14-
parsed, err := uuid.Parse(branchId)
14+
parsed, err := get.GetBranchID(ctx, branchId)
1515
if err != nil {
16-
return errors.Errorf("failed to parse branch ID: %w", err)
16+
return err
1717
}
1818
resp, err := utils.GetSupabase().V1DeleteABranchWithResponse(ctx, parsed)
1919
if err != nil {

internal/branches/get/get.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/supabase/cli/internal/migration/list"
1313
"github.com/supabase/cli/internal/projects/apiKeys"
1414
"github.com/supabase/cli/internal/utils"
15+
"github.com/supabase/cli/internal/utils/flags"
1516
"github.com/supabase/cli/pkg/api"
1617
"github.com/supabase/cli/pkg/cast"
1718
)
@@ -53,9 +54,9 @@ func Run(ctx context.Context, branchId string, fsys afero.Fs) error {
5354

5455
func getBranchDetail(ctx context.Context, branchId string) (api.BranchDetailResponse, error) {
5556
var result api.BranchDetailResponse
56-
parsed, err := uuid.Parse(branchId)
57+
parsed, err := GetBranchID(ctx, branchId)
5758
if err != nil {
58-
return result, errors.Errorf("failed to parse branch ID: %w", err)
59+
return result, err
5960
}
6061
resp, err := utils.GetSupabase().V1GetABranchConfigWithResponse(ctx, parsed)
6162
if err != nil {
@@ -76,6 +77,20 @@ func getBranchDetail(ctx context.Context, branchId string) (api.BranchDetailResp
7677
return *resp.JSON200, nil
7778
}
7879

80+
func GetBranchID(ctx context.Context, branchId string) (uuid.UUID, error) {
81+
parsed, err := uuid.Parse(branchId)
82+
if err == nil {
83+
return parsed, nil
84+
}
85+
resp, err := utils.GetSupabase().V1GetABranchWithResponse(ctx, flags.ProjectRef, branchId)
86+
if err != nil {
87+
return parsed, errors.Errorf("failed to get branch: %w", err)
88+
} else if resp.JSON200 == nil {
89+
return parsed, errors.Errorf("unexpected get branch status %d: %s", resp.StatusCode(), string(resp.Body))
90+
}
91+
return resp.JSON200.Id, nil
92+
}
93+
7994
func getPoolerConfig(ctx context.Context, ref string) (api.SupavisorConfigResponse, error) {
8095
var result api.SupavisorConfigResponse
8196
resp, err := utils.GetSupabase().V1GetPoolerConfigWithResponse(ctx, ref)
@@ -98,6 +113,7 @@ func toStandardEnvs(detail api.BranchDetailResponse, pooler api.SupavisorConfigR
98113
Port: cast.UIntToUInt16(cast.IntToUint(detail.DbPort)),
99114
User: *detail.DbUser,
100115
Password: *detail.DbPass,
116+
Database: "postgres",
101117
}
102118
config, err := utils.ParsePoolerURL(pooler.ConnectionString)
103119
if err != nil {

internal/branches/pause/pause.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package pause
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/go-errors/errors"
8+
"github.com/google/uuid"
9+
"github.com/supabase/cli/internal/utils"
10+
"github.com/supabase/cli/internal/utils/flags"
11+
)
12+
13+
func Run(ctx context.Context, branchId string) error {
14+
projectRef, err := GetBranchProjectRef(ctx, branchId)
15+
if err != nil {
16+
return err
17+
}
18+
if resp, err := utils.GetSupabase().V1PauseAProjectWithResponse(ctx, projectRef); err != nil {
19+
return errors.Errorf("failed to pause branch: %w", err)
20+
} else if resp.StatusCode() != http.StatusOK {
21+
return errors.Errorf("unexpected pause branch status %d: %s", resp.StatusCode(), string(resp.Body))
22+
}
23+
return nil
24+
}
25+
26+
func GetBranchProjectRef(ctx context.Context, branchId string) (string, error) {
27+
if parsed, err := uuid.Parse(branchId); err == nil {
28+
resp, err := utils.GetSupabase().V1GetABranchConfigWithResponse(ctx, parsed)
29+
if err != nil {
30+
return "", errors.Errorf("failed to get branch: %w", err)
31+
} else if resp.JSON200 == nil {
32+
return "", errors.Errorf("unexpected get branch status %d: %s", resp.StatusCode(), string(resp.Body))
33+
}
34+
return resp.JSON200.Ref, nil
35+
}
36+
resp, err := utils.GetSupabase().V1GetABranchWithResponse(ctx, flags.ProjectRef, branchId)
37+
if err != nil {
38+
return "", errors.Errorf("failed to get branch: %w", err)
39+
} else if resp.JSON200 == nil {
40+
return "", errors.Errorf("unexpected get branch status %d: %s", resp.StatusCode(), string(resp.Body))
41+
}
42+
return resp.JSON200.ProjectRef, nil
43+
}

internal/branches/unpause/unpause.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package unpause
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/go-errors/errors"
8+
"github.com/supabase/cli/internal/branches/pause"
9+
"github.com/supabase/cli/internal/utils"
10+
)
11+
12+
func Run(ctx context.Context, branchId string) error {
13+
projectRef, err := pause.GetBranchProjectRef(ctx, branchId)
14+
if err != nil {
15+
return err
16+
}
17+
if resp, err := utils.GetSupabase().V1RestoreAProjectWithResponse(ctx, projectRef); err != nil {
18+
return errors.Errorf("failed to unpause branch: %w", err)
19+
} else if resp.StatusCode() != http.StatusOK {
20+
return errors.Errorf("unexpected unpause branch status %d: %s", resp.StatusCode(), string(resp.Body))
21+
}
22+
return nil
23+
}

internal/branches/update/update.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import (
55
"fmt"
66

77
"github.com/go-errors/errors"
8-
"github.com/google/uuid"
98
"github.com/spf13/afero"
9+
"github.com/supabase/cli/internal/branches/get"
1010
"github.com/supabase/cli/internal/utils"
1111
"github.com/supabase/cli/pkg/api"
1212
)
1313

1414
func Run(ctx context.Context, branchId string, body api.UpdateBranchBody, fsys afero.Fs) error {
15-
parsed, err := uuid.Parse(branchId)
15+
parsed, err := get.GetBranchID(ctx, branchId)
1616
if err != nil {
17-
return errors.Errorf("failed to parse branch ID: %w", err)
17+
return err
1818
}
1919
resp, err := utils.GetSupabase().V1UpdateABranchConfigWithResponse(ctx, parsed, body)
2020
if err != nil {

0 commit comments

Comments
 (0)