Skip to content
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)
}
209 changes: 209 additions & 0 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
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
only []string

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 components to deploy
includeDb := false
includeFunctions := false
includeConfig := false
if len(only) == 0 {
includeDb = true
includeFunctions = true
includeConfig = true
} else {
for _, component := range only {
switch component {
case "db":
includeDb = true
case "functions":
includeFunctions = true
case "config":
includeConfig = true
default:
return errors.Errorf("unknown component to deploy: %s", component)
}
}
}

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...")
keep := func(name string) bool {
if deployDryRun {
fmt.Fprintln(os.Stderr, utils.Yellow("⏭ ")+"Would deploy:", name)
return false
}
return true
}
if err := funcDeploy.Run(ctx, []string{}, true, nil, "", 1, false, fsys, keep); 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()

deployCmd.Flags().StringSliceVar(&only, "only", []string{}, "Comma-separated list of components to deploy (e.g., db,storage,functions).")
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we intend to use this on CI/CD, I think it's neater to set this via config.toml. For eg. to disable deploying storage

[storage]
enabled = false

For adhoc deployments from local, I'd still prefer directing users to db specific commands.


cmdFlags.BoolVar(&deployDryRun, "dry-run", false, "Print operations that would be performed without executing them")

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
}
12 changes: 11 additions & 1 deletion cmd/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"os"

"github.com/go-errors/errors"
"github.com/spf13/afero"
Expand Down Expand Up @@ -59,6 +60,7 @@ var (
noVerifyJWT = new(bool)
importMapPath string
prune bool
functionsDryRun bool

functionsDeployCmd = &cobra.Command{
Use: "deploy [Function name]",
Expand All @@ -74,7 +76,14 @@ 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())
keep := func(name string) bool {
if functionsDryRun {
fmt.Fprintln(os.Stderr, "Would deploy:", name)
return false
}
return true
}
return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, afero.NewOsFs(), keep)
},
}

Expand Down Expand Up @@ -141,6 +150,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
Loading
Loading