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 new file mode 100644 index 000000000..991a4916b --- /dev/null +++ b/cmd/deploy.go @@ -0,0 +1,188 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + + "github.com/go-errors/errors" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/spf13/viper" + configPush "github.com/supabase/cli/internal/config/push" + "github.com/supabase/cli/internal/db/push" + funcDeploy "github.com/supabase/cli/internal/functions/deploy" + "github.com/supabase/cli/internal/utils" + "github.com/supabase/cli/internal/utils/flags" + "github.com/supabase/cli/pkg/api" + "github.com/supabase/cli/pkg/function" +) + +var ( + // Deploy flags + deployDryRun bool + deployIncludeAll bool + deployIncludeRoles bool + deployIncludeSeed bool + + deployCmd = &cobra.Command{ + GroupID: groupLocalDev, + Use: "deploy", + Short: "Push all local changes to a Supabase project", + Long: `Deploy local changes to a remote Supabase project. + +By default, this command will: + - Push database migrations (supabase db push) + - Deploy edge functions (supabase functions deploy) + +You can optionally include config changes with --include-config. +Use individual flags to customize what gets deployed.`, + // PreRunE: func(cmd *cobra.Command, args []string) error { + // return cmd.Root().PersistentPreRunE(cmd, args) + // }, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) + fsys := afero.NewOsFs() + + // Determine components to deploy + includeDb := true + includeFunctions := true + includeConfig := true + + fmt.Fprintln(os.Stderr, utils.Bold("Deploying to project:"), flags.ProjectRef) + + spinner := utils.NewSpinner("Connecting to project") + spinner.Start(context.Background()) + cancelSpinner := spinner.Start(context.Background()) + defer cancelSpinner() + if !isProjectHealthy(ctx) { + spinner.Fail("Project is not healthy. Please ensure all services are running before deploying.") + return errors.New("project is not healthy") + } + spinner.Stop("Connected to project") + + var deployErrors []error + + // Maybe deploy database migrations + if includeDb { + fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying database migrations...") + if err := push.Run(ctx, deployDryRun, deployIncludeAll, deployIncludeRoles, deployIncludeSeed, flags.DbConfig, fsys); err != nil { + deployErrors = append(deployErrors, errors.Errorf("db push failed: %w", err)) + return err // Stop on DB errors as functions might depend on schema + } + fmt.Fprintln(os.Stderr, "") + } + + // Maybe deploy edge functions + if includeFunctions { + fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying edge functions...") + keep := func(name string) bool { + if deployDryRun { + fmt.Fprintln(os.Stderr, utils.Yellow("⏭ ")+"Would deploy:", name) + return false + } + return true + } + if err := funcDeploy.Run(ctx, []string{}, true, nil, "", 1, false, fsys, keep); err != nil && !errors.Is(err, function.ErrNoDeploy) { + deployErrors = append(deployErrors, errors.Errorf("functions deploy failed: %w", err)) + fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:")+" Functions deployment failed:", err) + } else if errors.Is(err, function.ErrNoDeploy) { + fmt.Fprintln(os.Stderr, utils.Yellow("⏭ ")+"No functions to deploy") + } else { + // print error just in case + fmt.Fprintln(os.Stderr, err) + if deployDryRun { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Functions dry run complete") + } else { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Functions deployed successfully") + } + } + fmt.Fprintln(os.Stderr, "") + } + + // Maybe deploy config + if includeConfig { + fmt.Fprintln(os.Stderr, utils.Aqua(">>>")+" Deploying config...") + if err := configPush.Run(ctx, flags.ProjectRef, deployDryRun, fsys); err != nil { + deployErrors = append(deployErrors, errors.Errorf("config push failed: %w", err)) + fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:")+" Config deployment failed:", err) + } else { + if deployDryRun { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config dry run complete") + } else { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" Config deployed successfully") + } + } + fmt.Fprintln(os.Stderr, "") + } + + // Summary + if len(deployErrors) > 0 { + if deployDryRun { + fmt.Fprintln(os.Stderr, utils.Yellow("Dry run completed with warnings:")) + } else { + fmt.Fprintln(os.Stderr, utils.Yellow("Deploy completed with warnings:")) + } + for _, err := range deployErrors { + fmt.Fprintln(os.Stderr, " •", err) + } + return nil // Don't fail the command for non-critical errors + } + + if deployDryRun { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" "+utils.Bold("Dry run completed successfully!")) + } else { + fmt.Fprintln(os.Stderr, utils.Aqua("✓")+" "+utils.Bold("Deployment completed successfully!")) + } + return nil + }, + Example: ` supabase deploy + supabase deploy --include-config + supabase deploy --include-db --include-functions + supabase deploy --dry-run`, + } +) + +func init() { + cmdFlags := deployCmd.Flags() + + cmdFlags.BoolVar(&deployDryRun, "dry-run", false, "Print operations that would be performed without executing them") + + cmdFlags.String("db-url", "", "Deploys to the database specified by the connection string (must be percent-encoded)") + cmdFlags.Bool("linked", true, "Deploys to the linked project") + cmdFlags.Bool("local", false, "Deploys to the local database") + deployCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local") + cmdFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database") + cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", cmdFlags.Lookup("password"))) + cmdFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project") + + rootCmd.AddCommand(deployCmd) +} +func isProjectHealthy(ctx context.Context) bool { + services := []api.V1GetServicesHealthParamsServices{ + api.Auth, + // Not checking Realtime for now as it can be flaky + // api.Realtime, + api.Rest, + api.Storage, + api.Db, + } + resp, err := utils.GetSupabase().V1GetServicesHealthWithResponse(ctx, flags.ProjectRef, &api.V1GetServicesHealthParams{ + Services: services, + }) + if err != nil { + // return errors.Errorf("failed to check remote health: %w", err) + return false + } + if resp.JSON200 == nil { + // return errors.New("Unexpected error checking remote health: " + string(resp.Body)) + return false + } + for _, service := range *resp.JSON200 { + if !service.Healthy { + return false + } + } + return true +} diff --git a/cmd/functions.go b/cmd/functions.go index 6b5684bc2..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" @@ -59,6 +60,7 @@ var ( noVerifyJWT = new(bool) importMapPath string prune bool + functionsDryRun bool functionsDeployCmd = &cobra.Command{ Use: "deploy [Function name]", @@ -74,7 +76,14 @@ var ( } else if maxJobs > 1 { return errors.New("--jobs must be used together with --use-api") } - return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, afero.NewOsFs()) + keep := func(name string) bool { + if functionsDryRun { + fmt.Fprintln(os.Stderr, "Would deploy:", name) + return false + } + return true + } + return deploy.Run(cmd.Context(), args, useDocker, noVerifyJWT, importMapPath, maxJobs, prune, afero.NewOsFs(), keep) }, } @@ -141,6 +150,7 @@ func init() { deployFlags.UintVarP(&maxJobs, "jobs", "j", 1, "Maximum number of parallel jobs.") deployFlags.BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") deployFlags.BoolVar(&prune, "prune", false, "Delete Functions that exist in Supabase project but not locally.") + deployFlags.BoolVar(&functionsDryRun, "dry-run", false, "Print operations that would be performed without executing them.") deployFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") deployFlags.StringVar(&importMapPath, "import-map", "", "Path to import map file.") functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.") diff --git a/cmd/status.go b/cmd/status.go index 11b774854..ba7efd19d 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "os" "os/signal" @@ -12,13 +13,14 @@ import ( ) var ( - override []string - names status.CustomName + override []string + names status.CustomName + useLinkedProject bool statusCmd = &cobra.Command{ GroupID: groupLocalDev, Use: "status", - Short: "Show status of local Supabase containers", + Short: "Show status of local Supabase containers or linked project", PreRunE: func(cmd *cobra.Command, args []string) error { es, err := env.EnvironToEnvSet(override) if err != nil { @@ -28,15 +30,21 @@ var ( }, RunE: func(cmd *cobra.Command, args []string) error { ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt) + if useLinkedProject { + fmt.Fprintf(os.Stderr, "Project health check:\n") + return status.RunRemote(ctx, utils.OutputFormat.Value, afero.NewOsFs()) + } return status.Run(ctx, names, utils.OutputFormat.Value, afero.NewOsFs()) }, Example: ` supabase status -o env --override-name api.url=NEXT_PUBLIC_SUPABASE_URL - supabase status -o json`, + supabase status -o json + supabase status --linked`, } ) func init() { flags := statusCmd.Flags() flags.StringSliceVar(&override, "override-name", []string{}, "Override specific variable names.") + flags.BoolVar(&useLinkedProject, "linked", false, "Check health of linked project.") rootCmd.AddCommand(statusCmd) } 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/config/push/push.go b/internal/config/push/push.go index 1a2340060..5257a6219 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,9 +26,12 @@ func Run(ctx context.Context, ref string, fsys afero.Fs) error { if err != nil { return err } - fmt.Fprintln(os.Stderr, "Pushing config to project:", remote.ProjectId) + fmt.Fprintln(os.Stderr, "Checking config for project:", remote.ProjectId) console := utils.NewConsole() keep := func(name string) bool { + if dryRun { + return false + } title := fmt.Sprintf("Do you want to push %s config to remote?", name) if item, exists := cost[name]; exists { title = fmt.Sprintf("Enabling %s will cost you %s. Keep it enabled?", item.Name, item.Price) diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index bcee53c55..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, 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 @@ -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,12 +62,15 @@ 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) { + + // Deploy new and updated functions + if err := api.Deploy(ctx, functionConfig, afero.NewIOFS(fsys), filter...); errors.Is(err, function.ErrNoDeploy) { fmt.Fprintln(os.Stderr, err) - return nil + 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/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) +} 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 +} diff --git a/pkg/function/batch.go b/pkg/function/batch.go index fad409853..2bbdb00a3 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, filter...) +} + +func (s *EdgeRuntimeAPI) upsertFunctions(ctx context.Context, functionConfig config.FunctionConfig, filter ...func(string) bool) error { policy := backoff.WithContext(backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries), ctx) result, err := backoff.RetryWithData(func() ([]api.FunctionResponse, error) { resp, err := s.client.V1ListAllFunctionsWithResponse(ctx, s.project) @@ -47,7 +51,9 @@ func (s *EdgeRuntimeAPI) UpsertFunctions(ctx context.Context, functionConfig con for i, f := range result { slugToIndex[f.Slug] = i } + var toUpdate api.BulkUpdateFunctionBody + OUTER: for slug, function := range functionConfig { if !function.Enabled { @@ -77,6 +83,7 @@ OUTER: fmt.Fprintln(os.Stderr, "No change found in Function:", slug) continue } + // Update if function already exists upsert := func() (api.BulkUpdateFunctionBody, error) { if _, exists := slugToIndex[slug]; exists { @@ -97,6 +104,7 @@ OUTER: toUpdate = append(toUpdate, result...) policy.Reset() } + if len(toUpdate) > 1 { if err := backoff.Retry(func() error { if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil { diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index 88e2f464a..11fa0dfbb 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -19,9 +19,9 @@ import ( var ErrNoDeploy = errors.New("All Functions are up to date.") -func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error { +func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS, filter ...func(string) bool) 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 @@ -41,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 {