Skip to content
Open
5 changes: 4 additions & 1 deletion cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
)

var (
configDryRun bool

configCmd = &cobra.Command{
GroupID: groupManagementAPI,
Use: "config",
Expand All @@ -18,13 +20,14 @@ var (
Use: "push",
Short: "Pushes local config.toml to the linked project",
RunE: func(cmd *cobra.Command, args []string) error {
return push.Run(cmd.Context(), flags.ProjectRef, afero.NewOsFs())
return push.Run(cmd.Context(), flags.ProjectRef, configDryRun, afero.NewOsFs())
},
}
)

func init() {
configCmd.PersistentFlags().StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
configPushCmd.Flags().BoolVar(&configDryRun, "dry-run", false, "Print operations that would be performed without executing them.")
configCmd.AddCommand(configPushCmd)
rootCmd.AddCommand(configCmd)
}
192 changes: 192 additions & 0 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package cmd

import (
"context"
"fmt"
"os"
"os/signal"

"github.com/go-errors/errors"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/viper"
configPush "github.com/supabase/cli/internal/config/push"
"github.com/supabase/cli/internal/db/push"
funcDeploy "github.com/supabase/cli/internal/functions/deploy"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/api"
"github.com/supabase/cli/pkg/function"
)

var (
// Deploy flags
deployDryRun bool
deployIncludeAll bool
deployIncludeRoles bool
deployIncludeSeed bool

deployCmd = &cobra.Command{
GroupID: groupLocalDev,
Use: "deploy",
Short: "Push all local changes to a Supabase project",
Long: `Deploy local changes to a remote Supabase project.
By default, this command will:
- Push database migrations (supabase db push)
- Deploy edge functions (supabase functions deploy)
You can optionally include config changes with --include-config.
Use individual flags to customize what gets deployed.`,
// PreRunE: func(cmd *cobra.Command, args []string) error {
// return cmd.Root().PersistentPreRunE(cmd, args)
// },
RunE: func(cmd *cobra.Command, args []string) error {
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
fsys := afero.NewOsFs()

// Determine what to deploy
// If no specific flags are set, default to db and functions
includeDb, _ := cmd.Flags().GetBool("include-db")
includeFunctions, _ := cmd.Flags().GetBool("include-functions")
includeConfig, _ := cmd.Flags().GetBool("include-config")

fmt.Fprintln(os.Stderr, utils.Bold("Deploying to project:"), flags.ProjectRef)

spinner := utils.NewSpinner("Connecting to project")
spinner.Start(context.Background())
cancelSpinner := spinner.Start(context.Background())
defer cancelSpinner()
if !isProjectHealthy(ctx) {
spinner.Fail("Project is not healthy. Please ensure all services are running before deploying.")
return errors.New("project is not healthy")
}
spinner.Stop("Connected to project")

var deployErrors []error

// Maybe deploy database migrations
if includeDb {
fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying database migrations...")
if err := push.Run(ctx, deployDryRun, deployIncludeAll, deployIncludeRoles, deployIncludeSeed, flags.DbConfig, fsys); err != nil {
deployErrors = append(deployErrors, errors.Errorf("db push failed: %w", err))
return err // Stop on DB errors as functions might depend on schema
}
fmt.Fprintln(os.Stderr, "")
}

// Maybe deploy edge functions
if includeFunctions {
fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying edge functions...")
if err := funcDeploy.Run(ctx, []string{}, true, nil, "", 1, false, deployDryRun, fsys); err != nil && !errors.Is(err, function.ErrNoDeploy) {
deployErrors = append(deployErrors, errors.Errorf("functions deploy failed: %w", err))
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:")+" Functions deployment failed:", err)
} else if errors.Is(err, function.ErrNoDeploy) {
fmt.Fprintln(os.Stderr, utils.Yellow("⏭ ")+"No functions to deploy")
} else {
// print error just in case
fmt.Fprintln(os.Stderr, err)
if deployDryRun {
fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Functions dry run complete")
} else {
fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Functions deployed successfully")
}
}
fmt.Fprintln(os.Stderr, "")
}

// Maybe deploy config
if includeConfig {
fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying config...")
if err := configPush.Run(ctx, flags.ProjectRef, deployDryRun, fsys); err != nil {
deployErrors = append(deployErrors, errors.Errorf("config push failed: %w", err))
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:")+" Config deployment failed:", err)
} else {
if deployDryRun {
fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config dry run complete")
} else {
fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config deployed successfully")
}
}
fmt.Fprintln(os.Stderr, "")
}

// Summary
if len(deployErrors) > 0 {
if deployDryRun {
fmt.Fprintln(os.Stderr, utils.Yellow("Dry run completed with warnings:"))
} else {
fmt.Fprintln(os.Stderr, utils.Yellow("Deploy completed with warnings:"))
}
for _, err := range deployErrors {
fmt.Fprintln(os.Stderr, " •", err)
}
return nil // Don't fail the command for non-critical errors
}

if deployDryRun {
fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" "+utils.Bold("Dry run completed successfully!"))
} else {
fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" "+utils.Bold("Deployment completed successfully!"))
}
return nil
},
Example: ` supabase deploy
supabase deploy --include-config
supabase deploy --include-db --include-functions
supabase deploy --dry-run`,
}
)

func init() {
cmdFlags := deployCmd.Flags()

// What to deploy - use direct Bool() since we check via cmd.Flags().Changed()
cmdFlags.Bool("include-db", true, "Include database migrations (default: true)")
cmdFlags.Bool("include-functions", true, "Include edge functions (default: true)")
cmdFlags.Bool("include-config", true, "Include config.toml settings (default: true)")

// DB push options (from db push command)
cmdFlags.BoolVar(&deployDryRun, "dry-run", false, "Print operations that would be performed without executing them")
cmdFlags.BoolVar(&deployIncludeAll, "include-all", false, "Include all migrations not found on remote history table")
cmdFlags.BoolVar(&deployIncludeRoles, "include-roles", false, "Include custom roles from "+utils.CustomRolesPath)
cmdFlags.BoolVar(&deployIncludeSeed, "include-seed", false, "Include seed data from your config")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried that having all these flags would be too overwhelming for the user. Can we show a suggestion to use db specific commands instead? Generally we want to build commands that do one thing and one thing well.

Also from maintenance perspective, we want to very selective about which flag to add. We want to make supabase deploy easy for new users to get started but defer them to specific commands as soon as they need to customise the default behaviour.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've simplified things so you can filter down what you want to deploy with --only=db,config and removed some of those db-specifc flags.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some of these flags (db-url, linked, password, etc) it seems required for some of the sub commands. Is there a better way to utilize these without explicitly defining them?


// Project config
cmdFlags.String("db-url", "", "Deploys to the database specified by the connection string (must be percent-encoded)")
cmdFlags.Bool("linked", true, "Deploys to the linked project")
cmdFlags.Bool("local", false, "Deploys to the local database")
deployCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local")
cmdFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database")
cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", cmdFlags.Lookup("password")))
cmdFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project")

rootCmd.AddCommand(deployCmd)
}
func isProjectHealthy(ctx context.Context) bool {
services := []api.V1GetServicesHealthParamsServices{
api.Auth,
// Not checking Realtime for now as it can be flaky
// api.Realtime,
api.Rest,
api.Storage,
api.Db,
}
resp, err := utils.GetSupabase().V1GetServicesHealthWithResponse(ctx, flags.ProjectRef, &api.V1GetServicesHealthParams{
Services: services,
})
if err != nil {
// return errors.Errorf("failed to check remote health: %w", err)
return false
}
if resp.JSON200 == nil {
// return errors.New("Unexpected error checking remote health: " + string(resp.Body))
return false
}
for _, service := range *resp.JSON200 {
if !service.Healthy {
return false
}
}
return true
}
16 changes: 9 additions & 7 deletions cmd/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ var (
},
}

useApi bool
useDocker bool
useLegacyBundle bool
noVerifyJWT = new(bool)
importMapPath string
prune bool
useApi bool
useDocker bool
useLegacyBundle bool
noVerifyJWT = new(bool)
importMapPath string
prune bool
functionsDryRun bool

functionsDeployCmd = &cobra.Command{
Use: "deploy [Function name]",
Expand All @@ -74,7 +75,7 @@ var (
} else if maxJobs > 1 {
return errors.New("--jobs must be used together with --use-api")
}
return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, afero.NewOsFs())
return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, functionsDryRun, afero.NewOsFs())
},
}

Expand Down Expand Up @@ -141,6 +142,7 @@ func init() {
deployFlags.UintVarP(&maxJobs, "jobs", "j", 1, "Maximum number of parallel jobs.")
deployFlags.BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
deployFlags.BoolVar(&prune, "prune", false, "Delete Functions that exist in Supabase project but not locally.")
deployFlags.BoolVar(&functionsDryRun, "dry-run", false, "Print operations that would be performed without executing them.")
deployFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
deployFlags.StringVar(&importMapPath, "import-map", "", "Path to import map file.")
functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
Expand Down
16 changes: 12 additions & 4 deletions cmd/status.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"fmt"
"os"
"os/signal"

Expand All @@ -12,13 +13,14 @@ import (
)

var (
override []string
names status.CustomName
override []string
names status.CustomName
useLinkedProject bool

statusCmd = &cobra.Command{
GroupID: groupLocalDev,
Use: "status",
Short: "Show status of local Supabase containers",
Short: "Show status of local Supabase containers or linked project",
PreRunE: func(cmd *cobra.Command, args []string) error {
es, err := env.EnvironToEnvSet(override)
if err != nil {
Expand All @@ -28,15 +30,21 @@ var (
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
if useLinkedProject {
fmt.Fprintf(os.Stderr, "Project health check:\n")
return status.RunRemote(ctx, utils.OutputFormat.Value, afero.NewOsFs())
}
return status.Run(ctx, names, utils.OutputFormat.Value, afero.NewOsFs())
},
Example: ` supabase status -o env --override-name api.url=NEXT_PUBLIC_SUPABASE_URL
supabase status -o json`,
supabase status -o json
supabase status --linked`,
}
)

func init() {
flags := statusCmd.Flags()
flags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.")
flags.BoolVar(&useLinkedProject, "linked", false, "Check health of linked project.")
rootCmd.AddCommand(statusCmd)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ require (
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yarlson/pin v0.9.1 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
github.com/yarlson/pin v0.9.1 h1:ZfbMMTSpZw9X7ebq9QS6FAUq66PTv56S4WN4puO2HK0=
github.com/yarlson/pin v0.9.1/go.mod h1:FC/d9PacAtwh05XzSznZWhA447uvimitjgDDl5YaVLE=
github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
Expand Down
7 changes: 5 additions & 2 deletions internal/config/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/supabase/cli/pkg/config"
)

func Run(ctx context.Context, ref string, fsys afero.Fs) error {
func Run(ctx context.Context, ref string, dryRun bool, fsys afero.Fs) error {
if err := flags.LoadConfig(fsys); err != nil {
return err
}
Expand All @@ -26,9 +26,12 @@ func Run(ctx context.Context, ref string, fsys afero.Fs) error {
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "Pushing config to project:", remote.ProjectId)
fmt.Fprintln(os.Stderr, "Checking config for project:", remote.ProjectId)
console := utils.NewConsole()
keep := func(name string) bool {
if dryRun {
return false
}
title := fmt.Sprintf("Do you want to push %s config to remote?", name)
if item, exists := cost[name]; exists {
title = fmt.Sprintf("Enabling %s will cost you %s. Keep it enabled?", item.Name, item.Price)
Expand Down
23 changes: 20 additions & 3 deletions internal/functions/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"github.com/supabase/cli/pkg/function"
)

func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, prune bool, fsys afero.Fs) error {
func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, prune bool, dryRun bool, fsys afero.Fs) error {
// Load function config and project id
if err := flags.LoadConfig(fsys); err != nil {
return err
Expand Down Expand Up @@ -51,7 +51,8 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool,
if err != nil {
return err
}
// Deploy new and updated functions

// Setup API with optional bundler
opt := function.WithMaxJobs(maxJobs)
if useDocker {
if utils.IsDockerRunning(ctx) {
Expand All @@ -61,9 +62,25 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool,
}
}
api := function.NewEdgeRuntimeAPI(flags.ProjectRef, *utils.GetSupabase(), opt)

// In dry-run mode, check what would be deployed
if dryRun {
if err := api.DryRun(ctx, functionConfig, afero.NewIOFS(fsys)); errors.Is(err, function.ErrNoDeploy) {
fmt.Fprintln(os.Stderr, err)
return err
} else if err != nil {
return err
}
if prune {
fmt.Fprintln(os.Stderr, "\nWould check for functions to prune.")
}
return nil
}

// Deploy new and updated functions
if err := api.Deploy(ctx, functionConfig, afero.NewIOFS(fsys)); errors.Is(err, function.ErrNoDeploy) {
fmt.Fprintln(os.Stderr, err)
return nil
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were returning nil so that exit code is 0 when there are no changed functions to deploy. What's the reason for changing this?

} else if err != nil {
return err
}
Expand Down
Loading
Loading