diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index ee7c3e5ca..b89594f8a 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -41,7 +41,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: installer @@ -57,7 +57,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: installer @@ -73,7 +73,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: installer @@ -96,7 +96,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: installer @@ -115,7 +115,7 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v5 with: name: installer diff --git a/.github/workflows/mirror-image.yml b/.github/workflows/mirror-image.yml index 82f1bc395..1623f296d 100644 --- a/.github/workflows/mirror-image.yml +++ b/.github/workflows/mirror-image.yml @@ -29,7 +29,7 @@ jobs: TAG=${{ inputs.image }} echo "image=${TAG##*/}" >> $GITHUB_OUTPUT - name: configure aws credentials - uses: aws-actions/configure-aws-credentials@v4.2.1 + uses: aws-actions/configure-aws-credentials@v4.3.1 with: role-to-assume: ${{ secrets.PROD_AWS_ROLE }} aws-region: us-east-1 diff --git a/cmd/branches.go b/cmd/branches.go index a9a8ce392..c949c8858 100644 --- a/cmd/branches.go +++ b/cmd/branches.go @@ -6,7 +6,6 @@ import ( "os" "github.com/go-errors/errors" - "github.com/google/uuid" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/supabase/cli/internal/branches/create" @@ -14,11 +13,14 @@ import ( "github.com/supabase/cli/internal/branches/disable" "github.com/supabase/cli/internal/branches/get" "github.com/supabase/cli/internal/branches/list" + "github.com/supabase/cli/internal/branches/pause" + "github.com/supabase/cli/internal/branches/unpause" "github.com/supabase/cli/internal/branches/update" "github.com/supabase/cli/internal/gen/keys" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/cast" ) var ( @@ -40,7 +42,7 @@ var ( Long: "Create a preview branch for the linked project.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - var body api.CreateBranchBody + body := api.CreateBranchBody{IsDefault: cast.Ptr(false)} if len(args) > 0 { body.BranchName = args[0] } @@ -74,14 +76,16 @@ var ( branchId string branchGetCmd = &cobra.Command{ - Use: "get [branch-id]", + Use: "get [name]", Short: "Retrieve details of a preview branch", Long: "Retrieve details of the specified preview branch.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() fsys := afero.NewOsFs() - if err := promptBranchId(ctx, args, fsys); err != nil { + if len(args) > 0 { + branchId = args[0] + } else if err := promptBranchId(ctx, fsys); err != nil { return err } return get.Run(ctx, branchId, fsys) @@ -101,7 +105,7 @@ var ( gitBranch string branchUpdateCmd = &cobra.Command{ - Use: "update [branch-id]", + Use: "update [name]", Short: "Update a preview branch", Long: "Update a preview branch by its name or ID.", Args: cobra.MaximumNArgs(1), @@ -122,22 +126,58 @@ var ( } ctx := cmd.Context() fsys := afero.NewOsFs() - if err := promptBranchId(ctx, args, fsys); err != nil { + if len(args) > 0 { + branchId = args[0] + } else if err := promptBranchId(ctx, fsys); err != nil { return err } return update.Run(cmd.Context(), branchId, body, fsys) }, } + branchPauseCmd = &cobra.Command{ + Use: "pause [name]", + Short: "Pause a preview branch", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + fsys := afero.NewOsFs() + if len(args) > 0 { + branchId = args[0] + } else if err := promptBranchId(ctx, fsys); err != nil { + return err + } + return pause.Run(ctx, branchId) + }, + } + + branchUnpauseCmd = &cobra.Command{ + Use: "unpause [name]", + Short: "Unpause a preview branch", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + fsys := afero.NewOsFs() + if len(args) > 0 { + branchId = args[0] + } else if err := promptBranchId(ctx, fsys); err != nil { + return err + } + return unpause.Run(ctx, branchId) + }, + } + branchDeleteCmd = &cobra.Command{ - Use: "delete [branch-id]", + Use: "delete [name]", Short: "Delete a preview branch", Long: "Delete a preview branch by its name or ID.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() fsys := afero.NewOsFs() - if err := promptBranchId(ctx, args, fsys); err != nil { + if len(args) > 0 { + branchId = args[0] + } else if err := promptBranchId(ctx, fsys); err != nil { return err } return delete.Run(ctx, branchId) @@ -145,9 +185,10 @@ var ( } branchDisableCmd = &cobra.Command{ - Use: "disable", - Short: "Disable preview branching", - Long: "Disable preview branching for the linked project.", + Hidden: true, + Use: "disable", + Short: "Disable preview branching", + Long: "Disable preview branching for the linked project.", RunE: func(cmd *cobra.Command, args []string) error { return disable.Run(cmd.Context(), afero.NewOsFs()) }, @@ -173,18 +214,13 @@ func init() { branchesCmd.AddCommand(branchUpdateCmd) branchesCmd.AddCommand(branchDeleteCmd) branchesCmd.AddCommand(branchDisableCmd) + branchesCmd.AddCommand(branchPauseCmd) + branchesCmd.AddCommand(branchUnpauseCmd) rootCmd.AddCommand(branchesCmd) } -func promptBranchId(ctx context.Context, args []string, fsys afero.Fs) error { - var filter []list.BranchFilter - if len(args) > 0 { - if branchId = args[0]; uuid.Validate(branchId) == nil { - return nil - } - // Try resolving as branch name - filter = append(filter, list.FilterByName(branchId)) - } else if console := utils.NewConsole(); !console.IsTTY { +func promptBranchId(ctx context.Context, fsys afero.Fs) error { + if console := utils.NewConsole(); !console.IsTTY { // Only read from stdin if the terminal is non-interactive title := "Enter the name of your branch" if branchId = keys.GetGitBranch(fsys); len(branchId) > 0 { @@ -199,16 +235,14 @@ func promptBranchId(ctx context.Context, args []string, fsys afero.Fs) error { if len(branchId) == 0 { return errors.New("branch name cannot be empty") } - filter = append(filter, list.FilterByName(branchId)) + return nil } - branches, err := list.ListBranch(ctx, flags.ProjectRef, filter...) + branches, err := list.ListBranch(ctx, flags.ProjectRef) if err != nil { return err } else if len(branches) == 0 { - return errors.Errorf("branch not found: %s", branchId) - } else if len(branches) == 1 { - branchId = branches[0].Id.String() - return nil + utils.CmdSuggestion = fmt.Sprintf("Create your first branch with: %s", utils.Aqua("supabase branches create")) + return errors.Errorf("branching is disabled") } // Let user choose from a list of branches items := make([]utils.PromptItem, len(branches)) diff --git a/cmd/db.go b/cmd/db.go index 08906fd5f..ae275e3ab 100644 --- a/cmd/db.go +++ b/cmd/db.go @@ -100,7 +100,7 @@ var ( differ := diff.DiffSchemaMigra if usePgSchema { differ = diff.DiffPgSchema - fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as RLS policies, enums, and grants.") + fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as views and grants.") } return diff.Run(cmd.Context(), schema, file, flags.DbConfig, differ, afero.NewOsFs()) }, diff --git a/cmd/projects.go b/cmd/projects.go index 9e0353265..1a60b6170 100644 --- a/cmd/projects.go +++ b/cmd/projects.go @@ -105,7 +105,7 @@ var ( } projectsDeleteCmd = &cobra.Command{ - Use: "delete ", + Use: "delete [ref]", Short: "Delete a Supabase project", Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/root.go b/cmd/root.go index 24aad18d3..99b11d23d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -58,7 +58,6 @@ var experimental = []*cobra.Command{ sslEnforcementCmd, genKeysCmd, postgresCmd, - branchesCmd, storageCmd, } diff --git a/go.mod b/go.mod index 033126a15..742d09f32 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,10 @@ require ( github.com/charmbracelet/glamour v0.9.1 github.com/charmbracelet/lipgloss v1.1.0 github.com/containerd/errdefs v1.0.0 - github.com/containers/common v0.64.0 + github.com/containers/common v0.64.1 github.com/docker/cli v28.3.3+incompatible github.com/docker/docker v28.3.3+incompatible - github.com/docker/go-connections v0.5.0 + github.com/docker/go-connections v0.6.0 github.com/fsnotify/fsnotify v1.9.0 github.com/getsentry/sentry-go v0.35.0 github.com/go-errors/errors v1.5.1 @@ -45,9 +45,9 @@ require ( github.com/withfig/autocomplete-tools/packages/cobra v1.2.0 github.com/zalando/go-keyring v0.2.6 go.opentelemetry.io/otel v1.37.0 - golang.org/x/mod v0.26.0 + golang.org/x/mod v0.27.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/term v0.33.0 + golang.org/x/term v0.34.0 google.golang.org/grpc v1.74.2 gopkg.in/yaml.v3 v3.0.1 ) @@ -104,7 +104,7 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containers/storage v1.59.0 // indirect + github.com/containers/storage v1.59.1 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/daixiang0/gci v0.13.6 // indirect @@ -332,11 +332,13 @@ require ( go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.40.0 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect + golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/protobuf v1.36.6 // indirect diff --git a/go.sum b/go.sum index eff73ce59..0e33afdb9 100644 --- a/go.sum +++ b/go.sum @@ -199,10 +199,10 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containers/common v0.64.0 h1:Jdjq1e5tqrLov9tcAVc/AfvQCgX4krhcfDBgOXwrSfw= -github.com/containers/common v0.64.0/go.mod h1:bq2UIiFP8vUJdgM+WN8E8jkD7wF69SpDRGzU7epJljg= -github.com/containers/storage v1.59.0 h1:r2pYSTzQpJTROZbjJQ54Z0GT+rUC6+wHzlSY8yPjsXk= -github.com/containers/storage v1.59.0/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= +github.com/containers/common v0.64.1 h1:E8vSiL+B84/UCsyVSb70GoxY9cu+0bseLujm4EKF6GE= +github.com/containers/common v0.64.1/go.mod h1:CtfQNHoCAZqWeXMwdShcsxmMJSeGRgKKMqAwRKmWrHE= +github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= +github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -249,8 +249,8 @@ github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= @@ -1155,8 +1155,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1204,8 +1204,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1297,8 +1297,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1309,8 +1309,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1396,8 +1396,12 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 70aa6305b..b2986656a 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -9,7 +9,6 @@ import ( "os" "path/filepath" "strings" - "time" "github.com/cenkalti/backoff/v4" "github.com/go-errors/errors" @@ -88,12 +87,12 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options .. } // 3. Get api keys var keys []api.ApiKeyResponse - policy := newBackoffPolicy(ctx) + policy := utils.NewBackoffPolicy(ctx) if err := backoff.RetryNotify(func() error { fmt.Fprintln(os.Stderr, "Linking project...") keys, err = apiKeys.RunGetApiKeys(ctx, flags.ProjectRef) return err - }, policy, newErrorCallback()); err != nil { + }, policy, utils.NewErrorCallback()); err != nil { return err } // 4. Link project @@ -109,7 +108,7 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options .. if err := backoff.RetryNotify(func() error { fmt.Fprintln(os.Stderr, "Checking project health...") return checkProjectHealth(ctx) - }, policy, newErrorCallback()); err != nil { + }, policy, utils.NewErrorCallback()); err != nil { return err } // 6. Push migrations @@ -120,7 +119,7 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options .. policy.Reset() if err := backoff.RetryNotify(func() error { return push.Run(ctx, false, false, true, true, config, fsys) - }, policy, newErrorCallback()); err != nil { + }, policy, utils.NewErrorCallback()); err != nil { return err } // 7. TODO: deploy functions @@ -171,32 +170,6 @@ func checkProjectHealth(ctx context.Context) error { return nil } -const maxRetries = 8 - -func newBackoffPolicy(ctx context.Context) backoff.BackOffContext { - b := backoff.ExponentialBackOff{ - InitialInterval: 3 * time.Second, - RandomizationFactor: backoff.DefaultRandomizationFactor, - Multiplier: backoff.DefaultMultiplier, - MaxInterval: backoff.DefaultMaxInterval, - MaxElapsedTime: backoff.DefaultMaxElapsedTime, - Stop: backoff.Stop, - Clock: backoff.SystemClock, - } - b.Reset() - return backoff.WithContext(backoff.WithMaxRetries(&b, maxRetries), ctx) -} - -func newErrorCallback() backoff.Notify { - failureCount := 0 - logger := utils.GetDebugLogger() - return func(err error, d time.Duration) { - failureCount += 1 - fmt.Fprintln(logger, err) - fmt.Fprintf(os.Stderr, "Retry (%d/%d): ", failureCount, maxRetries) - } -} - const ( SUPABASE_SERVICE_ROLE_KEY = "SUPABASE_SERVICE_ROLE_KEY" SUPABASE_ANON_KEY = "SUPABASE_ANON_KEY" diff --git a/internal/branches/delete/delete.go b/internal/branches/delete/delete.go index 9c7222c9e..4eae6d5e5 100644 --- a/internal/branches/delete/delete.go +++ b/internal/branches/delete/delete.go @@ -6,14 +6,14 @@ import ( "net/http" "github.com/go-errors/errors" - "github.com/google/uuid" + "github.com/supabase/cli/internal/branches/get" "github.com/supabase/cli/internal/utils" ) func Run(ctx context.Context, branchId string) error { - parsed, err := uuid.Parse(branchId) + parsed, err := get.GetBranchID(ctx, branchId) if err != nil { - return errors.Errorf("failed to parse branch ID: %w", err) + return err } resp, err := utils.GetSupabase().V1DeleteABranchWithResponse(ctx, parsed) if err != nil { diff --git a/internal/branches/get/get.go b/internal/branches/get/get.go index 0a6c93d53..9d8f4a2ee 100644 --- a/internal/branches/get/get.go +++ b/internal/branches/get/get.go @@ -12,6 +12,7 @@ import ( "github.com/supabase/cli/internal/migration/list" "github.com/supabase/cli/internal/projects/apiKeys" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/cast" ) @@ -53,9 +54,9 @@ func Run(ctx context.Context, branchId string, fsys afero.Fs) error { func getBranchDetail(ctx context.Context, branchId string) (api.BranchDetailResponse, error) { var result api.BranchDetailResponse - parsed, err := uuid.Parse(branchId) + parsed, err := GetBranchID(ctx, branchId) if err != nil { - return result, errors.Errorf("failed to parse branch ID: %w", err) + return result, err } resp, err := utils.GetSupabase().V1GetABranchConfigWithResponse(ctx, parsed) if err != nil { @@ -76,6 +77,20 @@ func getBranchDetail(ctx context.Context, branchId string) (api.BranchDetailResp return *resp.JSON200, nil } +func GetBranchID(ctx context.Context, branchId string) (uuid.UUID, error) { + parsed, err := uuid.Parse(branchId) + if err == nil { + return parsed, nil + } + resp, err := utils.GetSupabase().V1GetABranchWithResponse(ctx, flags.ProjectRef, branchId) + if err != nil { + return parsed, errors.Errorf("failed to get branch: %w", err) + } else if resp.JSON200 == nil { + return parsed, errors.Errorf("unexpected get branch status %d: %s", resp.StatusCode(), string(resp.Body)) + } + return resp.JSON200.Id, nil +} + func getPoolerConfig(ctx context.Context, ref string) (api.SupavisorConfigResponse, error) { var result api.SupavisorConfigResponse resp, err := utils.GetSupabase().V1GetPoolerConfigWithResponse(ctx, ref) @@ -98,6 +113,7 @@ func toStandardEnvs(detail api.BranchDetailResponse, pooler api.SupavisorConfigR Port: cast.UIntToUInt16(cast.IntToUint(detail.DbPort)), User: *detail.DbUser, Password: *detail.DbPass, + Database: "postgres", } config, err := utils.ParsePoolerURL(pooler.ConnectionString) if err != nil { diff --git a/internal/branches/pause/pause.go b/internal/branches/pause/pause.go new file mode 100644 index 000000000..02d22bf3d --- /dev/null +++ b/internal/branches/pause/pause.go @@ -0,0 +1,43 @@ +package pause + +import ( + "context" + "net/http" + + "github.com/go-errors/errors" + "github.com/google/uuid" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" +) + +func Run(ctx context.Context, branchId string) error { + projectRef, err := GetBranchProjectRef(ctx, branchId) + if err != nil { + return err + } + if resp, err := utils.GetSupabase().V1PauseAProjectWithResponse(ctx, projectRef); err != nil { + return errors.Errorf("failed to pause branch: %w", err) + } else if resp.StatusCode() != http.StatusOK { + return errors.Errorf("unexpected pause branch status %d: %s", resp.StatusCode(), string(resp.Body)) + } + return nil +} + +func GetBranchProjectRef(ctx context.Context, branchId string) (string, error) { + if parsed, err := uuid.Parse(branchId); err == nil { + resp, err := utils.GetSupabase().V1GetABranchConfigWithResponse(ctx, parsed) + if err != nil { + return "", errors.Errorf("failed to get branch: %w", err) + } else if resp.JSON200 == nil { + return "", errors.Errorf("unexpected get branch status %d: %s", resp.StatusCode(), string(resp.Body)) + } + return resp.JSON200.Ref, nil + } + resp, err := utils.GetSupabase().V1GetABranchWithResponse(ctx, flags.ProjectRef, branchId) + if err != nil { + return "", errors.Errorf("failed to get branch: %w", err) + } else if resp.JSON200 == nil { + return "", errors.Errorf("unexpected get branch status %d: %s", resp.StatusCode(), string(resp.Body)) + } + return resp.JSON200.ProjectRef, nil +} diff --git a/internal/branches/unpause/unpause.go b/internal/branches/unpause/unpause.go new file mode 100644 index 000000000..69806d75f --- /dev/null +++ b/internal/branches/unpause/unpause.go @@ -0,0 +1,23 @@ +package unpause + +import ( + "context" + "net/http" + + "github.com/go-errors/errors" + "github.com/supabase/cli/internal/branches/pause" + "github.com/supabase/cli/internal/utils" +) + +func Run(ctx context.Context, branchId string) error { + projectRef, err := pause.GetBranchProjectRef(ctx, branchId) + if err != nil { + return err + } + if resp, err := utils.GetSupabase().V1RestoreAProjectWithResponse(ctx, projectRef); err != nil { + return errors.Errorf("failed to unpause branch: %w", err) + } else if resp.StatusCode() != http.StatusOK { + return errors.Errorf("unexpected unpause branch status %d: %s", resp.StatusCode(), string(resp.Body)) + } + return nil +} diff --git a/internal/branches/update/update.go b/internal/branches/update/update.go index 5b6957462..91261b174 100644 --- a/internal/branches/update/update.go +++ b/internal/branches/update/update.go @@ -5,16 +5,16 @@ import ( "fmt" "github.com/go-errors/errors" - "github.com/google/uuid" "github.com/spf13/afero" + "github.com/supabase/cli/internal/branches/get" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/api" ) func Run(ctx context.Context, branchId string, body api.UpdateBranchBody, fsys afero.Fs) error { - parsed, err := uuid.Parse(branchId) + parsed, err := get.GetBranchID(ctx, branchId) if err != nil { - return errors.Errorf("failed to parse branch ID: %w", err) + return err } resp, err := utils.GetSupabase().V1UpdateABranchConfigWithResponse(ctx, parsed, body) if err != nil { diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go index 3187fd242..761e94193 100644 --- a/internal/db/diff/diff.go +++ b/internal/db/diff/diff.go @@ -89,16 +89,6 @@ func findDropStatements(out string) []string { return drops } -func loadSchema(ctx context.Context, config pgconn.Config, options ...func(*pgx.ConnConfig)) ([]string, error) { - conn, err := utils.ConnectByConfig(ctx, config, options...) - if err != nil { - return nil, err - } - defer conn.Close(context.Background()) - // RLS policies in auth and storage schemas can be included with -s flag - return migration.ListUserSchemas(ctx, conn) -} - func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) { // Disable background workers in shadow database config := start.NewContainerConfig("-c", "max_worker_processes=0") @@ -178,12 +168,11 @@ func DiffDatabase(ctx context.Context, schema []string, config pgconn.Config, w } } // Load all user defined schemas - if len(schema) == 0 { - if schema, err = loadSchema(ctx, config, options...); err != nil { - return "", err - } + if len(schema) > 0 { + fmt.Fprintln(w, "Diffing schemas:", strings.Join(schema, ",")) + } else { + fmt.Fprintln(w, "Diffing schemas...") } - fmt.Fprintln(w, "Diffing schemas:", strings.Join(schema, ",")) source := utils.ToPostgresURL(shadowConfig) target := utils.ToPostgresURL(config) return differ(ctx, source, target, schema) diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go index 47e2a0d49..22fae53df 100644 --- a/internal/db/diff/diff_test.go +++ b/internal/db/diff/diff_test.go @@ -24,7 +24,6 @@ import ( "github.com/supabase/cli/internal/testing/helper" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/config" "github.com/supabase/cli/pkg/migration" "github.com/supabase/cli/pkg/pgtest" ) @@ -64,7 +63,7 @@ func TestRun(t *testing.T) { require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-storage", "")) apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.Auth.Image), "test-shadow-auth") require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-shadow-auth", "")) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.Images.Migra), "test-migra") + apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), "test-migra") diff := "create table test();" require.NoError(t, apitest.MockDockerLogs(utils.Docker, "test-migra", diff)) // Setup mock postgres @@ -285,7 +284,7 @@ create schema public`) gock.New(utils.Docker.DaemonHost()). Delete("/v" + utils.Docker.ClientVersion() + "/containers/test-shadow-db"). Reply(http.StatusOK) - apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(config.Images.Migra), "test-migra") + apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), "test-migra") gock.New(utils.Docker.DaemonHost()). Get("/v" + utils.Docker.ClientVersion() + "/containers/test-migra/logs"). ReplyError(errors.New("network error")) diff --git a/internal/db/diff/migra.go b/internal/db/diff/migra.go index 6c5cc4188..49ba9c447 100644 --- a/internal/db/diff/migra.go +++ b/internal/db/diff/migra.go @@ -9,15 +9,52 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/go-errors/errors" + "github.com/spf13/viper" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/pkg/config" ) -//go:embed templates/migra.sh -var diffSchemaScript string +var ( + //go:embed templates/migra.sh + diffSchemaScript string + //go:embed templates/migra.ts + diffSchemaTypeScript string + + managedSchemas = []string{ + // Local development + "_analytics", + "_realtime", + "_supavisor", + // Owned by extensions + "cron", + "graphql", + "graphql_public", + "net", + "pgroonga", + "pgtle", + "repack", + "tiger_data", + "vault", + // Deprecated extensions + "pgsodium", + "pgsodium_masks", + "timescaledb_experimental", + "timescaledb_information", + "_timescaledb_cache", + "_timescaledb_catalog", + "_timescaledb_config", + "_timescaledb_debug", + "_timescaledb_functions", + "_timescaledb_internal", + // Managed by Supabase + "pgbouncer", + "supabase_functions", + "supabase_migrations", + } +) // Diffs local database schema against shadow, dumps output to stdout. -func DiffSchemaMigra(ctx context.Context, source, target string, schema []string) (string, error) { +func DiffSchemaMigraBash(ctx context.Context, source, target string, schema []string) (string, error) { env := []string{"SOURCE=" + source, "TARGET=" + target} // Passing in script string means command line args must be set manually, ie. "$@" args := "set -- " + strings.Join(schema, " ") + ";" @@ -42,3 +79,41 @@ func DiffSchemaMigra(ctx context.Context, source, target string, schema []string } return out.String(), nil } + +func DiffSchemaMigra(ctx context.Context, source, target string, schema []string) (string, error) { + env := []string{"SOURCE=" + source, "TARGET=" + target} + if len(schema) > 0 { + env = append(env, "INCLUDED_SCHEMAS="+strings.Join(schema, ",")) + } else { + env = append(env, "EXCLUDED_SCHEMAS="+strings.Join(managedSchemas, ",")) + } + cmd := []string{"edge-runtime", "start", "--main-service=."} + if viper.GetBool("DEBUG") { + cmd = append(cmd, "--verbose") + } + cmdString := strings.Join(cmd, " ") + entrypoint := []string{"sh", "-c", `cat <<'EOF' > index.ts && ` + cmdString + ` +` + diffSchemaTypeScript + ` +EOF +`} + var out, stderr bytes.Buffer + if err := utils.DockerRunOnceWithConfig( + ctx, + container.Config{ + Image: utils.Config.EdgeRuntime.Image, + Env: env, + Entrypoint: entrypoint, + }, + container.HostConfig{ + Binds: []string{utils.EdgeRuntimeId + ":/root/.cache/deno:rw"}, + NetworkMode: network.NetworkHost, + }, + network.NetworkingConfig{}, + "", + &out, + &stderr, + ); err != nil && !strings.HasPrefix(stderr.String(), "main worker has been destroyed") { + return "", errors.Errorf("error diffing schema: %w:\n%s", err, stderr.String()) + } + return out.String(), nil +} diff --git a/internal/db/diff/pgschema.go b/internal/db/diff/pgschema.go index 75ead3b52..8967f7ad1 100644 --- a/internal/db/diff/pgschema.go +++ b/internal/db/diff/pgschema.go @@ -22,12 +22,24 @@ func DiffPgSchema(ctx context.Context, source, target string, schema []string) ( } defer dbDst.Close() // Generate DDL based on schema plan + opts := []pgschema.PlanOpt{pgschema.WithDoNotValidatePlan()} + if len(schema) > 0 { + opts = append(opts, pgschema.WithIncludeSchemas(schema...)) + } else { + opts = append(opts, + pgschema.WithExcludeSchemas(managedSchemas...), + pgschema.WithExcludeSchemas( + "topology", // unsupported due to views + "realtime", // unsupported due to partitioned table + "storage", // unsupported due to unique index + ), + ) + } plan, err := pgschema.Generate( ctx, pgschema.DBSchemaSource(dbSrc), pgschema.DBSchemaSource(dbDst), - pgschema.WithDoNotValidatePlan(), - pgschema.WithIncludeSchemas(schema...), + opts..., ) if err != nil { return "", errors.Errorf("failed to generate plan: %w", err) diff --git a/internal/db/diff/templates/migra.ts b/internal/db/diff/templates/migra.ts new file mode 100644 index 000000000..91d21701f --- /dev/null +++ b/internal/db/diff/templates/migra.ts @@ -0,0 +1,73 @@ +import { createClient } from "npm:@pgkit/client"; +import { Migration } from "npm:@pgkit/migra"; + +const clientBase = createClient(Deno.env.get("SOURCE")); +const clientHead = createClient(Deno.env.get("TARGET")); +const includedSchemas = Deno.env.get("INCLUDED_SCHEMAS")?.split(",") ?? []; +const excludedSchemas = Deno.env.get("EXCLUDED_SCHEMAS")?.split(",") ?? []; + +const managedSchemas = ["auth", "realtime", "storage"]; +const extensionSchemas = [ + "pg_catalog", + "extensions", + "pgmq", + "tiger", + "topology", +]; + +try { + let sql = ""; + for (const schema of includedSchemas) { + const m = await Migration.create(clientBase, clientHead, { + schema, + ignore_extension_versions: true, + }); + m.set_safety(false); + m.add_all_changes(true); + sql += m.sql; + } + if (includedSchemas.length === 0) { + // Migra does not ignore custom types and triggers created by extensions, so we diff + // them separately. This workaround only applies to a known list of managed schemas. + for (const schema of extensionSchemas) { + const e = await Migration.create(clientBase, clientHead, { + schema, + ignore_extension_versions: true, + }); + e.set_safety(false); + e.add(e.changes.schemas({ creations_only: true })); + e.add_extension_changes(); + sql += e.sql; + } + // Diff user defined entities in non-managed schemas, including extensions. + const m = await Migration.create(clientBase, clientHead, { + exclude_schema: [ + ...managedSchemas, + ...extensionSchemas, + ...excludedSchemas, + ], + ignore_extension_versions: true, + }); + m.set_safety(false); + m.add_all_changes(true); + sql += m.sql; + // For managed schemas, we want to include triggers and RLS policies only. + for (const schema of managedSchemas) { + const s = await Migration.create(clientBase, clientHead, { + schema, + ignore_extension_versions: true, + }); + s.set_safety(false); + s.add(s.changes.triggers({ drops_only: true })); + s.add(s.changes.rlspolicies({ drops_only: true })); + s.add(s.changes.rlspolicies({ creations_only: true })); + s.add(s.changes.triggers({ creations_only: true })); + sql += s.sql; + } + } + console.log(sql); +} catch (e) { + console.error(e); +} finally { + await Promise.all([clientHead.end(), clientBase.end()]); +} diff --git a/internal/start/start.go b/internal/start/start.go index f922addf3..7ba53d0e7 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -730,6 +730,14 @@ EOF ctx, container.Config{ Image: utils.Config.Inbucket.Image, + Healthcheck: &container.HealthConfig{ + Test: []string{"CMD", "/mailpit", "readyz"}, + Interval: 10 * time.Second, + Timeout: 2 * time.Second, + Retries: 3, + // StartPeriod taken from upstream Dockerfile + StartPeriod: 10 * time.Second, + }, }, container.HostConfig{ PortBindings: inbucketPortBindings, diff --git a/internal/utils/flags/db_url.go b/internal/utils/flags/db_url.go index 0b666d6e1..027e3ec3a 100644 --- a/internal/utils/flags/db_url.go +++ b/internal/utils/flags/db_url.go @@ -12,6 +12,7 @@ import ( "strings" "text/template" + "github.com/cenkalti/backoff/v4" "github.com/go-errors/errors" "github.com/jackc/pgconn" "github.com/spf13/afero" @@ -133,6 +134,7 @@ func NewDbConfigWithPassword(ctx context.Context, projectRef string) pgconn.Conf // Special handling for pooler username if suffix := "." + projectRef; strings.HasSuffix(config.User, suffix) { newRole.User += suffix + defer tryPooler(ctx, &config) } config.User = newRole.User return config @@ -148,6 +150,18 @@ func NewDbConfigWithPassword(ctx context.Context, projectRef string) pgconn.Conf return config } +func tryPooler(ctx context.Context, config *pgconn.Config) { + if err := backoff.RetryNotify(func() error { + conn, err := pgconn.ConnectConfig(ctx, config) + if err != nil { + return errors.Errorf("failed to connect as temp role: %w", err) + } + return conn.Close(ctx) + }, utils.NewBackoffPolicy(ctx), utils.NewErrorCallback()); err != nil { + fmt.Fprintln(os.Stderr, err) + } +} + var ( //go:embed queries/role.sql initRoleEmbed string diff --git a/internal/utils/retry.go b/internal/utils/retry.go new file mode 100644 index 000000000..2c94d82dc --- /dev/null +++ b/internal/utils/retry.go @@ -0,0 +1,30 @@ +package utils + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/cenkalti/backoff/v4" +) + +const maxRetries = 8 + +func NewBackoffPolicy(ctx context.Context) backoff.BackOffContext { + b := backoff.NewExponentialBackOff(backoff.WithInitialInterval(3 * time.Second)) + return backoff.WithContext(backoff.WithMaxRetries(b, maxRetries), ctx) +} + +func NewErrorCallback() backoff.Notify { + failureCount := 0 + logger := GetDebugLogger() + return func(err error, d time.Duration) { + failureCount += 1 + if failureCount*3 > maxRetries { + logger = os.Stderr + } + fmt.Fprintln(logger, err) + fmt.Fprintf(logger, "Retry (%d/%d): ", failureCount, maxRetries) + } +} diff --git a/pkg/api/client.gen.go b/pkg/api/client.gen.go index b3898e0f3..eae5082f6 100644 --- a/pkg/api/client.gen.go +++ b/pkg/api/client.gen.go @@ -9818,7 +9818,7 @@ func (r V1GetProjectLogsResponse) StatusCode() int { type V1GetProjectUsageApiCountResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *AnalyticsResponse + JSON200 *V1GetUsageApiCountResponse } // Status returns HTTPResponse.Status @@ -9840,7 +9840,7 @@ func (r V1GetProjectUsageApiCountResponse) StatusCode() int { type V1GetProjectUsageRequestCountResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *AnalyticsResponse + JSON200 *V1GetUsageApiRequestsCountResponse } // Status returns HTTPResponse.Status @@ -14234,7 +14234,7 @@ func ParseV1GetProjectUsageApiCountResponse(rsp *http.Response) (*V1GetProjectUs switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest AnalyticsResponse + var dest V1GetUsageApiCountResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -14260,7 +14260,7 @@ func ParseV1GetProjectUsageRequestCountResponse(rsp *http.Response) (*V1GetProje switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest AnalyticsResponse + var dest V1GetUsageApiRequestsCountResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 7079302fc..eb639a8fa 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -1174,6 +1174,7 @@ type AuthConfigResponse struct { ExternalTwitterClientId nullable.Nullable[string] `json:"external_twitter_client_id"` ExternalTwitterEnabled nullable.Nullable[bool] `json:"external_twitter_enabled"` ExternalTwitterSecret nullable.Nullable[string] `json:"external_twitter_secret"` + ExternalWeb3EthereumEnabled nullable.Nullable[bool] `json:"external_web3_ethereum_enabled"` ExternalWeb3SolanaEnabled nullable.Nullable[bool] `json:"external_web3_solana_enabled"` ExternalWorkosClientId nullable.Nullable[string] `json:"external_workos_client_id"` ExternalWorkosEnabled nullable.Nullable[bool] `json:"external_workos_enabled"` @@ -2429,6 +2430,7 @@ type UpdateAuthConfigBody struct { ExternalTwitterClientId nullable.Nullable[string] `json:"external_twitter_client_id,omitempty"` ExternalTwitterEnabled nullable.Nullable[bool] `json:"external_twitter_enabled,omitempty"` ExternalTwitterSecret nullable.Nullable[string] `json:"external_twitter_secret,omitempty"` + ExternalWeb3EthereumEnabled nullable.Nullable[bool] `json:"external_web3_ethereum_enabled,omitempty"` ExternalWeb3SolanaEnabled nullable.Nullable[bool] `json:"external_web3_solana_enabled,omitempty"` ExternalWorkosClientId nullable.Nullable[string] `json:"external_workos_client_id,omitempty"` ExternalWorkosEnabled nullable.Nullable[bool] `json:"external_workos_enabled,omitempty"` @@ -2817,6 +2819,70 @@ type V1CreateProjectBodyPlan string // V1CreateProjectBodyRegion Region you want your server to reside in type V1CreateProjectBodyRegion string +// V1GetUsageApiCountResponse defines model for V1GetUsageApiCountResponse. +type V1GetUsageApiCountResponse struct { + Error *V1GetUsageApiCountResponse_Error `json:"error,omitempty"` + Result *[]struct { + Timestamp time.Time `json:"timestamp"` + TotalAuthRequests float32 `json:"total_auth_requests"` + TotalRealtimeRequests float32 `json:"total_realtime_requests"` + TotalRestRequests float32 `json:"total_rest_requests"` + TotalStorageRequests float32 `json:"total_storage_requests"` + } `json:"result,omitempty"` +} + +// V1GetUsageApiCountResponseError0 defines model for . +type V1GetUsageApiCountResponseError0 = string + +// V1GetUsageApiCountResponseError1 defines model for . +type V1GetUsageApiCountResponseError1 struct { + Code float32 `json:"code"` + Errors []struct { + Domain string `json:"domain"` + Location string `json:"location"` + LocationType string `json:"locationType"` + Message string `json:"message"` + Reason string `json:"reason"` + } `json:"errors"` + Message string `json:"message"` + Status string `json:"status"` +} + +// V1GetUsageApiCountResponse_Error defines model for V1GetUsageApiCountResponse.Error. +type V1GetUsageApiCountResponse_Error struct { + union json.RawMessage +} + +// V1GetUsageApiRequestsCountResponse defines model for V1GetUsageApiRequestsCountResponse. +type V1GetUsageApiRequestsCountResponse struct { + Error *V1GetUsageApiRequestsCountResponse_Error `json:"error,omitempty"` + Result *[]struct { + Count float32 `json:"count"` + } `json:"result,omitempty"` +} + +// V1GetUsageApiRequestsCountResponseError0 defines model for . +type V1GetUsageApiRequestsCountResponseError0 = string + +// V1GetUsageApiRequestsCountResponseError1 defines model for . +type V1GetUsageApiRequestsCountResponseError1 struct { + Code float32 `json:"code"` + Errors []struct { + Domain string `json:"domain"` + Location string `json:"location"` + LocationType string `json:"locationType"` + Message string `json:"message"` + Reason string `json:"reason"` + } `json:"errors"` + Message string `json:"message"` + Status string `json:"status"` +} + +// V1GetUsageApiRequestsCountResponse_Error defines model for V1GetUsageApiRequestsCountResponse.Error. +type V1GetUsageApiRequestsCountResponse_Error struct { + union json.RawMessage +} + // V1ListMigrationsResponse defines model for V1ListMigrationsResponse. type V1ListMigrationsResponse = []struct { Name *string `json:"name,omitempty"` @@ -4251,6 +4317,130 @@ func (t *ListProjectAddonsResponse_SelectedAddons_Variant_Id) UnmarshalJSON(b [] return err } +// AsV1GetUsageApiCountResponseError0 returns the union data inside the V1GetUsageApiCountResponse_Error as a V1GetUsageApiCountResponseError0 +func (t V1GetUsageApiCountResponse_Error) AsV1GetUsageApiCountResponseError0() (V1GetUsageApiCountResponseError0, error) { + var body V1GetUsageApiCountResponseError0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromV1GetUsageApiCountResponseError0 overwrites any union data inside the V1GetUsageApiCountResponse_Error as the provided V1GetUsageApiCountResponseError0 +func (t *V1GetUsageApiCountResponse_Error) FromV1GetUsageApiCountResponseError0(v V1GetUsageApiCountResponseError0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeV1GetUsageApiCountResponseError0 performs a merge with any union data inside the V1GetUsageApiCountResponse_Error, using the provided V1GetUsageApiCountResponseError0 +func (t *V1GetUsageApiCountResponse_Error) MergeV1GetUsageApiCountResponseError0(v V1GetUsageApiCountResponseError0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsV1GetUsageApiCountResponseError1 returns the union data inside the V1GetUsageApiCountResponse_Error as a V1GetUsageApiCountResponseError1 +func (t V1GetUsageApiCountResponse_Error) AsV1GetUsageApiCountResponseError1() (V1GetUsageApiCountResponseError1, error) { + var body V1GetUsageApiCountResponseError1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromV1GetUsageApiCountResponseError1 overwrites any union data inside the V1GetUsageApiCountResponse_Error as the provided V1GetUsageApiCountResponseError1 +func (t *V1GetUsageApiCountResponse_Error) FromV1GetUsageApiCountResponseError1(v V1GetUsageApiCountResponseError1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeV1GetUsageApiCountResponseError1 performs a merge with any union data inside the V1GetUsageApiCountResponse_Error, using the provided V1GetUsageApiCountResponseError1 +func (t *V1GetUsageApiCountResponse_Error) MergeV1GetUsageApiCountResponseError1(v V1GetUsageApiCountResponseError1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t V1GetUsageApiCountResponse_Error) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *V1GetUsageApiCountResponse_Error) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// AsV1GetUsageApiRequestsCountResponseError0 returns the union data inside the V1GetUsageApiRequestsCountResponse_Error as a V1GetUsageApiRequestsCountResponseError0 +func (t V1GetUsageApiRequestsCountResponse_Error) AsV1GetUsageApiRequestsCountResponseError0() (V1GetUsageApiRequestsCountResponseError0, error) { + var body V1GetUsageApiRequestsCountResponseError0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromV1GetUsageApiRequestsCountResponseError0 overwrites any union data inside the V1GetUsageApiRequestsCountResponse_Error as the provided V1GetUsageApiRequestsCountResponseError0 +func (t *V1GetUsageApiRequestsCountResponse_Error) FromV1GetUsageApiRequestsCountResponseError0(v V1GetUsageApiRequestsCountResponseError0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeV1GetUsageApiRequestsCountResponseError0 performs a merge with any union data inside the V1GetUsageApiRequestsCountResponse_Error, using the provided V1GetUsageApiRequestsCountResponseError0 +func (t *V1GetUsageApiRequestsCountResponse_Error) MergeV1GetUsageApiRequestsCountResponseError0(v V1GetUsageApiRequestsCountResponseError0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsV1GetUsageApiRequestsCountResponseError1 returns the union data inside the V1GetUsageApiRequestsCountResponse_Error as a V1GetUsageApiRequestsCountResponseError1 +func (t V1GetUsageApiRequestsCountResponse_Error) AsV1GetUsageApiRequestsCountResponseError1() (V1GetUsageApiRequestsCountResponseError1, error) { + var body V1GetUsageApiRequestsCountResponseError1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromV1GetUsageApiRequestsCountResponseError1 overwrites any union data inside the V1GetUsageApiRequestsCountResponse_Error as the provided V1GetUsageApiRequestsCountResponseError1 +func (t *V1GetUsageApiRequestsCountResponse_Error) FromV1GetUsageApiRequestsCountResponseError1(v V1GetUsageApiRequestsCountResponseError1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeV1GetUsageApiRequestsCountResponseError1 performs a merge with any union data inside the V1GetUsageApiRequestsCountResponse_Error, using the provided V1GetUsageApiRequestsCountResponseError1 +func (t *V1GetUsageApiRequestsCountResponse_Error) MergeV1GetUsageApiRequestsCountResponseError1(v V1GetUsageApiRequestsCountResponseError1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t V1GetUsageApiRequestsCountResponse_Error) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *V1GetUsageApiRequestsCountResponse_Error) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + // AsV1ServiceHealthResponseInfo0 returns the union data inside the V1ServiceHealthResponse_Info as a V1ServiceHealthResponseInfo0 func (t V1ServiceHealthResponse_Info) AsV1ServiceHealthResponseInfo0() (V1ServiceHealthResponseInfo0, error) { var body V1ServiceHealthResponseInfo0 diff --git a/pkg/config/templates/Dockerfile b/pkg/config/templates/Dockerfile index 80a6488fd..0038d3dd4 100644 --- a/pkg/config/templates/Dockerfile +++ b/pkg/config/templates/Dockerfile @@ -1,19 +1,19 @@ # Exposed for updates by .github/dependabot.yml -FROM supabase/postgres:17.4.1.068 AS pg +FROM supabase/postgres:17.4.1.072 AS pg # Append to ServiceImages when adding new dependencies below FROM library/kong:2.8.1 AS kong FROM axllent/mailpit:v1.22.3 AS mailpit FROM postgrest/postgrest:v13.0.4 AS postgrest -FROM supabase/postgres-meta:v0.91.4 AS pgmeta -FROM supabase/studio:2025.07.28-sha-578b707 AS studio +FROM supabase/postgres-meta:v0.91.5 AS pgmeta +FROM supabase/studio:2025.08.04-sha-6e99ca6 AS studio FROM darthsim/imgproxy:v3.8.0 AS imgproxy FROM supabase/edge-runtime:v1.68.3 AS edgeruntime FROM timberio/vector:0.28.1-alpine AS vector -FROM supabase/supavisor:2.6.0 AS supavisor -FROM supabase/gotrue:v2.177.0 AS gotrue -FROM supabase/realtime:v2.41.11 AS realtime -FROM supabase/storage-api:v1.26.0 AS storage -FROM supabase/logflare:1.14.2 AS logflare +FROM supabase/supavisor:2.6.1 AS supavisor +FROM supabase/gotrue:v2.178.0 AS gotrue +FROM supabase/realtime:v2.41.23 AS realtime +FROM supabase/storage-api:v1.26.3 AS storage +FROM supabase/logflare:1.18.3 AS logflare # Append to JobImages when adding new dependencies below FROM supabase/pgadmin-schema-diff:cli-0.0.5 AS differ FROM supabase/migra:3.0.1663481299 AS migra diff --git a/pkg/go.mod b/pkg/go.mod index da9ff4f6c..e913dce72 100644 --- a/pkg/go.mod +++ b/pkg/go.mod @@ -25,7 +25,7 @@ require ( github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/tidwall/jsonc v0.3.2 - golang.org/x/mod v0.26.0 + golang.org/x/mod v0.27.0 google.golang.org/grpc v1.74.2 ) diff --git a/pkg/go.sum b/pkg/go.sum index c7f70e16f..ee04f3436 100644 --- a/pkg/go.sum +++ b/pkg/go.sum @@ -226,8 +226,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/pkg/pgxv5/connect.go b/pkg/pgxv5/connect.go index c16f769c8..daacb5fc0 100644 --- a/pkg/pgxv5/connect.go +++ b/pkg/pgxv5/connect.go @@ -5,9 +5,7 @@ import ( "fmt" "os" "strings" - "time" - "github.com/cenkalti/backoff/v4" "github.com/go-errors/errors" "github.com/jackc/pgconn" "github.com/jackc/pgx/v4" @@ -28,31 +26,19 @@ func Connect(ctx context.Context, connString string, options ...func(*pgx.ConnCo config.OnNotice = func(pc *pgconn.PgConn, n *pgconn.Notice) { fmt.Fprintf(os.Stderr, "%s (%s): %s\n", n.Severity, n.Code, n.Message) } - maxRetries := uint64(0) if strings.HasPrefix(config.User, CLI_LOGIN_ROLE) { config.AfterConnect = func(ctx context.Context, pgconn *pgconn.PgConn) error { return pgconn.Exec(ctx, SET_SESSION_ROLE).Close() } - // Add retry to allow enough time for password change to propagate to pooler - if len(config.User) > len(CLI_LOGIN_ROLE) { - maxRetries = 3 - } } // Apply config overrides for _, op := range options { op(config) } // Connect to database - connect := func() (*pgx.Conn, error) { - conn, err := pgx.ConnectConfig(ctx, config) - if err != nil { - return nil, errors.Errorf("failed to connect to postgres: %w", err) - } - return conn, nil + conn, err := pgx.ConnectConfig(ctx, config) + if err != nil { + return nil, errors.Errorf("failed to connect to postgres: %w", err) } - policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff( - backoff.WithInitialInterval(3*time.Second)), - maxRetries), - ctx) - return backoff.RetryWithData(connect, policy) + return conn, nil }