Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8270fce
basic single command to deploy everything
matlin Oct 21, 2025
4895f05
Add remote flag to status
matlin Oct 23, 2025
dcb990c
Add project health spinner to deploy cmd
matlin Oct 24, 2025
02e7aad
feat: add dry-run support to config and functions deployments
matlin Oct 26, 2025
4c2492c
Use linked naming instead of remote for status cmd
matlin Oct 28, 2025
adbda2e
Use seperate dry run funcs for config updating
matlin Oct 28, 2025
e0e0323
simplify config dryrun logic
matlin Oct 28, 2025
b9b3785
use filter for function dry run instead of new param
matlin Oct 29, 2025
afe2ea7
Use filter approach instead of dry run var
matlin Oct 29, 2025
83eb7ae
Simplify deployment component selection
matlin Oct 29, 2025
5a98731
remove 'only' flag
matlin Oct 31, 2025
0584ed7
use quick start group
matlin Nov 7, 2025
e7ab596
address comments
matlin Nov 19, 2025
0bb83b7
Merge branch 'develop' into feat-single-deploy-cmd
sweatybridge Nov 26, 2025
6608f1f
chore: refactor deploy implementation to internal package
sweatybridge Nov 26, 2025
b6a3e01
chore: add missing package
sweatybridge Nov 26, 2025
5bf01c9
Merge branch 'develop' into feat-single-deploy-cmd
sweatybridge Nov 27, 2025
937ab4b
Merge branch 'develop' into feat-single-deploy-cmd
sweatybridge Dec 1, 2025
297ee98
Revert "Use linked naming instead of remote for status cmd"
sweatybridge Dec 1, 2025
3b40392
Revert "Add remote flag to status"
sweatybridge Dec 1, 2025
6e85645
chore: remove unnecessary log
sweatybridge Dec 1, 2025
a318fe3
fix: dry run function deploy with pruning
sweatybridge Dec 1, 2025
9c100ac
chore: prompt before dry run config update
sweatybridge Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")

// 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
} else if err != nil {
return err
}
Expand Down
Loading
Loading