From 8270fcec945b5d550f191169c1b34ee40da3dc81 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Tue, 21 Oct 2025 11:35:46 -0700 Subject: [PATCH 01/20] basic single command to deploy everything --- cmd/deploy.go | 143 ++++++++++++++++++++++++++++ cmd/root.go | 1 + internal/functions/deploy/deploy.go | 2 +- pkg/function/batch.go | 3 +- 4 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 cmd/deploy.go diff --git a/cmd/deploy.go b/cmd/deploy.go new file mode 100644 index 000000000..5ef803c69 --- /dev/null +++ b/cmd/deploy.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "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" + "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/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() + + // Load config + // if err := flags.LoadConfig(fsys); err != nil { + // return err + // } + + // 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) + fmt.Fprintln(os.Stderr, "") + + var deployErrors []error + + // 1. Deploy config first (if requested) + if includeConfig { + fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying config...") + if err := configPush.Run(ctx, flags.ProjectRef, 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 { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config deployed successfully") + } + fmt.Fprintln(os.Stderr, "") + } + + // 2. 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, "") + } + + // 3. Deploy edge functions + if includeFunctions { + fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying edge functions...") + if err := deploy.Run(ctx, []string{}, true, nil, "", 1, false, 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) + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Functions deployed successfully") + } + fmt.Fprintln(os.Stderr, "") + } + + // Summary + if len(deployErrors) > 0 { + 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 + } + + 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) +} diff --git a/cmd/root.go b/cmd/root.go index 5a1072284..f0c945d3f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,6 +113,7 @@ var ( } } } + fmt.Println("Parsing database config from ROOT") if err := flags.ParseDatabaseConfig(ctx, cmd.Flags(), fsys); err != nil { return err } diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index bcee53c55..6abe2e7b6 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -63,7 +63,7 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, api := function.NewEdgeRuntimeAPI(flags.ProjectRef, *utils.GetSupabase(), opt) 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 } diff --git a/pkg/function/batch.go b/pkg/function/batch.go index fad409853..3f2f4782f 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -97,6 +97,7 @@ OUTER: toUpdate = append(toUpdate, result...) policy.Reset() } + fmt.Fprintf(os.Stderr, "Updating %d Functions...\n", len(toUpdate)) if len(toUpdate) > 1 { if err := backoff.Retry(func() error { if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil { @@ -109,7 +110,7 @@ OUTER: return err } } - return nil + return ErrNoDeploy } func (s *EdgeRuntimeAPI) updateFunction(ctx context.Context, slug string, meta FunctionDeployMetadata, body io.Reader) (api.BulkUpdateFunctionBody, error) { From 4895f0598a60014aa50d6a452c83c0199ec9998e Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Thu, 23 Oct 2025 07:46:35 -0700 Subject: [PATCH 02/20] Add remote flag to status --- cmd/root.go | 1 - cmd/status.go | 12 +++++-- internal/status/status.go | 55 +++++++++++++++++++++++++++++ internal/status/status_test.go | 63 ++++++++++++++++++++++++++++++++++ internal/utils/colors.go | 10 ++++++ 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index f0c945d3f..5a1072284 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,7 +113,6 @@ var ( } } } - fmt.Println("Parsing database config from ROOT") if err := flags.ParseDatabaseConfig(ctx, cmd.Flags(), fsys); err != nil { return err } diff --git a/cmd/status.go b/cmd/status.go index 11b774854..b63cb019d 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "os" "os/signal" @@ -14,11 +15,12 @@ import ( var ( override []string names status.CustomName + remote bool statusCmd = &cobra.Command{ GroupID: groupLocalDev, Use: "status", - Short: "Show status of local Supabase containers", + Short: "Show status of local Supabase containers or remote project", PreRunE: func(cmd *cobra.Command, args []string) error { es, err := env.EnvironToEnvSet(override) if err != nil { @@ -28,15 +30,21 @@ var ( }, RunE: func(cmd *cobra.Command, args []string) error { ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) + if remote { + 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 --remote`, } ) func init() { flags := statusCmd.Flags() flags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.") + flags.BoolVar(&remote, "remote", false, "Check health of remote project.") rootCmd.AddCommand(statusCmd) } diff --git a/internal/status/status.go b/internal/status/status.go index ee087062b..fd4c62afb 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/afero" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/fetcher" ) @@ -251,3 +252,57 @@ func isDeprecated(tag string) bool { } return false } + +func RunRemote(ctx context.Context, format string, fsys afero.Fs) error { + // Parse project ref + if err := flags.ParseProjectRef(ctx, fsys); err != nil { + return err + } + + // Define services to check + services := []api.V1GetServicesHealthParamsServices{ + api.Auth, + api.Realtime, + api.Rest, + api.Storage, + api.Db, + } + + // Call health check API + 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) + } + if resp.JSON200 == nil { + return errors.New("Unexpected error checking remote health: " + string(resp.Body)) + } + + // Print results + if format == utils.OutputPretty { + return prettyPrintRemoteHealth(os.Stdout, *resp.JSON200) + } + return utils.EncodeOutput(format, os.Stdout, resp.JSON200) +} + +func prettyPrintRemoteHealth(w io.Writer, health []api.V1ServiceHealthResponse) error { + fmt.Fprintf(w, "\n") + for _, service := range health { + statusSymbol := "✓" + statusColor := utils.Green + if !service.Healthy { + statusSymbol = "✗" + statusColor = utils.Red + } + + fmt.Fprintf(w, "%s %s %s\n", statusColor(statusSymbol), utils.Aqua(string(service.Name)), utils.Dim(string(service.Status))) + + if service.Error != nil && *service.Error != "" { + fmt.Fprintf(w, " Error: %s\n", utils.Red(*service.Error)) + } + } + fmt.Fprintf(w, "\n") + + return nil +} diff --git a/internal/status/status_test.go b/internal/status/status_test.go index c7bfc1bc4..ec5582541 100644 --- a/internal/status/status_test.go +++ b/internal/status/status_test.go @@ -14,6 +14,8 @@ import ( "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" ) func TestStatusCommand(t *testing.T) { @@ -176,3 +178,64 @@ func TestPrintStatus(t *testing.T) { assert.Equal(t, "DB_URL = \"postgresql://postgres:postgres@127.0.0.1:0/postgres\"\n", stdout.String()) }) } + +func TestRemoteStatusCommand(t *testing.T) { + t.Run("shows remote health status", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + projectRef := apitest.RandomProjectRef() + require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(projectRef), 0644)) + // Setup access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + // Setup mock API + defer gock.OffAll() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + projectRef + "/health"). + ParamPresent("services"). + Reply(http.StatusOK). + JSON([]api.V1ServiceHealthResponse{ + { + Name: api.V1ServiceHealthResponseNameAuth, + Healthy: true, + Status: api.ACTIVEHEALTHY, + }, + { + Name: api.V1ServiceHealthResponseNameRealtime, + Healthy: true, + Status: api.ACTIVEHEALTHY, + }, + { + Name: api.V1ServiceHealthResponseNameRest, + Healthy: true, + Status: api.ACTIVEHEALTHY, + }, + { + Name: api.V1ServiceHealthResponseNameStorage, + Healthy: true, + Status: api.ACTIVEHEALTHY, + }, + { + Name: api.V1ServiceHealthResponseNameDb, + Healthy: true, + Status: api.ACTIVEHEALTHY, + }, + }) + // Run test + assert.NoError(t, RunRemote(context.Background(), utils.OutputPretty, fsys)) + // Check error + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + + t.Run("throws error on missing project ref", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + defer gock.OffAll() + // Reset global state + flags.ProjectRef = "" + // Run test + err := RunRemote(context.Background(), utils.OutputPretty, fsys) + // Check error + assert.ErrorContains(t, err, "project ref") + }) +} diff --git a/internal/utils/colors.go b/internal/utils/colors.go index ed710c4a2..a703fb926 100644 --- a/internal/utils/colors.go +++ b/internal/utils/colors.go @@ -22,3 +22,13 @@ func Red(str string) string { func Bold(str string) string { return lipgloss.NewStyle().Bold(true).Render(str) } + +// For success, healthy, etc. +func Green(str string) string { + return lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render(str) +} + +// For secondary labels +func Dim(str string) string { + return lipgloss.NewStyle().Faint(true).Render(str) +} From dcb990c68e4ffe87ce2592d7d7100d26214981e8 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Fri, 24 Oct 2025 13:11:54 -0500 Subject: [PATCH 03/20] Add project health spinner to deploy cmd --- cmd/deploy.go | 77 ++++++++++++++++++++++++++++----------- cmd/status.go | 10 ++--- go.mod | 1 + go.sum | 2 + internal/utils/spinner.go | 10 +++++ 5 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 internal/utils/spinner.go diff --git a/cmd/deploy.go b/cmd/deploy.go index 5ef803c69..aa96efae2 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "os" "os/signal" @@ -11,9 +12,10 @@ import ( "github.com/spf13/viper" configPush "github.com/supabase/cli/internal/config/push" "github.com/supabase/cli/internal/db/push" - "github.com/supabase/cli/internal/functions/deploy" + 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" ) @@ -43,11 +45,6 @@ Use individual flags to customize what gets deployed.`, ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) fsys := afero.NewOsFs() - // Load config - // if err := flags.LoadConfig(fsys); err != nil { - // return err - // } - // Determine what to deploy // If no specific flags are set, default to db and functions includeDb, _ := cmd.Flags().GetBool("include-db") @@ -55,23 +52,20 @@ Use individual flags to customize what gets deployed.`, includeConfig, _ := cmd.Flags().GetBool("include-config") fmt.Fprintln(os.Stderr, utils.Bold("Deploying to project:"), flags.ProjectRef) - fmt.Fprintln(os.Stderr, "") - - var deployErrors []error - // 1. Deploy config first (if requested) - if includeConfig { - fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying config...") - if err := configPush.Run(ctx, flags.ProjectRef, 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 { - fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config deployed successfully") - } - fmt.Fprintln(os.Stderr, "") + 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") - // 2. Deploy database migrations + 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 { @@ -81,10 +75,10 @@ Use individual flags to customize what gets deployed.`, fmt.Fprintln(os.Stderr, "") } - // 3. Deploy edge functions + // Maybe deploy edge functions if includeFunctions { fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying edge functions...") - if err := deploy.Run(ctx, []string{}, true, nil, "", 1, false, fsys); err != nil && !errors.Is(err, function.ErrNoDeploy) { + if err := funcDeploy.Run(ctx, []string{}, true, nil, "", 1, false, 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) { @@ -97,6 +91,18 @@ Use individual flags to customize what gets deployed.`, fmt.Fprintln(os.Stderr, "") } + // Maybe deploy config + if includeConfig { + fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying config...") + if err := configPush.Run(ctx, flags.ProjectRef, 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 { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config deployed successfully") + } + fmt.Fprintln(os.Stderr, "") + } + // Summary if len(deployErrors) > 0 { fmt.Fprintln(os.Stderr, utils.Yellow("Deploy completed with warnings:")) @@ -141,3 +147,30 @@ func init() { 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 +} diff --git a/cmd/status.go b/cmd/status.go index b63cb019d..8022b0616 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -13,9 +13,9 @@ import ( ) var ( - override []string - names status.CustomName - remote bool + override []string + names status.CustomName + useRemoteProject bool statusCmd = &cobra.Command{ GroupID: groupLocalDev, @@ -30,7 +30,7 @@ var ( }, RunE: func(cmd *cobra.Command, args []string) error { ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) - if remote { + if useRemoteProject { fmt.Fprintf(os.Stderr, "Project health check:\n") return status.RunRemote(ctx, utils.OutputFormat.Value, afero.NewOsFs()) } @@ -45,6 +45,6 @@ var ( func init() { flags := statusCmd.Flags() flags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.") - flags.BoolVar(&remote, "remote", false, "Check health of remote project.") + flags.BoolVar(&useRemoteProject, "remote", false, "Check health of remote project.") rootCmd.AddCommand(statusCmd) } diff --git a/go.mod b/go.mod index 6d5bfe49b..184715503 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ad6b798e7..bb3ab85c6 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/utils/spinner.go b/internal/utils/spinner.go new file mode 100644 index 000000000..22a404adb --- /dev/null +++ b/internal/utils/spinner.go @@ -0,0 +1,10 @@ +package utils + +import ( + "github.com/yarlson/pin" +) + +func NewSpinner(text string) *pin.Pin { + s := pin.New(text) + return s +} From 02e7aadefc53401e30ba6e47acbb8412090b50ae Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Sun, 26 Oct 2025 12:41:45 -0500 Subject: [PATCH 04/20] feat: add dry-run support to config and functions deployments --- cmd/config.go | 5 +- cmd/deploy.go | 28 +++++++--- cmd/functions.go | 16 +++--- internal/config/push/push.go | 9 +++- internal/functions/deploy/deploy.go | 21 +++++++- pkg/config/updater.go | 60 ++++++++++++++++------ pkg/function/batch.go | 80 ++++++++++++++++++++++++++++- pkg/function/deploy.go | 37 +++++++++++++ 8 files changed, 220 insertions(+), 36 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index 1d0f60733..3836a8b47 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,6 +8,8 @@ import ( ) var ( + configDryRun bool + configCmd = &cobra.Command{ GroupID: groupManagementAPI, Use: "config", @@ -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) } diff --git a/cmd/deploy.go b/cmd/deploy.go index aa96efae2..8aa20a7b4 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -78,7 +78,7 @@ Use individual flags to customize what gets deployed.`, // 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, fsys); err != nil && !errors.Is(err, function.ErrNoDeploy) { + 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) { @@ -86,7 +86,11 @@ Use individual flags to customize what gets deployed.`, } else { // print error just in case fmt.Fprintln(os.Stderr, err) - fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Functions deployed successfully") + 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, "") } @@ -94,25 +98,37 @@ Use individual flags to customize what gets deployed.`, // Maybe deploy config if includeConfig { fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying config...") - if err := configPush.Run(ctx, flags.ProjectRef, fsys); err != nil { + 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 { - fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config deployed successfully") + 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 { - fmt.Fprintln(os.Stderr, utils.Yellow("Deploy completed with warnings:")) + 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 } - fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" "+utils.Bold("Deployment completed successfully!")) + 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 diff --git a/cmd/functions.go b/cmd/functions.go index 6b5684bc2..e050ffd70 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -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]", @@ -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()) }, } @@ -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.") diff --git a/internal/config/push/push.go b/internal/config/push/push.go index 1a2340060..4e999c0f9 100644 --- a/internal/config/push/push.go +++ b/internal/config/push/push.go @@ -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 } @@ -26,6 +26,11 @@ func Run(ctx context.Context, ref string, fsys afero.Fs) error { if err != nil { return err } + if dryRun { + fmt.Fprintln(os.Stderr, "DRY RUN: config will *not* be pushed to the project.") + fmt.Fprintln(os.Stderr, "Checking config for project:", remote.ProjectId) + return client.UpdateRemoteConfig(ctx, remote, dryRun) + } fmt.Fprintln(os.Stderr, "Pushing config to project:", remote.ProjectId) console := utils.NewConsole() keep := func(name string) bool { @@ -39,7 +44,7 @@ func Run(ctx context.Context, ref string, fsys afero.Fs) error { } return shouldPush } - return client.UpdateRemoteConfig(ctx, remote, keep) + return client.UpdateRemoteConfig(ctx, remote, dryRun, keep) } type CostItem struct { diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index 6abe2e7b6..4ecf45ddd 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -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 @@ -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) { @@ -61,6 +62,22 @@ 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 err diff --git a/pkg/config/updater.go b/pkg/config/updater.go index 8915f43fb..255f09c4a 100644 --- a/pkg/config/updater.go +++ b/pkg/config/updater.go @@ -17,26 +17,26 @@ func NewConfigUpdater(client v1API.ClientWithResponses) ConfigUpdater { return ConfigUpdater{client: client} } -func (u *ConfigUpdater) UpdateRemoteConfig(ctx context.Context, remote baseConfig, filter ...func(string) bool) error { - if err := u.UpdateApiConfig(ctx, remote.ProjectId, remote.Api, filter...); err != nil { +func (u *ConfigUpdater) UpdateRemoteConfig(ctx context.Context, remote baseConfig, dryRun bool, filter ...func(string) bool) error { + if err := u.UpdateApiConfig(ctx, remote.ProjectId, remote.Api, dryRun, filter...); err != nil { return err } - if err := u.UpdateDbConfig(ctx, remote.ProjectId, remote.Db, filter...); err != nil { + if err := u.UpdateDbConfig(ctx, remote.ProjectId, remote.Db, dryRun, filter...); err != nil { return err } - if err := u.UpdateAuthConfig(ctx, remote.ProjectId, remote.Auth, filter...); err != nil { + if err := u.UpdateAuthConfig(ctx, remote.ProjectId, remote.Auth, dryRun, filter...); err != nil { return err } - if err := u.UpdateStorageConfig(ctx, remote.ProjectId, remote.Storage, filter...); err != nil { + if err := u.UpdateStorageConfig(ctx, remote.ProjectId, remote.Storage, dryRun, filter...); err != nil { return err } - if err := u.UpdateExperimentalConfig(ctx, remote.ProjectId, remote.Experimental, filter...); err != nil { + if err := u.UpdateExperimentalConfig(ctx, remote.ProjectId, remote.Experimental, dryRun, filter...); err != nil { return err } return nil } -func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, c api, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, c api, dryRun bool, filter ...func(string) bool) error { apiConfig, err := u.client.V1GetPostgrestServiceConfigWithResponse(ctx, projectRef) if err != nil { return errors.Errorf("failed to read API config: %w", err) @@ -50,6 +50,10 @@ func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, fmt.Fprintln(os.Stderr, "Remote API config is up to date.") return nil } + if dryRun { + fmt.Fprintln(os.Stderr, "Would update API service with config:", string(apiDiff)) + return nil + } fmt.Fprintln(os.Stderr, "Updating API service with config:", string(apiDiff)) for _, keep := range filter { if !keep("api") { @@ -64,7 +68,7 @@ func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, return nil } -func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef string, s settings, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef string, s settings, dryRun bool, filter ...func(string) bool) error { dbConfig, err := u.client.V1GetPostgresConfigWithResponse(ctx, projectRef) if err != nil { return errors.Errorf("failed to read DB config: %w", err) @@ -78,6 +82,10 @@ func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef s fmt.Fprintln(os.Stderr, "Remote DB config is up to date.") return nil } + if dryRun { + fmt.Fprintln(os.Stderr, "Would update DB service with config:", string(dbDiff)) + return nil + } fmt.Fprintln(os.Stderr, "Updating DB service with config:", string(dbDiff)) for _, keep := range filter { if !keep("db") { @@ -93,17 +101,17 @@ func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef s return nil } -func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c db, filter ...func(string) bool) error { - if err := u.UpdateDbSettingsConfig(ctx, projectRef, c.Settings, filter...); err != nil { +func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c db, dryRun bool, filter ...func(string) bool) error { + if err := u.UpdateDbSettingsConfig(ctx, projectRef, c.Settings, dryRun, filter...); err != nil { return err } - if err := u.UpdateDbNetworkRestrictionsConfig(ctx, projectRef, c.NetworkRestrictions, filter...); err != nil { + if err := u.UpdateDbNetworkRestrictionsConfig(ctx, projectRef, c.NetworkRestrictions, dryRun, filter...); err != nil { return err } return nil } -func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, dryRun bool, filter ...func(string) bool) error { networkRestrictionsConfig, err := u.client.V1GetNetworkRestrictionsWithResponse(ctx, projectRef) if err != nil { return errors.Errorf("failed to read network restrictions config: %w", err) @@ -117,6 +125,10 @@ func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, p fmt.Fprintln(os.Stderr, "Remote DB Network restrictions config is up to date.") return nil } + if dryRun { + fmt.Fprintln(os.Stderr, "Would update network restrictions with config:", string(networkRestrictionsDiff)) + return nil + } fmt.Fprintln(os.Stderr, "Updating network restrictions with config:", string(networkRestrictionsDiff)) for _, keep := range filter { if !keep("db") { @@ -132,7 +144,7 @@ func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, p return nil } -func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, dryRun bool, filter ...func(string) bool) error { if !c.Enabled { return nil } @@ -149,6 +161,10 @@ func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, fmt.Fprintln(os.Stderr, "Remote Auth config is up to date.") return nil } + if dryRun { + fmt.Fprintln(os.Stderr, "Would update Auth service with config:", string(authDiff)) + return nil + } fmt.Fprintln(os.Stderr, "Updating Auth service with config:", string(authDiff)) for _, keep := range filter { if !keep("auth") { @@ -163,7 +179,7 @@ func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, return nil } -func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, dryRun bool, filter ...func(string) bool) error { if len(signingKeys) == 0 { return nil } @@ -193,6 +209,10 @@ func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string for _, k := range toInsert { fmt.Fprintln(os.Stderr, " -", k.KeyID) } + if dryRun { + fmt.Fprintln(os.Stderr, "Would insert", len(toInsert), "signing keys") + return nil + } for _, keep := range filter { if !keep("signing keys") { return nil @@ -234,7 +254,7 @@ func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string return nil } -func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, dryRun bool, filter ...func(string) bool) error { if !c.Enabled { return nil } @@ -251,6 +271,10 @@ func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef stri fmt.Fprintln(os.Stderr, "Remote Storage config is up to date.") return nil } + if dryRun { + fmt.Fprintln(os.Stderr, "Would update Storage service with config:", string(storageDiff)) + return nil + } fmt.Fprintln(os.Stderr, "Updating Storage service with config:", string(storageDiff)) for _, keep := range filter { if !keep("storage") { @@ -265,8 +289,12 @@ func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef stri return nil } -func (u *ConfigUpdater) UpdateExperimentalConfig(ctx context.Context, projectRef string, exp experimental, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateExperimentalConfig(ctx context.Context, projectRef string, exp experimental, dryRun bool, filter ...func(string) bool) error { if exp.Webhooks != nil && exp.Webhooks.Enabled { + if dryRun { + fmt.Fprintln(os.Stderr, "Would enable webhooks for project:", projectRef) + return nil + } fmt.Fprintln(os.Stderr, "Enabling webhooks for project:", projectRef) for _, keep := range filter { if !keep("webhooks") { diff --git a/pkg/function/batch.go b/pkg/function/batch.go index 3f2f4782f..0a38fcbbc 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -25,6 +25,10 @@ const ( ) func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { + return s.upsertFunctions(ctx, functionConfig, false, filter...) +} + +func (s *EdgeRuntimeAPI) upsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, dryRun bool, filter ...func(string) bool) error { policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx) result, err := backoff.RetryWithData(func() ([]api.FunctionResponse, error) { resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project) @@ -47,11 +51,22 @@ func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig con for i, f := range result { slugToIndex[f.Slug] = i } + + // Track functions by status for reporting + var toCreate []string + var toUpdateList []string + var upToDate []string + var disabled []string var toUpdate api.BulkUpdateFunctionBody + OUTER: for slug, function := range functionConfig { if !function.Enabled { - fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) + if dryRun { + disabled = append(disabled, slug) + } else { + fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) + } continue } for _, keep := range filter { @@ -74,9 +89,25 @@ OUTER: if i, exists := slugToIndex[slug]; exists && i >= 0 && result[i].EzbrSha256 != nil && *result[i].EzbrSha256 == meta.SHA256 && result[i].VerifyJwt != nil && *result[i].VerifyJwt == function.VerifyJWT { - fmt.Fprintln(os.Stderr, "No change found in Function:", slug) + if dryRun { + upToDate = append(upToDate, slug) + } else { + fmt.Fprintln(os.Stderr, "No change found in Function:", slug) + } + continue + } + + // Track what would be created vs updated + _, exists := slugToIndex[slug] + if dryRun { + if !exists { + toCreate = append(toCreate, slug) + } else { + toUpdateList = append(toUpdateList, slug) + } continue } + // Update if function already exists upsert := func() (api.BulkUpdateFunctionBody, error) { if _, exists := slugToIndex[slug]; exists { @@ -97,6 +128,51 @@ OUTER: toUpdate = append(toUpdate, result...) policy.Reset() } + + // In dry-run mode, print summary and return + if dryRun { + fmt.Fprintln(os.Stderr, "DRY RUN: functions will *not* be deployed.") + + if len(toCreate) > 0 { + fmt.Fprintln(os.Stderr, "\nWould create these functions:") + for _, slug := range toCreate { + fc := functionConfig[slug] + fmt.Fprintf(os.Stderr, " • %s\n", slug) + fmt.Fprintf(os.Stderr, " - Entrypoint: %s\n", fc.Entrypoint) + if fc.ImportMap != "" { + fmt.Fprintf(os.Stderr, " - Import map: %s\n", fc.ImportMap) + } + fmt.Fprintf(os.Stderr, " - Verify JWT: %v\n", fc.VerifyJWT) + } + } + + if len(toUpdateList) > 0 { + fmt.Fprintln(os.Stderr, "\nWould update these functions (code or config changed):") + for _, slug := range toUpdateList { + fmt.Fprintf(os.Stderr, " • %s\n", slug) + } + } + + if len(upToDate) > 0 { + fmt.Fprintln(os.Stderr, "\nThese functions are up to date:") + for _, slug := range upToDate { + fmt.Fprintf(os.Stderr, " • %s\n", slug) + } + } + + if len(disabled) > 0 { + fmt.Fprintln(os.Stderr, "\nThese functions are disabled (would be skipped):") + for _, slug := range disabled { + fmt.Fprintf(os.Stderr, " • %s\n", slug) + } + } + + if len(toCreate) == 0 && len(toUpdateList) == 0 { + return ErrNoDeploy + } + return nil + } + fmt.Fprintf(os.Stderr, "Updating %d Functions...\n", len(toUpdate)) if len(toUpdate) > 1 { if err := backoff.Retry(func() error { diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index 88e2f464a..3593cd10d 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -19,6 +19,43 @@ import ( var ErrNoDeploy = errors.New("All Functions are up to date.") +func (s *EdgeRuntimeAPI) DryRun(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error { + // If we have an eszip bundler, use the same logic as UpsertFunctions + if s.eszip != nil { + return s.upsertFunctions(ctx, functionConfig, true) + } + + // Without eszip bundler, we can't accurately detect changes + // Fallback to listing what would be deployed based on API deploy logic + var toDeploy []string + for slug, fc := range functionConfig { + if !fc.Enabled { + fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) + continue + } + toDeploy = append(toDeploy, slug) + } + + if len(toDeploy) == 0 { + return errors.New(ErrNoDeploy) + } + + fmt.Fprintln(os.Stderr, "DRY RUN: functions will *not* be deployed.") + fmt.Fprintln(os.Stderr, "\nWould deploy these functions:") + fmt.Fprintln(os.Stderr, "(Unable to detect changes without Docker bundler)") + for _, slug := range toDeploy { + fc := functionConfig[slug] + fmt.Fprintf(os.Stderr, " • %s\n", slug) + fmt.Fprintf(os.Stderr, " - Entrypoint: %s\n", fc.Entrypoint) + if fc.ImportMap != "" { + fmt.Fprintf(os.Stderr, " - Import map: %s\n", fc.ImportMap) + } + fmt.Fprintf(os.Stderr, " - Verify JWT: %v\n", fc.VerifyJWT) + } + + return nil +} + func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error { if s.eszip != nil { return s.UpsertFunctions(ctx, functionConfig) From 4c2492cb75cd25f128eebf474d8d28922fabf5bb Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Tue, 28 Oct 2025 10:12:01 -0500 Subject: [PATCH 05/20] Use linked naming instead of remote for status cmd --- cmd/status.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/status.go b/cmd/status.go index 8022b0616..ba7efd19d 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -15,12 +15,12 @@ import ( var ( override []string names status.CustomName - useRemoteProject bool + useLinkedProject bool statusCmd = &cobra.Command{ GroupID: groupLocalDev, Use: "status", - Short: "Show status of local Supabase containers or remote project", + 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 { @@ -30,7 +30,7 @@ var ( }, RunE: func(cmd *cobra.Command, args []string) error { ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) - if useRemoteProject { + if useLinkedProject { fmt.Fprintf(os.Stderr, "Project health check:\n") return status.RunRemote(ctx, utils.OutputFormat.Value, afero.NewOsFs()) } @@ -38,13 +38,13 @@ var ( }, Example: ` supabase status -o env --override-name api.url=NEXT_PUBLIC_SUPABASE_URL supabase status -o json - supabase status --remote`, + supabase status --linked`, } ) func init() { flags := statusCmd.Flags() flags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.") - flags.BoolVar(&useRemoteProject, "remote", false, "Check health of remote project.") + flags.BoolVar(&useLinkedProject, "linked", false, "Check health of linked project.") rootCmd.AddCommand(statusCmd) } From adbda2ea2eb87287eeae603b66a5bee2df9fd1dd Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Tue, 28 Oct 2025 11:02:04 -0500 Subject: [PATCH 06/20] Use seperate dry run funcs for config updating --- internal/config/push/push.go | 4 +- pkg/config/updater.go | 249 +++++++++++++++++++++++++++-------- 2 files changed, 194 insertions(+), 59 deletions(-) diff --git a/internal/config/push/push.go b/internal/config/push/push.go index 4e999c0f9..944c24231 100644 --- a/internal/config/push/push.go +++ b/internal/config/push/push.go @@ -29,7 +29,7 @@ func Run(ctx context.Context, ref string, dryRun bool, fsys afero.Fs) error { if dryRun { fmt.Fprintln(os.Stderr, "DRY RUN: config will *not* be pushed to the project.") fmt.Fprintln(os.Stderr, "Checking config for project:", remote.ProjectId) - return client.UpdateRemoteConfig(ctx, remote, dryRun) + return client.UpdateRemoteConfigDryRun(ctx, remote) } fmt.Fprintln(os.Stderr, "Pushing config to project:", remote.ProjectId) console := utils.NewConsole() @@ -44,7 +44,7 @@ func Run(ctx context.Context, ref string, dryRun bool, fsys afero.Fs) error { } return shouldPush } - return client.UpdateRemoteConfig(ctx, remote, dryRun, keep) + return client.UpdateRemoteConfig(ctx, remote, keep) } type CostItem struct { diff --git a/pkg/config/updater.go b/pkg/config/updater.go index 255f09c4a..e9dccaebf 100644 --- a/pkg/config/updater.go +++ b/pkg/config/updater.go @@ -17,43 +17,78 @@ func NewConfigUpdater(client v1API.ClientWithResponses) ConfigUpdater { return ConfigUpdater{client: client} } -func (u *ConfigUpdater) UpdateRemoteConfig(ctx context.Context, remote baseConfig, dryRun bool, filter ...func(string) bool) error { - if err := u.UpdateApiConfig(ctx, remote.ProjectId, remote.Api, dryRun, filter...); err != nil { +func (u *ConfigUpdater) UpdateRemoteConfig(ctx context.Context, remote baseConfig, filter ...func(string) bool) error { + if err := u.UpdateApiConfig(ctx, remote.ProjectId, remote.Api, filter...); err != nil { return err } - if err := u.UpdateDbConfig(ctx, remote.ProjectId, remote.Db, dryRun, filter...); err != nil { + if err := u.UpdateDbConfig(ctx, remote.ProjectId, remote.Db, filter...); err != nil { return err } - if err := u.UpdateAuthConfig(ctx, remote.ProjectId, remote.Auth, dryRun, filter...); err != nil { + if err := u.UpdateAuthConfig(ctx, remote.ProjectId, remote.Auth, filter...); err != nil { return err } - if err := u.UpdateStorageConfig(ctx, remote.ProjectId, remote.Storage, dryRun, filter...); err != nil { + if err := u.UpdateStorageConfig(ctx, remote.ProjectId, remote.Storage, filter...); err != nil { return err } - if err := u.UpdateExperimentalConfig(ctx, remote.ProjectId, remote.Experimental, dryRun, filter...); err != nil { + if err := u.UpdateExperimentalConfig(ctx, remote.ProjectId, remote.Experimental, filter...); err != nil { + return err + } + return nil +} +func (u *ConfigUpdater) UpdateRemoteConfigDryRun(ctx context.Context, remote baseConfig, filter ...func(string) bool) error { + // Implement dry run logic for remote config updates + if err := u.UpdateApiConfigDryRun(ctx, remote.ProjectId, remote.Api, filter...); err != nil { + return err + } + if err := u.UpdateDbConfigDryRun(ctx, remote.ProjectId, remote.Db, filter...); err != nil { + return err + } + if err := u.UpdateAuthConfigDryRun(ctx, remote.ProjectId, remote.Auth, filter...); err != nil { + return err + } + if err := u.UpdateStorageConfigDryRun(ctx, remote.ProjectId, remote.Storage, filter...); err != nil { + return err + } + if err := u.UpdateExperimentalConfigDryRun(ctx, remote.ProjectId, remote.Experimental, filter...); err != nil { return err } return nil } -func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, c api, dryRun bool, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateApiConfigDryRun(ctx context.Context, projectRef string, c api, filter ...func(string) bool) error { + apiDiff, err := u.GetApiConfigDiff(ctx, projectRef, c) + if err != nil { + return err + } else if len(apiDiff) == 0 { + fmt.Fprintln(os.Stderr, "Remote API config is up to date.") + return nil + } + fmt.Fprintln(os.Stderr, "Would update API service with config:", string(apiDiff)) + return nil +} + +func (u *ConfigUpdater) GetApiConfigDiff(ctx context.Context, projectRef string, c api) ([]byte, error) { apiConfig, err := u.client.V1GetPostgrestServiceConfigWithResponse(ctx, projectRef) if err != nil { - return errors.Errorf("failed to read API config: %w", err) + return nil, errors.Errorf("failed to read API config: %w", err) } else if apiConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", apiConfig.StatusCode(), string(apiConfig.Body)) + return nil, errors.Errorf("unexpected status %d: %s", apiConfig.StatusCode(), string(apiConfig.Body)) } apiDiff, err := c.DiffWithRemote(*apiConfig.JSON200) + if err != nil { + return nil, err + } + return apiDiff, nil +} + +func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, c api, filter ...func(string) bool) error { + apiDiff, err := u.GetApiConfigDiff(ctx, projectRef, c) if err != nil { return err } else if len(apiDiff) == 0 { fmt.Fprintln(os.Stderr, "Remote API config is up to date.") return nil } - if dryRun { - fmt.Fprintln(os.Stderr, "Would update API service with config:", string(apiDiff)) - return nil - } fmt.Fprintln(os.Stderr, "Updating API service with config:", string(apiDiff)) for _, keep := range filter { if !keep("api") { @@ -68,22 +103,38 @@ func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, return nil } -func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef string, s settings, dryRun bool, filter ...func(string) bool) error { +func (u *ConfigUpdater) GetDBSettingsConfigDiff(ctx context.Context, projectRef string, s settings) ([]byte, error) { dbConfig, err := u.client.V1GetPostgresConfigWithResponse(ctx, projectRef) if err != nil { - return errors.Errorf("failed to read DB config: %w", err) + return nil, errors.Errorf("failed to read DB config: %w", err) } else if dbConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", dbConfig.StatusCode(), string(dbConfig.Body)) + return nil, errors.Errorf("unexpected status %d: %s", dbConfig.StatusCode(), string(dbConfig.Body)) } dbDiff, err := s.DiffWithRemote(*dbConfig.JSON200) + if err != nil { + return nil, err + } + return dbDiff, nil +} + +func (u *ConfigUpdater) UpdateDbSettingsConfigDryRun(ctx context.Context, projectRef string, c settings, filter ...func(string) bool) error { + apiDiff, err := u.GetDBSettingsConfigDiff(ctx, projectRef, c) if err != nil { return err - } else if len(dbDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote DB config is up to date.") + } else if len(apiDiff) == 0 { + fmt.Fprintln(os.Stderr, "Remote API config is up to date.") return nil } - if dryRun { - fmt.Fprintln(os.Stderr, "Would update DB service with config:", string(dbDiff)) + fmt.Fprintln(os.Stderr, "Would update API service with config:", string(apiDiff)) + return nil +} + +func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef string, s settings, filter ...func(string) bool) error { + dbDiff, err := u.GetDBSettingsConfigDiff(ctx, projectRef, s) + if err != nil { + return err + } else if len(dbDiff) == 0 { + fmt.Fprintln(os.Stderr, "Remote DB config is up to date.") return nil } fmt.Fprintln(os.Stderr, "Updating DB service with config:", string(dbDiff)) @@ -101,32 +152,58 @@ func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef s return nil } -func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c db, dryRun bool, filter ...func(string) bool) error { - if err := u.UpdateDbSettingsConfig(ctx, projectRef, c.Settings, dryRun, filter...); err != nil { +func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c db, filter ...func(string) bool) error { + if err := u.UpdateDbSettingsConfig(ctx, projectRef, c.Settings, filter...); err != nil { return err } - if err := u.UpdateDbNetworkRestrictionsConfig(ctx, projectRef, c.NetworkRestrictions, dryRun, filter...); err != nil { + if err := u.UpdateDbNetworkRestrictionsConfig(ctx, projectRef, c.NetworkRestrictions, filter...); err != nil { return err } return nil } -func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, dryRun bool, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateDbConfigDryRun(ctx context.Context, projectRef string, c db, filter ...func(string) bool) error { + if err := u.UpdateDbSettingsConfigDryRun(ctx, projectRef, c.Settings, filter...); err != nil { + return err + } + if err := u.UpdateDbNetworkRestrictionsConfigDryRun(ctx, projectRef, c.NetworkRestrictions, filter...); err != nil { + return err + } + return nil +} + +func (u *ConfigUpdater) GetDBNetworkRestrictionsConfigDiff(ctx context.Context, projectRef string, n networkRestrictions) ([]byte, error) { networkRestrictionsConfig, err := u.client.V1GetNetworkRestrictionsWithResponse(ctx, projectRef) if err != nil { - return errors.Errorf("failed to read network restrictions config: %w", err) + return nil, errors.Errorf("failed to read network restrictions config: %w", err) } else if networkRestrictionsConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", networkRestrictionsConfig.StatusCode(), string(networkRestrictionsConfig.Body)) + return nil, errors.Errorf("unexpected status %d: %s", networkRestrictionsConfig.StatusCode(), string(networkRestrictionsConfig.Body)) } networkRestrictionsDiff, err := n.DiffWithRemote(*networkRestrictionsConfig.JSON200) + if err != nil { + return nil, err + } + return networkRestrictionsDiff, nil +} + +func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfigDryRun(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { + networkRestrictionsDiff, err := u.GetDBNetworkRestrictionsConfigDiff(ctx, projectRef, n) if err != nil { return err } else if len(networkRestrictionsDiff) == 0 { fmt.Fprintln(os.Stderr, "Remote DB Network restrictions config is up to date.") return nil } - if dryRun { - fmt.Fprintln(os.Stderr, "Would update network restrictions with config:", string(networkRestrictionsDiff)) + fmt.Fprintln(os.Stderr, "Would update network restrictions with config:", string(networkRestrictionsDiff)) + return nil +} + +func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { + networkRestrictionsDiff, err := u.GetDBNetworkRestrictionsConfigDiff(ctx, projectRef, n) + if err != nil { + return err + } else if len(networkRestrictionsDiff) == 0 { + fmt.Fprintln(os.Stderr, "Remote DB Network restrictions config is up to date.") return nil } fmt.Fprintln(os.Stderr, "Updating network restrictions with config:", string(networkRestrictionsDiff)) @@ -144,25 +221,38 @@ func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, p return nil } -func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, dryRun bool, filter ...func(string) bool) error { - if !c.Enabled { - return nil - } +func (u *ConfigUpdater) GetAuthConfigDiff(ctx context.Context, projectRef string, c auth) ([]byte, error) { authConfig, err := u.client.V1GetAuthServiceConfigWithResponse(ctx, projectRef) if err != nil { - return errors.Errorf("failed to read Auth config: %w", err) + return nil, errors.Errorf("failed to read Auth config: %w", err) } else if authConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", authConfig.StatusCode(), string(authConfig.Body)) + return nil, errors.Errorf("unexpected status %d: %s", authConfig.StatusCode(), string(authConfig.Body)) + } + authDiff, err := c.DiffWithRemote(*authConfig.JSON200) + if err != nil { + return nil, err } - authDiff, err := c.DiffWithRemote(*authConfig.JSON200, filter...) + return authDiff, nil +} + +func (u *ConfigUpdater) UpdateAuthConfigDryRun(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { + authDiff, err := u.GetAuthConfigDiff(ctx, projectRef, c) if err != nil { return err } else if len(authDiff) == 0 { fmt.Fprintln(os.Stderr, "Remote Auth config is up to date.") return nil } - if dryRun { - fmt.Fprintln(os.Stderr, "Would update Auth service with config:", string(authDiff)) + fmt.Fprintln(os.Stderr, "Would update Auth service with config:", string(authDiff)) + return nil +} + +func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { + authDiff, err := u.GetAuthConfigDiff(ctx, projectRef, c) + if err != nil { + return err + } else if len(authDiff) == 0 { + fmt.Fprintln(os.Stderr, "Remote Auth config is up to date.") return nil } fmt.Fprintln(os.Stderr, "Updating Auth service with config:", string(authDiff)) @@ -179,15 +269,12 @@ func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, return nil } -func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, dryRun bool, filter ...func(string) bool) error { - if len(signingKeys) == 0 { - return nil - } +func (u *ConfigUpdater) GetSigningKeysDiff(ctx context.Context, projectRef string, signingKeys []JWK) ([]JWK, error) { resp, err := u.client.V1GetProjectSigningKeysWithResponse(ctx, projectRef) if err != nil { - return errors.Errorf("failed to fetch signing keys: %w", err) + return nil, errors.Errorf("failed to fetch signing keys: %w", err) } else if resp.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) + return nil, errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) } exists := map[string]struct{}{} for _, k := range resp.JSON200.Keys { @@ -201,6 +288,14 @@ func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string toInsert = append(toInsert, k) } } + return toInsert, nil +} + +func (u *ConfigUpdater) UpdateSigningKeysDryRun(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { + toInsert, err := u.GetSigningKeysDiff(ctx, projectRef, signingKeys) + if err != nil { + return err + } if len(toInsert) == 0 { fmt.Fprintln(os.Stderr, "Remote JWT signing keys are up to date.") return nil @@ -209,10 +304,25 @@ func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string for _, k := range toInsert { fmt.Fprintln(os.Stderr, " -", k.KeyID) } - if dryRun { - fmt.Fprintln(os.Stderr, "Would insert", len(toInsert), "signing keys") + return nil +} + +func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { + if len(signingKeys) == 0 { + return nil + } + toInsert, err := u.GetSigningKeysDiff(ctx, projectRef, signingKeys) + if err != nil { + return err + } + if len(toInsert) == 0 { + fmt.Fprintln(os.Stderr, "Remote JWT signing keys are up to date.") return nil } + fmt.Fprintln(os.Stderr, "JWT signing keys to insert:") + for _, k := range toInsert { + fmt.Fprintln(os.Stderr, " -", k.KeyID) + } for _, keep := range filter { if !keep("signing keys") { return nil @@ -254,25 +364,47 @@ func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string return nil } -func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, dryRun bool, filter ...func(string) bool) error { +func (u *ConfigUpdater) GetStorageConfigDiff(ctx context.Context, projectRef string, c storage) ([]byte, error) { if !c.Enabled { - return nil + return nil, nil } storageConfig, err := u.client.V1GetStorageConfigWithResponse(ctx, projectRef) if err != nil { - return errors.Errorf("failed to read Storage config: %w", err) + return nil, errors.Errorf("failed to read Storage config: %w", err) } else if storageConfig.JSON200 == nil { - return errors.Errorf("unexpected status %d: %s", storageConfig.StatusCode(), string(storageConfig.Body)) + return nil, errors.Errorf("unexpected status %d: %s", storageConfig.StatusCode(), string(storageConfig.Body)) } storageDiff, err := c.DiffWithRemote(*storageConfig.JSON200) + if err != nil { + return nil, err + } + return storageDiff, nil +} + +func (u *ConfigUpdater) UpdateStorageConfigDryRun(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { + if !c.Enabled { + return nil + } + diff, err := u.GetStorageConfigDiff(ctx, projectRef, c) if err != nil { return err - } else if len(storageDiff) == 0 { + } else if len(diff) == 0 { fmt.Fprintln(os.Stderr, "Remote Storage config is up to date.") return nil } - if dryRun { - fmt.Fprintln(os.Stderr, "Would update Storage service with config:", string(storageDiff)) + fmt.Fprintln(os.Stderr, "Would update Storage service with config:", string(diff)) + return nil +} + +func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { + if !c.Enabled { + return nil + } + storageDiff, err := u.GetStorageConfigDiff(ctx, projectRef, c) + if err != nil { + return err + } else if len(storageDiff) == 0 { + fmt.Fprintln(os.Stderr, "Remote Storage config is up to date.") return nil } fmt.Fprintln(os.Stderr, "Updating Storage service with config:", string(storageDiff)) @@ -289,12 +421,15 @@ func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef stri return nil } -func (u *ConfigUpdater) UpdateExperimentalConfig(ctx context.Context, projectRef string, exp experimental, dryRun bool, filter ...func(string) bool) error { +func (u *ConfigUpdater) UpdateExperimentalConfigDryRun(ctx context.Context, projectRef string, exp experimental, filter ...func(string) bool) error { + if exp.Webhooks != nil && exp.Webhooks.Enabled { + fmt.Fprintln(os.Stderr, "Would enable webhooks for project:", projectRef) + } + return nil +} + +func (u *ConfigUpdater) UpdateExperimentalConfig(ctx context.Context, projectRef string, exp experimental, filter ...func(string) bool) error { if exp.Webhooks != nil && exp.Webhooks.Enabled { - if dryRun { - fmt.Fprintln(os.Stderr, "Would enable webhooks for project:", projectRef) - return nil - } fmt.Fprintln(os.Stderr, "Enabling webhooks for project:", projectRef) for _, keep := range filter { if !keep("webhooks") { From e0e032358a32a4f37f081a766970c9a1f5e21f42 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Tue, 28 Oct 2025 15:37:34 -0500 Subject: [PATCH 07/20] simplify config dryrun logic --- internal/config/push/push.go | 10 +- pkg/config/updater.go | 215 +++++------------------------------ 2 files changed, 30 insertions(+), 195 deletions(-) diff --git a/internal/config/push/push.go b/internal/config/push/push.go index 944c24231..5257a6219 100644 --- a/internal/config/push/push.go +++ b/internal/config/push/push.go @@ -26,14 +26,12 @@ func Run(ctx context.Context, ref string, dryRun bool, fsys afero.Fs) error { if err != nil { return err } - if dryRun { - fmt.Fprintln(os.Stderr, "DRY RUN: config will *not* be pushed to the project.") - fmt.Fprintln(os.Stderr, "Checking config for project:", remote.ProjectId) - return client.UpdateRemoteConfigDryRun(ctx, remote) - } - 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) diff --git a/pkg/config/updater.go b/pkg/config/updater.go index e9dccaebf..8915f43fb 100644 --- a/pkg/config/updater.go +++ b/pkg/config/updater.go @@ -35,54 +35,15 @@ func (u *ConfigUpdater) UpdateRemoteConfig(ctx context.Context, remote baseConfi } return nil } -func (u *ConfigUpdater) UpdateRemoteConfigDryRun(ctx context.Context, remote baseConfig, filter ...func(string) bool) error { - // Implement dry run logic for remote config updates - if err := u.UpdateApiConfigDryRun(ctx, remote.ProjectId, remote.Api, filter...); err != nil { - return err - } - if err := u.UpdateDbConfigDryRun(ctx, remote.ProjectId, remote.Db, filter...); err != nil { - return err - } - if err := u.UpdateAuthConfigDryRun(ctx, remote.ProjectId, remote.Auth, filter...); err != nil { - return err - } - if err := u.UpdateStorageConfigDryRun(ctx, remote.ProjectId, remote.Storage, filter...); err != nil { - return err - } - if err := u.UpdateExperimentalConfigDryRun(ctx, remote.ProjectId, remote.Experimental, filter...); err != nil { - return err - } - return nil -} - -func (u *ConfigUpdater) UpdateApiConfigDryRun(ctx context.Context, projectRef string, c api, filter ...func(string) bool) error { - apiDiff, err := u.GetApiConfigDiff(ctx, projectRef, c) - if err != nil { - return err - } else if len(apiDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote API config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Would update API service with config:", string(apiDiff)) - return nil -} -func (u *ConfigUpdater) GetApiConfigDiff(ctx context.Context, projectRef string, c api) ([]byte, error) { +func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, c api, filter ...func(string) bool) error { apiConfig, err := u.client.V1GetPostgrestServiceConfigWithResponse(ctx, projectRef) if err != nil { - return nil, errors.Errorf("failed to read API config: %w", err) + return errors.Errorf("failed to read API config: %w", err) } else if apiConfig.JSON200 == nil { - return nil, errors.Errorf("unexpected status %d: %s", apiConfig.StatusCode(), string(apiConfig.Body)) + return errors.Errorf("unexpected status %d: %s", apiConfig.StatusCode(), string(apiConfig.Body)) } apiDiff, err := c.DiffWithRemote(*apiConfig.JSON200) - if err != nil { - return nil, err - } - return apiDiff, nil -} - -func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, c api, filter ...func(string) bool) error { - apiDiff, err := u.GetApiConfigDiff(ctx, projectRef, c) if err != nil { return err } else if len(apiDiff) == 0 { @@ -103,34 +64,14 @@ func (u *ConfigUpdater) UpdateApiConfig(ctx context.Context, projectRef string, return nil } -func (u *ConfigUpdater) GetDBSettingsConfigDiff(ctx context.Context, projectRef string, s settings) ([]byte, error) { +func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef string, s settings, filter ...func(string) bool) error { dbConfig, err := u.client.V1GetPostgresConfigWithResponse(ctx, projectRef) if err != nil { - return nil, errors.Errorf("failed to read DB config: %w", err) + return errors.Errorf("failed to read DB config: %w", err) } else if dbConfig.JSON200 == nil { - return nil, errors.Errorf("unexpected status %d: %s", dbConfig.StatusCode(), string(dbConfig.Body)) + return errors.Errorf("unexpected status %d: %s", dbConfig.StatusCode(), string(dbConfig.Body)) } dbDiff, err := s.DiffWithRemote(*dbConfig.JSON200) - if err != nil { - return nil, err - } - return dbDiff, nil -} - -func (u *ConfigUpdater) UpdateDbSettingsConfigDryRun(ctx context.Context, projectRef string, c settings, filter ...func(string) bool) error { - apiDiff, err := u.GetDBSettingsConfigDiff(ctx, projectRef, c) - if err != nil { - return err - } else if len(apiDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote API config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Would update API service with config:", string(apiDiff)) - return nil -} - -func (u *ConfigUpdater) UpdateDbSettingsConfig(ctx context.Context, projectRef string, s settings, filter ...func(string) bool) error { - dbDiff, err := u.GetDBSettingsConfigDiff(ctx, projectRef, s) if err != nil { return err } else if len(dbDiff) == 0 { @@ -162,44 +103,14 @@ func (u *ConfigUpdater) UpdateDbConfig(ctx context.Context, projectRef string, c return nil } -func (u *ConfigUpdater) UpdateDbConfigDryRun(ctx context.Context, projectRef string, c db, filter ...func(string) bool) error { - if err := u.UpdateDbSettingsConfigDryRun(ctx, projectRef, c.Settings, filter...); err != nil { - return err - } - if err := u.UpdateDbNetworkRestrictionsConfigDryRun(ctx, projectRef, c.NetworkRestrictions, filter...); err != nil { - return err - } - return nil -} - -func (u *ConfigUpdater) GetDBNetworkRestrictionsConfigDiff(ctx context.Context, projectRef string, n networkRestrictions) ([]byte, error) { +func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { networkRestrictionsConfig, err := u.client.V1GetNetworkRestrictionsWithResponse(ctx, projectRef) if err != nil { - return nil, errors.Errorf("failed to read network restrictions config: %w", err) + return errors.Errorf("failed to read network restrictions config: %w", err) } else if networkRestrictionsConfig.JSON200 == nil { - return nil, errors.Errorf("unexpected status %d: %s", networkRestrictionsConfig.StatusCode(), string(networkRestrictionsConfig.Body)) + return errors.Errorf("unexpected status %d: %s", networkRestrictionsConfig.StatusCode(), string(networkRestrictionsConfig.Body)) } networkRestrictionsDiff, err := n.DiffWithRemote(*networkRestrictionsConfig.JSON200) - if err != nil { - return nil, err - } - return networkRestrictionsDiff, nil -} - -func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfigDryRun(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { - networkRestrictionsDiff, err := u.GetDBNetworkRestrictionsConfigDiff(ctx, projectRef, n) - if err != nil { - return err - } else if len(networkRestrictionsDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote DB Network restrictions config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Would update network restrictions with config:", string(networkRestrictionsDiff)) - return nil -} - -func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, projectRef string, n networkRestrictions, filter ...func(string) bool) error { - networkRestrictionsDiff, err := u.GetDBNetworkRestrictionsConfigDiff(ctx, projectRef, n) if err != nil { return err } else if len(networkRestrictionsDiff) == 0 { @@ -221,34 +132,17 @@ func (u *ConfigUpdater) UpdateDbNetworkRestrictionsConfig(ctx context.Context, p return nil } -func (u *ConfigUpdater) GetAuthConfigDiff(ctx context.Context, projectRef string, c auth) ([]byte, error) { +func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { + if !c.Enabled { + return nil + } authConfig, err := u.client.V1GetAuthServiceConfigWithResponse(ctx, projectRef) if err != nil { - return nil, errors.Errorf("failed to read Auth config: %w", err) + return errors.Errorf("failed to read Auth config: %w", err) } else if authConfig.JSON200 == nil { - return nil, errors.Errorf("unexpected status %d: %s", authConfig.StatusCode(), string(authConfig.Body)) + return errors.Errorf("unexpected status %d: %s", authConfig.StatusCode(), string(authConfig.Body)) } - authDiff, err := c.DiffWithRemote(*authConfig.JSON200) - if err != nil { - return nil, err - } - return authDiff, nil -} - -func (u *ConfigUpdater) UpdateAuthConfigDryRun(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { - authDiff, err := u.GetAuthConfigDiff(ctx, projectRef, c) - if err != nil { - return err - } else if len(authDiff) == 0 { - fmt.Fprintln(os.Stderr, "Remote Auth config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Would update Auth service with config:", string(authDiff)) - return nil -} - -func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, c auth, filter ...func(string) bool) error { - authDiff, err := u.GetAuthConfigDiff(ctx, projectRef, c) + authDiff, err := c.DiffWithRemote(*authConfig.JSON200, filter...) if err != nil { return err } else if len(authDiff) == 0 { @@ -269,12 +163,15 @@ func (u *ConfigUpdater) UpdateAuthConfig(ctx context.Context, projectRef string, return nil } -func (u *ConfigUpdater) GetSigningKeysDiff(ctx context.Context, projectRef string, signingKeys []JWK) ([]JWK, error) { +func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { + if len(signingKeys) == 0 { + return nil + } resp, err := u.client.V1GetProjectSigningKeysWithResponse(ctx, projectRef) if err != nil { - return nil, errors.Errorf("failed to fetch signing keys: %w", err) + return errors.Errorf("failed to fetch signing keys: %w", err) } else if resp.JSON200 == nil { - return nil, errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) + return errors.Errorf("unexpected status %d: %s", resp.StatusCode(), string(resp.Body)) } exists := map[string]struct{}{} for _, k := range resp.JSON200.Keys { @@ -288,33 +185,6 @@ func (u *ConfigUpdater) GetSigningKeysDiff(ctx context.Context, projectRef strin toInsert = append(toInsert, k) } } - return toInsert, nil -} - -func (u *ConfigUpdater) UpdateSigningKeysDryRun(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { - toInsert, err := u.GetSigningKeysDiff(ctx, projectRef, signingKeys) - if err != nil { - return err - } - if len(toInsert) == 0 { - fmt.Fprintln(os.Stderr, "Remote JWT signing keys are up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "JWT signing keys to insert:") - for _, k := range toInsert { - fmt.Fprintln(os.Stderr, " -", k.KeyID) - } - return nil -} - -func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string, signingKeys []JWK, filter ...func(string) bool) error { - if len(signingKeys) == 0 { - return nil - } - toInsert, err := u.GetSigningKeysDiff(ctx, projectRef, signingKeys) - if err != nil { - return err - } if len(toInsert) == 0 { fmt.Fprintln(os.Stderr, "Remote JWT signing keys are up to date.") return nil @@ -364,43 +234,17 @@ func (u *ConfigUpdater) UpdateSigningKeys(ctx context.Context, projectRef string return nil } -func (u *ConfigUpdater) GetStorageConfigDiff(ctx context.Context, projectRef string, c storage) ([]byte, error) { +func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { if !c.Enabled { - return nil, nil + return nil } storageConfig, err := u.client.V1GetStorageConfigWithResponse(ctx, projectRef) if err != nil { - return nil, errors.Errorf("failed to read Storage config: %w", err) + return errors.Errorf("failed to read Storage config: %w", err) } else if storageConfig.JSON200 == nil { - return nil, errors.Errorf("unexpected status %d: %s", storageConfig.StatusCode(), string(storageConfig.Body)) + return errors.Errorf("unexpected status %d: %s", storageConfig.StatusCode(), string(storageConfig.Body)) } storageDiff, err := c.DiffWithRemote(*storageConfig.JSON200) - if err != nil { - return nil, err - } - return storageDiff, nil -} - -func (u *ConfigUpdater) UpdateStorageConfigDryRun(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { - if !c.Enabled { - return nil - } - diff, err := u.GetStorageConfigDiff(ctx, projectRef, c) - if err != nil { - return err - } else if len(diff) == 0 { - fmt.Fprintln(os.Stderr, "Remote Storage config is up to date.") - return nil - } - fmt.Fprintln(os.Stderr, "Would update Storage service with config:", string(diff)) - return nil -} - -func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef string, c storage, filter ...func(string) bool) error { - if !c.Enabled { - return nil - } - storageDiff, err := u.GetStorageConfigDiff(ctx, projectRef, c) if err != nil { return err } else if len(storageDiff) == 0 { @@ -421,13 +265,6 @@ func (u *ConfigUpdater) UpdateStorageConfig(ctx context.Context, projectRef stri return nil } -func (u *ConfigUpdater) UpdateExperimentalConfigDryRun(ctx context.Context, projectRef string, exp experimental, filter ...func(string) bool) error { - if exp.Webhooks != nil && exp.Webhooks.Enabled { - fmt.Fprintln(os.Stderr, "Would enable webhooks for project:", projectRef) - } - return nil -} - func (u *ConfigUpdater) UpdateExperimentalConfig(ctx context.Context, projectRef string, exp experimental, filter ...func(string) bool) error { if exp.Webhooks != nil && exp.Webhooks.Enabled { fmt.Fprintln(os.Stderr, "Enabling webhooks for project:", projectRef) From b9b3785b97d21c39164926bd672eed2a57a3c0e1 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Wed, 29 Oct 2025 03:27:23 -0500 Subject: [PATCH 08/20] use filter for function dry run instead of new param --- pkg/function/batch.go | 76 +++--------------------------------------- pkg/function/deploy.go | 5 ++- 2 files changed, 8 insertions(+), 73 deletions(-) diff --git a/pkg/function/batch.go b/pkg/function/batch.go index 0a38fcbbc..ce6081205 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -25,10 +25,10 @@ const ( ) func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { - return s.upsertFunctions(ctx, functionConfig, false, filter...) + return s.upsertFunctions(ctx, functionConfig, filter...) } -func (s *EdgeRuntimeAPI) upsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, dryRun bool, filter ...func(string) bool) error { +func (s *EdgeRuntimeAPI) upsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx) result, err := backoff.RetryWithData(func() ([]api.FunctionResponse, error) { resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project) @@ -52,21 +52,12 @@ func (s *EdgeRuntimeAPI) upsertFunctions(ctx context.Context, functionConfig con slugToIndex[f.Slug] = i } - // Track functions by status for reporting - var toCreate []string - var toUpdateList []string - var upToDate []string - var disabled []string var toUpdate api.BulkUpdateFunctionBody OUTER: for slug, function := range functionConfig { if !function.Enabled { - if dryRun { - disabled = append(disabled, slug) - } else { - fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) - } + fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) continue } for _, keep := range filter { @@ -89,22 +80,7 @@ OUTER: if i, exists := slugToIndex[slug]; exists && i >= 0 && result[i].EzbrSha256 != nil && *result[i].EzbrSha256 == meta.SHA256 && result[i].VerifyJwt != nil && *result[i].VerifyJwt == function.VerifyJWT { - if dryRun { - upToDate = append(upToDate, slug) - } else { - fmt.Fprintln(os.Stderr, "No change found in Function:", slug) - } - continue - } - - // Track what would be created vs updated - _, exists := slugToIndex[slug] - if dryRun { - if !exists { - toCreate = append(toCreate, slug) - } else { - toUpdateList = append(toUpdateList, slug) - } + fmt.Fprintln(os.Stderr, "No change found in Function:", slug) continue } @@ -129,50 +105,6 @@ OUTER: policy.Reset() } - // In dry-run mode, print summary and return - if dryRun { - fmt.Fprintln(os.Stderr, "DRY RUN: functions will *not* be deployed.") - - if len(toCreate) > 0 { - fmt.Fprintln(os.Stderr, "\nWould create these functions:") - for _, slug := range toCreate { - fc := functionConfig[slug] - fmt.Fprintf(os.Stderr, " • %s\n", slug) - fmt.Fprintf(os.Stderr, " - Entrypoint: %s\n", fc.Entrypoint) - if fc.ImportMap != "" { - fmt.Fprintf(os.Stderr, " - Import map: %s\n", fc.ImportMap) - } - fmt.Fprintf(os.Stderr, " - Verify JWT: %v\n", fc.VerifyJWT) - } - } - - if len(toUpdateList) > 0 { - fmt.Fprintln(os.Stderr, "\nWould update these functions (code or config changed):") - for _, slug := range toUpdateList { - fmt.Fprintf(os.Stderr, " • %s\n", slug) - } - } - - if len(upToDate) > 0 { - fmt.Fprintln(os.Stderr, "\nThese functions are up to date:") - for _, slug := range upToDate { - fmt.Fprintf(os.Stderr, " • %s\n", slug) - } - } - - if len(disabled) > 0 { - fmt.Fprintln(os.Stderr, "\nThese functions are disabled (would be skipped):") - for _, slug := range disabled { - fmt.Fprintf(os.Stderr, " • %s\n", slug) - } - } - - if len(toCreate) == 0 && len(toUpdateList) == 0 { - return ErrNoDeploy - } - return nil - } - fmt.Fprintf(os.Stderr, "Updating %d Functions...\n", len(toUpdate)) if len(toUpdate) > 1 { if err := backoff.Retry(func() error { diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index 3593cd10d..f14987775 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -22,7 +22,10 @@ var ErrNoDeploy = errors.New("All Functions are up to date.") func (s *EdgeRuntimeAPI) DryRun(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error { // If we have an eszip bundler, use the same logic as UpsertFunctions if s.eszip != nil { - return s.upsertFunctions(ctx, functionConfig, true) + keep := func(name string) bool { + return false + } + return s.upsertFunctions(ctx, functionConfig, keep) } // Without eszip bundler, we can't accurately detect changes From afe2ea74503eb82fb84d3db1ee7038ff59d1f824 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Wed, 29 Oct 2025 04:28:15 -0500 Subject: [PATCH 09/20] Use filter approach instead of dry run var --- cmd/deploy.go | 9 ++++- cmd/functions.go | 24 ++++++++----- internal/functions/deploy/deploy.go | 19 ++-------- pkg/function/batch.go | 3 +- pkg/function/deploy.go | 55 +++++++---------------------- 5 files changed, 41 insertions(+), 69 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 8aa20a7b4..5ee175cd7 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -78,7 +78,14 @@ Use individual flags to customize what gets deployed.`, // 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) { + 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) { diff --git a/cmd/functions.go b/cmd/functions.go index e050ffd70..a371cc601 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -53,13 +54,13 @@ var ( }, } - useApi bool - useDocker bool - useLegacyBundle bool - noVerifyJWT = new(bool) - importMapPath string - prune bool - functionsDryRun bool + useApi bool + useDocker bool + useLegacyBundle bool + noVerifyJWT = new(bool) + importMapPath string + prune bool + functionsDryRun bool functionsDeployCmd = &cobra.Command{ Use: "deploy [Function name]", @@ -75,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, functionsDryRun, 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) }, } diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index 4ecf45ddd..c0917a075 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -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, dryRun bool, fsys afero.Fs) error { +func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, prune bool, fsys afero.Fs, filter ...(func(string) bool)) error { // Load function config and project id if err := flags.LoadConfig(fsys); err != nil { return err @@ -63,27 +63,14 @@ 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) { + if err := api.Deploy(ctx, functionConfig, afero.NewIOFS(fsys), filter...); errors.Is(err, function.ErrNoDeploy) { fmt.Fprintln(os.Stderr, err) return err } else if err != nil { return err } + // TODO make this message conditional e.g. only when there are changes or not in dry run fmt.Printf("Deployed Functions on project %s: %s\n", utils.Aqua(flags.ProjectRef), strings.Join(slugs, ", ")) url := fmt.Sprintf("%s/project/%v/functions", utils.GetSupabaseDashboardURL(), flags.ProjectRef) fmt.Println("You can inspect your deployment in the Dashboard: " + url) diff --git a/pkg/function/batch.go b/pkg/function/batch.go index ce6081205..2bbdb00a3 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -105,7 +105,6 @@ OUTER: policy.Reset() } - fmt.Fprintf(os.Stderr, "Updating %d Functions...\n", len(toUpdate)) if len(toUpdate) > 1 { if err := backoff.Retry(func() error { if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil { @@ -118,7 +117,7 @@ OUTER: return err } } - return ErrNoDeploy + return nil } func (s *EdgeRuntimeAPI) updateFunction(ctx context.Context, slug string, meta FunctionDeployMetadata, body io.Reader) (api.BulkUpdateFunctionBody, error) { diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index f14987775..11fa0dfbb 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -19,49 +19,9 @@ import ( var ErrNoDeploy = errors.New("All Functions are up to date.") -func (s *EdgeRuntimeAPI) DryRun(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error { - // If we have an eszip bundler, use the same logic as UpsertFunctions +func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS, filter ...func(string) bool) error { if s.eszip != nil { - keep := func(name string) bool { - return false - } - return s.upsertFunctions(ctx, functionConfig, keep) - } - - // Without eszip bundler, we can't accurately detect changes - // Fallback to listing what would be deployed based on API deploy logic - var toDeploy []string - for slug, fc := range functionConfig { - if !fc.Enabled { - fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) - continue - } - toDeploy = append(toDeploy, slug) - } - - if len(toDeploy) == 0 { - return errors.New(ErrNoDeploy) - } - - fmt.Fprintln(os.Stderr, "DRY RUN: functions will *not* be deployed.") - fmt.Fprintln(os.Stderr, "\nWould deploy these functions:") - fmt.Fprintln(os.Stderr, "(Unable to detect changes without Docker bundler)") - for _, slug := range toDeploy { - fc := functionConfig[slug] - fmt.Fprintf(os.Stderr, " • %s\n", slug) - fmt.Fprintf(os.Stderr, " - Entrypoint: %s\n", fc.Entrypoint) - if fc.ImportMap != "" { - fmt.Fprintf(os.Stderr, " - Import map: %s\n", fc.ImportMap) - } - fmt.Fprintf(os.Stderr, " - Verify JWT: %v\n", fc.VerifyJWT) - } - - return nil -} - -func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error { - if s.eszip != nil { - return s.UpsertFunctions(ctx, functionConfig) + return s.UpsertFunctions(ctx, functionConfig, filter...) } // Convert all paths in functions config to relative when using api deploy var toDeploy []FunctionDeployMetadata @@ -81,6 +41,17 @@ func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.Funct files[i] = toRelPath(sf) } meta.StaticPatterns = &files + shouldDeploy := true + for _, keep := range filter { + if !keep(slug) { + shouldDeploy = false + break + } + } + if !shouldDeploy { + fmt.Fprintln(os.Stderr, "Would deploy:", slug) + continue + } toDeploy = append(toDeploy, meta) } if len(toDeploy) == 0 { From 83eb7ae9dde6462446e4c4e9757c75c5b9e213a4 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Wed, 29 Oct 2025 05:21:38 -0500 Subject: [PATCH 10/20] Simplify deployment component selection --- cmd/deploy.go | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 5ee175cd7..290f41a2a 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -25,6 +25,7 @@ var ( deployIncludeAll bool deployIncludeRoles bool deployIncludeSeed bool + only []string deployCmd = &cobra.Command{ GroupID: groupLocalDev, @@ -45,11 +46,28 @@ Use individual flags to customize what gets deployed.`, 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") + // 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) @@ -148,18 +166,10 @@ Use individual flags to customize what gets deployed.`, 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)") + deployCmd.Flags().StringSliceVar(&only, "only", []string{}, "Comma-separated list of components to deploy (e.g., db,storage,functions).") - // 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") From 5a98731a5cd376e922dbb274f7a61d65e70a7785 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Thu, 30 Oct 2025 21:50:48 -0500 Subject: [PATCH 11/20] remove 'only' flag --- cmd/deploy.go | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 290f41a2a..991a4916b 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -25,7 +25,6 @@ var ( deployIncludeAll bool deployIncludeRoles bool deployIncludeSeed bool - only []string deployCmd = &cobra.Command{ GroupID: groupLocalDev, @@ -47,27 +46,9 @@ Use individual flags to customize what gets deployed.`, 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) - } - } - } + includeDb := true + includeFunctions := true + includeConfig := true fmt.Fprintln(os.Stderr, utils.Bold("Deploying to project:"), flags.ProjectRef) @@ -166,8 +147,6 @@ Use individual flags to customize what gets deployed.`, func init() { cmdFlags := deployCmd.Flags() - deployCmd.Flags().StringSliceVar(&only, "only", []string{}, "Comma-separated list of components to deploy (e.g., db,storage,functions).") - 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)") From 0584ed7773ec39a7d18f7ee76924a2a84cc6191b Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Fri, 7 Nov 2025 15:26:44 -0600 Subject: [PATCH 12/20] use quick start group --- cmd/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/deploy.go b/cmd/deploy.go index 991a4916b..3a4dcfe6d 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -27,7 +27,7 @@ var ( deployIncludeSeed bool deployCmd = &cobra.Command{ - GroupID: groupLocalDev, + GroupID: groupQuickStart, Use: "deploy", Short: "Push all local changes to a Supabase project", Long: `Deploy local changes to a remote Supabase project. From e7ab596985911eefbfd19f768fd5cda323470e88 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Wed, 19 Nov 2025 17:28:05 -0600 Subject: [PATCH 13/20] address comments --- internal/functions/deploy/deploy.go | 2 +- pkg/function/batch.go | 8 -------- pkg/function/deploy.go | 1 - 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index c0917a075..f2429afa0 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -66,7 +66,7 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, // Deploy new and updated functions if err := api.Deploy(ctx, functionConfig, afero.NewIOFS(fsys), filter...); errors.Is(err, function.ErrNoDeploy) { fmt.Fprintln(os.Stderr, err) - return err + return nil } else if err != nil { return err } diff --git a/pkg/function/batch.go b/pkg/function/batch.go index 2bbdb00a3..fad409853 100644 --- a/pkg/function/batch.go +++ b/pkg/function/batch.go @@ -25,10 +25,6 @@ const ( ) func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { - return s.upsertFunctions(ctx, functionConfig, filter...) -} - -func (s *EdgeRuntimeAPI) upsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx) result, err := backoff.RetryWithData(func() ([]api.FunctionResponse, error) { resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project) @@ -51,9 +47,7 @@ func (s *EdgeRuntimeAPI) upsertFunctions(ctx context.Context, functionConfig con for i, f := range result { slugToIndex[f.Slug] = i } - var toUpdate api.BulkUpdateFunctionBody - OUTER: for slug, function := range functionConfig { if !function.Enabled { @@ -83,7 +77,6 @@ OUTER: fmt.Fprintln(os.Stderr, "No change found in Function:", slug) continue } - // Update if function already exists upsert := func() (api.BulkUpdateFunctionBody, error) { if _, exists := slugToIndex[slug]; exists { @@ -104,7 +97,6 @@ OUTER: toUpdate = append(toUpdate, result...) policy.Reset() } - if len(toUpdate) > 1 { if err := backoff.Retry(func() error { if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil { diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index 11fa0dfbb..0b0e389a0 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -49,7 +49,6 @@ func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.Funct } } if !shouldDeploy { - fmt.Fprintln(os.Stderr, "Would deploy:", slug) continue } toDeploy = append(toDeploy, meta) From 6608f1ff76353b22235ac58e291dbe43a3aa0c5d Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Wed, 26 Nov 2025 16:01:29 +0800 Subject: [PATCH 14/20] chore: refactor deploy implementation to internal package --- cmd/config.go | 6 +- cmd/deploy.go | 144 +----------------------------- go.mod | 1 - go.sum | 2 - internal/bootstrap/bootstrap.go | 27 +++--- internal/config/push/push_test.go | 4 +- internal/utils/spinner.go | 10 --- pkg/function/deploy.go | 16 ++-- 8 files changed, 28 insertions(+), 182 deletions(-) delete mode 100644 internal/utils/spinner.go diff --git a/cmd/config.go b/cmd/config.go index 3836a8b47..b382084ba 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,8 +8,6 @@ import ( ) var ( - configDryRun bool - configCmd = &cobra.Command{ GroupID: groupManagementAPI, Use: "config", @@ -20,14 +18,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, configDryRun, afero.NewOsFs()) + return push.Run(cmd.Context(), flags.ProjectRef, dryRun, 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.") + configPushCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Print operations that would be performed without executing them.") configCmd.AddCommand(configPushCmd) rootCmd.AddCommand(configCmd) } diff --git a/cmd/deploy.go b/cmd/deploy.go index 3a4dcfe6d..66209a033 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -1,31 +1,17 @@ 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/deploy" "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: groupQuickStart, Use: "deploy", @@ -38,104 +24,10 @@ By default, this command will: 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 := true - includeFunctions := true - includeConfig := true - - 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 + return deploy.Run(ctx, dryRun, fsys) }, Example: ` supabase deploy supabase deploy --include-config @@ -146,9 +38,7 @@ Use individual flags to customize what gets deployed.`, func init() { cmdFlags := deployCmd.Flags() - - cmdFlags.BoolVar(&deployDryRun, "dry-run", false, "Print operations that would be performed without executing them") - + cmdFlags.BoolVar(&dryRun, "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") @@ -156,33 +46,5 @@ func init() { 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 -} diff --git a/go.mod b/go.mod index 6423fd7f0..5ce13247d 100644 --- a/go.mod +++ b/go.mod @@ -391,7 +391,6 @@ require ( github.com/xhit/go-str2duration/v2 v2.1.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 diff --git a/go.sum b/go.sum index a1762577a..c18199eef 100644 --- a/go.sum +++ b/go.sum @@ -1109,8 +1109,6 @@ 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= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 6e93c0acb..cd6d8b2ce 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -107,7 +107,7 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options .. policy.Reset() if err := backoff.RetryNotify(func() error { fmt.Fprintln(os.Stderr, "Checking project health...") - return checkProjectHealth(ctx) + return CheckProjectHealth(ctx, flags.ProjectRef, api.Db) }, policy, utils.NewErrorCallback()); err != nil { return err } @@ -154,23 +154,26 @@ func suggestAppStart(cwd, command string) string { return suggestion } -func checkProjectHealth(ctx context.Context) error { - params := api.V1GetServicesHealthParams{ - Services: []api.V1GetServicesHealthParamsServices{api.Db}, - } - resp, err := utils.GetSupabase().V1GetServicesHealthWithResponse(ctx, flags.ProjectRef, ¶ms) +func CheckProjectHealth(ctx context.Context, projectRef string, services ...api.V1GetServicesHealthParamsServices) error { + params := api.V1GetServicesHealthParams{Services: services} + resp, err := utils.GetSupabase().V1GetServicesHealthWithResponse(ctx, projectRef, ¶ms) if err != nil { - return err - } - if resp.JSON200 == nil { - return errors.Errorf("Error status %d: %s", resp.StatusCode(), resp.Body) + return errors.Errorf("failed to check health: %w", err) + } else if resp.JSON200 == nil { + return errors.Errorf("unexpected health check status %d: %s", resp.StatusCode(), string(resp.Body)) } + var allErrors []error for _, service := range *resp.JSON200 { if !service.Healthy { - return errors.Errorf("Service not healthy: %s (%s)", service.Name, service.Status) + msg := string(service.Status) + if service.Error != nil { + msg = *service.Error + } + err := errors.Errorf("%s service not healthy: %s", service.Name, msg) + allErrors = append(allErrors, err) } } - return nil + return errors.Join(allErrors...) } const ( diff --git a/internal/config/push/push_test.go b/internal/config/push/push_test.go index 5af8fb273..f826db693 100644 --- a/internal/config/push/push_test.go +++ b/internal/config/push/push_test.go @@ -25,7 +25,7 @@ func TestPushConfig(t *testing.T) { fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteFile(utils.ConfigPath, []byte("malformed"), fsys)) // Run test - err := Run(context.Background(), "", fsys) + err := Run(context.Background(), "", false, fsys) // Check error assert.ErrorContains(t, err, "toml: expected = after a key, but the document ends there") }) @@ -39,7 +39,7 @@ func TestPushConfig(t *testing.T) { Get("/v1/projects/" + project + "/billing/addons"). Reply(http.StatusServiceUnavailable) // Run test - err := Run(context.Background(), project, fsys) + err := Run(context.Background(), project, false, fsys) // Check error assert.ErrorContains(t, err, "unexpected list addons status 503:") }) diff --git a/internal/utils/spinner.go b/internal/utils/spinner.go deleted file mode 100644 index 22a404adb..000000000 --- a/internal/utils/spinner.go +++ /dev/null @@ -1,10 +0,0 @@ -package utils - -import ( - "github.com/yarlson/pin" -) - -func NewSpinner(text string) *pin.Pin { - s := pin.New(text) - return s -} diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index 0b0e389a0..362d1a3b1 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -25,11 +25,17 @@ func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.Funct } // Convert all paths in functions config to relative when using api deploy var toDeploy []FunctionDeployMetadata +OUTER: for slug, fc := range functionConfig { if !fc.Enabled { fmt.Fprintln(os.Stderr, "Skipping disabled Function:", slug) continue } + for _, keep := range filter { + if !keep(slug) { + continue OUTER + } + } meta := FunctionDeployMetadata{ Name: &slug, EntrypointPath: toRelPath(fc.Entrypoint), @@ -41,16 +47,6 @@ func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.Funct files[i] = toRelPath(sf) } meta.StaticPatterns = &files - shouldDeploy := true - for _, keep := range filter { - if !keep(slug) { - shouldDeploy = false - break - } - } - if !shouldDeploy { - continue - } toDeploy = append(toDeploy, meta) } if len(toDeploy) == 0 { From b6a3e01eb999749d7d99b91c581d53158f7b1ef1 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Wed, 26 Nov 2025 18:07:02 +0800 Subject: [PATCH 15/20] chore: add missing package --- internal/deploy/deploy.go | 101 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 internal/deploy/deploy.go diff --git a/internal/deploy/deploy.go b/internal/deploy/deploy.go new file mode 100644 index 000000000..f5c13e55e --- /dev/null +++ b/internal/deploy/deploy.go @@ -0,0 +1,101 @@ +package deploy + +import ( + "context" + "fmt" + "os" + + "github.com/go-errors/errors" + "github.com/spf13/afero" + "github.com/supabase/cli/internal/bootstrap" + 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" +) + +func Run(ctx context.Context, dryRun bool, fsys afero.Fs) error { + fmt.Fprintln(os.Stderr, "Deploying to project:", flags.ProjectRef) + services := []api.V1GetServicesHealthParamsServices{ + api.Auth, + // Not checking Realtime for now as it can be flaky + // api.Realtime, + api.Rest, + api.Storage, + api.Db, + } + if err := bootstrap.CheckProjectHealth(ctx, flags.ProjectRef, services...); err != nil { + return err + } + + var deployErrors []error + + // Maybe deploy database migrations + fmt.Fprintln(os.Stderr, utils.Aqua(">>>"), "Deploying database migrations...") + if err := push.Run(ctx, dryRun, false, false, false, flags.DbConfig, fsys); err != nil { + deployErrors = append(deployErrors, errors.Errorf("db push failed: %w", err)) + } + fmt.Fprintln(os.Stderr) + + // Maybe deploy edge functions + fmt.Fprintln(os.Stderr, utils.Aqua(">>>"), "Deploying edge functions...") + keep := func(name string) bool { + if dryRun { + 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 dryRun { + 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 + fmt.Fprintln(os.Stderr, utils.Aqua(">>>"), "Deploying config...") + if err := configPush.Run(ctx, flags.ProjectRef, dryRun, 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 dryRun { + 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 dryRun { + 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 dryRun { + 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 +} From 297ee9858042221b4a97d00e3c8a91da1a52022e Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 1 Dec 2025 13:51:21 +0800 Subject: [PATCH 16/20] Revert "Use linked naming instead of remote for status cmd" This reverts commit 4c2492cb75cd25f128eebf474d8d28922fabf5bb. --- cmd/status.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/status.go b/cmd/status.go index ba7efd19d..8022b0616 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -15,12 +15,12 @@ import ( var ( override []string names status.CustomName - useLinkedProject bool + useRemoteProject bool statusCmd = &cobra.Command{ GroupID: groupLocalDev, Use: "status", - Short: "Show status of local Supabase containers or linked project", + Short: "Show status of local Supabase containers or remote project", PreRunE: func(cmd *cobra.Command, args []string) error { es, err := env.EnvironToEnvSet(override) if err != nil { @@ -30,7 +30,7 @@ var ( }, RunE: func(cmd *cobra.Command, args []string) error { ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) - if useLinkedProject { + if useRemoteProject { fmt.Fprintf(os.Stderr, "Project health check:\n") return status.RunRemote(ctx, utils.OutputFormat.Value, afero.NewOsFs()) } @@ -38,13 +38,13 @@ var ( }, Example: ` supabase status -o env --override-name api.url=NEXT_PUBLIC_SUPABASE_URL supabase status -o json - supabase status --linked`, + supabase status --remote`, } ) 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.") + flags.BoolVar(&useRemoteProject, "remote", false, "Check health of remote project.") rootCmd.AddCommand(statusCmd) } From 3b4039299aa2fb7acf2d1705243c6ab261cbd487 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 1 Dec 2025 13:52:18 +0800 Subject: [PATCH 17/20] Revert "Add remote flag to status" This reverts commit 4895f0598a60014aa50d6a452c83c0199ec9998e. --- cmd/root.go | 1 + cmd/status.go | 16 +++------ internal/status/status.go | 55 ----------------------------- internal/status/status_test.go | 63 ---------------------------------- internal/utils/colors.go | 10 ------ 5 files changed, 5 insertions(+), 140 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5a1072284..f0c945d3f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,6 +113,7 @@ var ( } } } + fmt.Println("Parsing database config from ROOT") if err := flags.ParseDatabaseConfig(ctx, cmd.Flags(), fsys); err != nil { return err } diff --git a/cmd/status.go b/cmd/status.go index 8022b0616..11b774854 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "os" "os/signal" @@ -13,14 +12,13 @@ import ( ) var ( - override []string - names status.CustomName - useRemoteProject bool + override []string + names status.CustomName statusCmd = &cobra.Command{ GroupID: groupLocalDev, Use: "status", - Short: "Show status of local Supabase containers or remote project", + Short: "Show status of local Supabase containers", PreRunE: func(cmd *cobra.Command, args []string) error { es, err := env.EnvironToEnvSet(override) if err != nil { @@ -30,21 +28,15 @@ var ( }, RunE: func(cmd *cobra.Command, args []string) error { ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) - if useRemoteProject { - 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 --remote`, + supabase status -o json`, } ) func init() { flags := statusCmd.Flags() flags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.") - flags.BoolVar(&useRemoteProject, "remote", false, "Check health of remote project.") rootCmd.AddCommand(statusCmd) } diff --git a/internal/status/status.go b/internal/status/status.go index 8174b93cc..62532d7ea 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -23,7 +23,6 @@ import ( "github.com/spf13/afero" "github.com/supabase/cli/internal/utils" "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/api" "github.com/supabase/cli/pkg/fetcher" ) @@ -375,57 +374,3 @@ func (g *OutputGroup) printTable(w io.Writer) error { return nil } - -func RunRemote(ctx context.Context, format string, fsys afero.Fs) error { - // Parse project ref - if err := flags.ParseProjectRef(ctx, fsys); err != nil { - return err - } - - // Define services to check - services := []api.V1GetServicesHealthParamsServices{ - api.Auth, - api.Realtime, - api.Rest, - api.Storage, - api.Db, - } - - // Call health check API - 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) - } - if resp.JSON200 == nil { - return errors.New("Unexpected error checking remote health: " + string(resp.Body)) - } - - // Print results - if format == utils.OutputPretty { - return prettyPrintRemoteHealth(os.Stdout, *resp.JSON200) - } - return utils.EncodeOutput(format, os.Stdout, resp.JSON200) -} - -func prettyPrintRemoteHealth(w io.Writer, health []api.V1ServiceHealthResponse) error { - fmt.Fprintf(w, "\n") - for _, service := range health { - statusSymbol := "✓" - statusColor := utils.Green - if !service.Healthy { - statusSymbol = "✗" - statusColor = utils.Red - } - - fmt.Fprintf(w, "%s %s %s\n", statusColor(statusSymbol), utils.Aqua(string(service.Name)), utils.Dim(string(service.Status))) - - if service.Error != nil && *service.Error != "" { - fmt.Fprintf(w, " Error: %s\n", utils.Red(*service.Error)) - } - } - fmt.Fprintf(w, "\n") - - return nil -} diff --git a/internal/status/status_test.go b/internal/status/status_test.go index ec5582541..c7bfc1bc4 100644 --- a/internal/status/status_test.go +++ b/internal/status/status_test.go @@ -14,8 +14,6 @@ import ( "github.com/stretchr/testify/require" "github.com/supabase/cli/internal/testing/apitest" "github.com/supabase/cli/internal/utils" - "github.com/supabase/cli/internal/utils/flags" - "github.com/supabase/cli/pkg/api" ) func TestStatusCommand(t *testing.T) { @@ -178,64 +176,3 @@ func TestPrintStatus(t *testing.T) { assert.Equal(t, "DB_URL = \"postgresql://postgres:postgres@127.0.0.1:0/postgres\"\n", stdout.String()) }) } - -func TestRemoteStatusCommand(t *testing.T) { - t.Run("shows remote health status", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - projectRef := apitest.RandomProjectRef() - require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte(projectRef), 0644)) - // Setup access token - token := apitest.RandomAccessToken(t) - t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) - // Setup mock API - defer gock.OffAll() - gock.New(utils.DefaultApiHost). - Get("/v1/projects/" + projectRef + "/health"). - ParamPresent("services"). - Reply(http.StatusOK). - JSON([]api.V1ServiceHealthResponse{ - { - Name: api.V1ServiceHealthResponseNameAuth, - Healthy: true, - Status: api.ACTIVEHEALTHY, - }, - { - Name: api.V1ServiceHealthResponseNameRealtime, - Healthy: true, - Status: api.ACTIVEHEALTHY, - }, - { - Name: api.V1ServiceHealthResponseNameRest, - Healthy: true, - Status: api.ACTIVEHEALTHY, - }, - { - Name: api.V1ServiceHealthResponseNameStorage, - Healthy: true, - Status: api.ACTIVEHEALTHY, - }, - { - Name: api.V1ServiceHealthResponseNameDb, - Healthy: true, - Status: api.ACTIVEHEALTHY, - }, - }) - // Run test - assert.NoError(t, RunRemote(context.Background(), utils.OutputPretty, fsys)) - // Check error - assert.Empty(t, apitest.ListUnmatchedRequests()) - }) - - t.Run("throws error on missing project ref", func(t *testing.T) { - // Setup in-memory fs - fsys := afero.NewMemMapFs() - defer gock.OffAll() - // Reset global state - flags.ProjectRef = "" - // Run test - err := RunRemote(context.Background(), utils.OutputPretty, fsys) - // Check error - assert.ErrorContains(t, err, "project ref") - }) -} diff --git a/internal/utils/colors.go b/internal/utils/colors.go index 1b57a4f5b..f4f82652c 100644 --- a/internal/utils/colors.go +++ b/internal/utils/colors.go @@ -26,13 +26,3 @@ func Red(str string) string { func Bold(str string) string { return lipgloss.NewStyle().Bold(true).Render(str) } - -// For success, healthy, etc. -func Green(str string) string { - return lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Render(str) -} - -// For secondary labels -func Dim(str string) string { - return lipgloss.NewStyle().Faint(true).Render(str) -} From 6e85645bf967f264f1286a1700e9372cefd2aa18 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 1 Dec 2025 13:53:02 +0800 Subject: [PATCH 18/20] chore: remove unnecessary log --- cmd/root.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index f0c945d3f..5a1072284 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -113,7 +113,6 @@ var ( } } } - fmt.Println("Parsing database config from ROOT") if err := flags.ParseDatabaseConfig(ctx, cmd.Flags(), fsys); err != nil { return err } From a318fe3f5ecdff25e3f36e5c4af5ac5e213d3321 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 1 Dec 2025 16:33:02 +0800 Subject: [PATCH 19/20] fix: dry run function deploy with pruning --- cmd/functions.go | 13 ++------- internal/deploy/deploy.go | 9 +----- internal/functions/deploy/deploy.go | 36 +++++++++++++++++------- internal/functions/deploy/deploy_test.go | 14 ++++----- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/cmd/functions.go b/cmd/functions.go index 1cfa4ac70..f3f7df13b 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -63,7 +62,6 @@ var ( noVerifyJWT = new(bool) importMapPath string prune bool - functionsDryRun bool functionsDeployCmd = &cobra.Command{ Use: "deploy [Function name]", @@ -79,14 +77,7 @@ var ( } else if maxJobs > 1 { return errors.New("--jobs must be used together with --use-api") } - 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) + return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, dryRun, afero.NewOsFs()) }, } @@ -154,7 +145,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.BoolVar(&dryRun, "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.") diff --git a/internal/deploy/deploy.go b/internal/deploy/deploy.go index f5c13e55e..ce42c76d9 100644 --- a/internal/deploy/deploy.go +++ b/internal/deploy/deploy.go @@ -42,14 +42,7 @@ func Run(ctx context.Context, dryRun bool, fsys afero.Fs) error { // Maybe deploy edge functions fmt.Fprintln(os.Stderr, utils.Aqua(">>>"), "Deploying edge functions...") - keep := func(name string) bool { - if dryRun { - 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) { + if err := funcDeploy.Run(ctx, []string{}, true, nil, "", 1, false, dryRun, 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) { diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index f2429afa0..a27d7acc0 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -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, filter ...(func(string) bool)) error { +func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, importMapPath string, maxJobs uint, prune, dryRun bool, fsys afero.Fs) error { // Load function config and project id if err := flags.LoadConfig(fsys); err != nil { return err @@ -51,7 +51,6 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, if err != nil { return err } - // Setup API with optional bundler opt := function.WithMaxJobs(maxJobs) if useDocker { @@ -62,22 +61,33 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, } } api := function.NewEdgeRuntimeAPI(flags.ProjectRef, *utils.GetSupabase(), opt) - // Deploy new and updated functions - if err := api.Deploy(ctx, functionConfig, afero.NewIOFS(fsys), filter...); errors.Is(err, function.ErrNoDeploy) { + var keep, drop []func(string) bool + if dryRun { + keep = append(keep, func(name string) bool { + fmt.Fprintln(os.Stderr, "Would deploy:", name) + return false + }) + drop = append(keep, func(name string) bool { + fmt.Fprintln(os.Stderr, "Would delete:", name) + return false + }) + } + if err := api.Deploy(ctx, functionConfig, afero.NewIOFS(fsys), keep...); errors.Is(err, function.ErrNoDeploy) { fmt.Fprintln(os.Stderr, err) return nil } else if err != nil { return err } - // TODO make this message conditional e.g. only when there are changes or not in dry run - fmt.Printf("Deployed Functions on project %s: %s\n", utils.Aqua(flags.ProjectRef), strings.Join(slugs, ", ")) - url := fmt.Sprintf("%s/project/%v/functions", utils.GetSupabaseDashboardURL(), flags.ProjectRef) - fmt.Println("You can inspect your deployment in the Dashboard: " + url) + if !dryRun { + fmt.Printf("Deployed Functions on project %s: %s\n", utils.Aqua(flags.ProjectRef), strings.Join(slugs, ", ")) + url := fmt.Sprintf("%s/project/%v/functions", utils.GetSupabaseDashboardURL(), flags.ProjectRef) + fmt.Println("You can inspect your deployment in the Dashboard: " + url) + } if !prune { return nil } - return pruneFunctions(ctx, functionConfig) + return pruneFunctions(ctx, functionConfig, drop...) } func GetFunctionSlugs(fsys afero.Fs) (slugs []string, err error) { @@ -167,7 +177,7 @@ func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool, } // pruneFunctions deletes functions that exist remotely but not locally -func pruneFunctions(ctx context.Context, functionConfig config.FunctionConfig) error { +func pruneFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { resp, err := utils.GetSupabase().V1ListAllFunctionsWithResponse(ctx, flags.ProjectRef) if err != nil { return errors.Errorf("failed to list functions: %w", err) @@ -195,7 +205,13 @@ func pruneFunctions(ctx context.Context, functionConfig config.FunctionConfig) e } else if !shouldDelete { return errors.New(context.Canceled) } +OUTER: for _, slug := range toDelete { + for _, keep := range filter { + if !keep(slug) { + continue OUTER + } + } fmt.Fprintln(os.Stderr, "Deleting Function:", slug) if err := delete.Undeploy(ctx, flags.ProjectRef, slug); errors.Is(err, delete.ErrNoDelete) { fmt.Fprintln(utils.GetDebugLogger(), err) diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go index e746da927..14c2cfb5f 100644 --- a/internal/functions/deploy/deploy_test.go +++ b/internal/functions/deploy/deploy_test.go @@ -75,7 +75,7 @@ func TestDeployCommand(t *testing.T) { } // Run test noVerifyJWT := true - err = Run(context.Background(), functions, true, &noVerifyJWT, "", 1, false, fsys) + err = Run(context.Background(), functions, true, &noVerifyJWT, "", 1, false, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -130,7 +130,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, true, nil, "", 1, false, fsys) + err = Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -183,7 +183,7 @@ import_map = "./import_map.json" outputDir := filepath.Join(utils.TempDir, ".output_enabled-func") require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - err = Run(context.Background(), nil, true, nil, "", 1, false, fsys) + err = Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) // Check error assert.NoError(t, err) assert.Empty(t, apitest.ListUnmatchedRequests()) @@ -194,7 +194,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), []string{"_invalid"}, true, nil, "", 1, false, fsys) + err := Run(context.Background(), []string{"_invalid"}, true, nil, "", 1, false, false, fsys) // Check error assert.ErrorContains(t, err, "Invalid Function name.") }) @@ -204,7 +204,7 @@ import_map = "./import_map.json" fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) // Run test - err := Run(context.Background(), nil, true, nil, "", 1, false, fsys) + err := Run(context.Background(), nil, true, nil, "", 1, false, false, fsys) // Check error assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions") }) @@ -250,7 +250,7 @@ verify_jwt = false outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test - assert.NoError(t, Run(context.Background(), []string{slug}, true, nil, "", 1, false, fsys)) + assert.NoError(t, Run(context.Background(), []string{slug}, true, nil, "", 1, false, false, fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) @@ -297,7 +297,7 @@ verify_jwt = false require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) // Run test noVerifyJWT := false - assert.NoError(t, Run(context.Background(), []string{slug}, true, &noVerifyJWT, "", 1, false, fsys)) + assert.NoError(t, Run(context.Background(), []string{slug}, true, &noVerifyJWT, "", 1, false, false, fsys)) // Validate api assert.Empty(t, apitest.ListUnmatchedRequests()) }) From 9c100ac60c1b8584c667e68d3aebea714d0b90f6 Mon Sep 17 00:00:00 2001 From: Qiao Han Date: Mon, 1 Dec 2025 17:27:14 +0800 Subject: [PATCH 20/20] chore: prompt before dry run config update --- internal/config/push/push.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/config/push/push.go b/internal/config/push/push.go index 5257a6219..35e003e60 100644 --- a/internal/config/push/push.go +++ b/internal/config/push/push.go @@ -29,18 +29,20 @@ func Run(ctx context.Context, ref string, dryRun bool, fsys afero.Fs) error { 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) } - shouldPush, err := console.PromptYesNo(ctx, title, true) - if err != nil { + if shouldPush, err := console.PromptYesNo(ctx, title, true); err != nil { fmt.Fprintln(os.Stderr, err) + } else if !shouldPush { + return false + } + if dryRun { + fmt.Fprintln(os.Stderr, "Would update config:", name) + return false } - return shouldPush + return true } return client.UpdateRemoteConfig(ctx, remote, keep) }