From 8270fcec945b5d550f191169c1b34ee40da3dc81 Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Tue, 21 Oct 2025 11:35:46 -0700 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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)")