From 5f93ecd17cbb29d5b2d85f92fd114d26698ca774 Mon Sep 17 00:00:00 2001
From: "Alex Ellis (OpenFaaS Ltd)" <alexellis2@gmail.com>
Date: Fri, 30 Jun 2023 15:48:32 +0100
Subject: [PATCH 1/5] Implement --watch for localrun with cancellation support

This commit adds --watch support to localrun by using a context
to cancel and remove any running container when a change is
detected.

watchLoop has been updated to support a context.

Tested with VSCode - I suggest turning auto-save off when
using this feature.

Tested again with faas-cli up to make sure there was no
regression:

https://asciinema.org/a/594072

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
---
 commands/build.go     |   7 +-
 commands/local_run.go |  78 +++++++++++--
 commands/up.go        | 202 ++--------------------------------
 commands/watch.go     | 248 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 325 insertions(+), 210 deletions(-)
 create mode 100644 commands/watch.go

diff --git a/commands/build.go b/commands/build.go
index b37c56107..e71a241c0 100644
--- a/commands/build.go
+++ b/commands/build.go
@@ -192,7 +192,7 @@ func runBuild(cmd *cobra.Command, args []string) error {
 			return fmt.Errorf("please provide the deployed --name of your function")
 		}
 
-		err := builder.BuildImage(image,
+		if err := builder.BuildImage(image,
 			handler,
 			functionName,
 			language,
@@ -207,10 +207,10 @@ func runBuild(cmd *cobra.Command, args []string) error {
 			copyExtra,
 			remoteBuilder,
 			payloadSecretPath,
-		)
-		if err != nil {
+		); err != nil {
 			return err
 		}
+
 		return nil
 	}
 
@@ -222,6 +222,7 @@ func runBuild(cmd *cobra.Command, args []string) error {
 		}
 		return fmt.Errorf("%s", aec.Apply(errorSummary, aec.RedF))
 	}
+
 	return nil
 }
 
diff --git a/commands/local_run.go b/commands/local_run.go
index a9d96b5d3..024636d7f 100644
--- a/commands/local_run.go
+++ b/commands/local_run.go
@@ -4,16 +4,20 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"log"
 	"os"
 	"path/filepath"
 	"strings"
+	"syscall"
 
 	"os/exec"
+	"os/signal"
 
 	"github.com/openfaas/faas-cli/builder"
 	"github.com/openfaas/faas-cli/schema"
 	"github.com/openfaas/faas-cli/stack"
 	"github.com/spf13/cobra"
+	"golang.org/x/sync/errgroup"
 )
 
 const localSecretsDir = ".secrets"
@@ -78,7 +82,7 @@ services deployed within your OpenFaaS cluster.`,
 
 	cmd.Flags().StringVar(&opts.network, "network", "", "connect function to an existing network, use 'host' to access other process already running on localhost. When using this, '--port' is ignored, if you have port collisions, you may change the port using '-e port=NEW_PORT'")
 	cmd.Flags().StringToStringVarP(&opts.extraEnv, "env", "e", map[string]string{}, "additional environment variables (ENVVAR=VALUE), use this to experiment with different values for your function")
-	cmd.Flags().Bool("watch", false, "Watch for changes in handler code and rebuild")
+	cmd.Flags().BoolVar(&watch, "watch", false, "Watch for changes in files and re-deploy")
 
 	build, _, _ := faasCmd.Find([]string{"build"})
 	cmd.Flags().AddFlagSet(build.Flags())
@@ -98,18 +102,19 @@ func runLocalRunE(cmd *cobra.Command, args []string) error {
 		return watchLoop(cmd, args, localRunExec)
 	}
 
-	return localRunExec(cmd, args)
+	ctx, cancel := context.WithCancel(cmd.Context())
+	defer cancel()
+
+	return localRunExec(cmd, args, ctx)
 }
 
-func localRunExec(cmd *cobra.Command, args []string) error {
+func localRunExec(cmd *cobra.Command, args []string, ctx context.Context) error {
 	if opts.build {
 		if err := localBuild(cmd, args); err != nil {
 			return err
 		}
 	}
 
-	ctx := cmd.Context()
-
 	opts.output = cmd.OutOrStdout()
 	opts.err = cmd.ErrOrStderr()
 
@@ -187,7 +192,10 @@ func runFunction(ctx context.Context, name string, opts runOptions, args []strin
 		}
 	}
 
-	cmd, err := buildDockerRun(ctx, services.Functions[name], opts)
+	// Always try to remove before running, to clear up any previous state
+	removeContainer(name)
+
+	cmd, err := buildDockerRun(ctx, name, services.Functions[name], opts)
 	if err != nil {
 		return err
 	}
@@ -197,21 +205,67 @@ func runFunction(ctx context.Context, name string, opts runOptions, args []strin
 		return nil
 	}
 
+	sigs := make(chan os.Signal, 1)
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
 	cmd.Stdout = opts.output
 	cmd.Stderr = opts.err
 
 	fmt.Printf("Starting local-run for: %s on: http://0.0.0.0:%d\n\n", name, opts.port)
+	grpContext := context.Background()
+	grpContext, cancel := context.WithCancel(grpContext)
+	defer cancel()
 
-	if err = cmd.Start(); err != nil {
-		return err
-	}
+	errGrp, _ := errgroup.WithContext(grpContext)
+
+	errGrp.Go(func() error {
+		if err = cmd.Start(); err != nil {
+			return err
+		}
+
+		if err := cmd.Wait(); err != nil {
+			if strings.Contains(err.Error(), "signal: killed") {
+				return nil
+			} else if strings.Contains(err.Error(), "os: process already finished") {
+				return nil
+			}
+
+			return err
+		}
+		return nil
+	})
+
+	// Always try to remove the container
+	defer func() {
+		removeContainer(name)
+	}()
+
+	errGrp.Go(func() error {
+
+		select {
+		case <-sigs:
+			log.Printf("Caught signal, exiting")
+			cancel()
+		case <-ctx.Done():
+			log.Printf("Context cancelled, exiting..")
+			cancel()
+		}
+		return nil
+	})
+
+	return errGrp.Wait()
+}
+
+func removeContainer(name string) {
+
+	runDockerRm := exec.Command("docker", "rm", "-f", name)
+	runDockerRm.Run()
 
-	return cmd.Wait()
 }
 
 // buildDockerRun constructs a exec.Cmd from the given stack Function
-func buildDockerRun(ctx context.Context, fnc stack.Function, opts runOptions) (*exec.Cmd, error) {
-	args := []string{"run", "--rm", "-i", fmt.Sprintf("-p=%d:8080", opts.port)}
+func buildDockerRun(ctx context.Context, name string, fnc stack.Function, opts runOptions) (*exec.Cmd, error) {
+	args := []string{"run", "--name", name, "--rm", "-i", fmt.Sprintf("-p=%d:8080", opts.port)}
 
 	if opts.network != "" {
 		args = append(args, fmt.Sprintf("--network=%s", opts.network))
diff --git a/commands/up.go b/commands/up.go
index 67e4de1a3..49e1d3c0e 100644
--- a/commands/up.go
+++ b/commands/up.go
@@ -5,20 +5,12 @@ package commands
 
 import (
 	"bufio"
+	"context"
 	"fmt"
-	"log"
 	"os"
-	"os/signal"
-	"path"
-	"path/filepath"
 	"strings"
-	"syscall"
-	"time"
 
-	"github.com/bep/debounce"
-	"github.com/fsnotify/fsnotify"
 	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
-	"github.com/openfaas/faas-cli/stack"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 )
@@ -82,197 +74,17 @@ func preRunUp(cmd *cobra.Command, args []string) error {
 }
 
 func upHandler(cmd *cobra.Command, args []string) error {
-	return watchLoop(cmd, args, func(cmd *cobra.Command, args []string) error {
-		return upRunner(cmd, args)
-	})
-}
-
-func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Command, args []string) error) error {
-
-	// Always run an initial build to freshen up
-	if err := onChange(cmd, args); err != nil {
-		return err
-	}
-
-	var services stack.Services
-	if len(yamlFile) > 0 {
-		parsedServices, err := stack.ParseYAMLFile(yamlFile, regex, filter, envsubst)
-		if err != nil {
-			return err
-		}
-
-		if parsedServices != nil {
-			services = *parsedServices
-		}
-	}
-
-	fnNames := []string{}
-	for name, _ := range services.Functions {
-		fnNames = append(fnNames, name)
-	}
-
-	fmt.Printf("[Watch] monitoring %d functions: %s\n", len(fnNames), strings.Join(fnNames, ", "))
-
 	if watch {
-		watcher, err := fsnotify.NewWatcher()
-		if err != nil {
-			return err
-		}
-		defer watcher.Close()
-
-		patterns, err := ignorePatterns()
-		if err != nil {
-			return err
-		}
-
-		matcher := gitignore.NewMatcher(patterns)
-
-		cwd, err := os.Getwd()
-		if err != nil {
-			return err
-		}
-		yamlPath := path.Join(cwd, yamlFile)
-
-		debug := os.Getenv("FAAS_DEBUG")
-		if debug == "1" {
-			fmt.Printf("[Watch] added: %s\n", yamlPath)
-		}
-		watcher.Add(yamlPath)
-
-		// map to determine which function belongs to changed files
-		// when responding to events
-		handlerMap := make(map[string]string)
-
-		for serviceName, service := range services.Functions {
-			handlerMap[serviceName] = path.Join(cwd, service.Handler)
-
-			handlerFullPath := path.Join(cwd, service.Handler)
-
-			if err := addPath(watcher, handlerFullPath); err != nil {
-				return err
-			}
-		}
-
-		signalChannel := make(chan os.Signal, 1)
-		// Exit on Ctrl+C or kill
-		signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
-
-		d := debounce.New(2 * time.Second)
-
-		for {
-			select {
-			case event, ok := <-watcher.Events:
-				if !ok {
-					return fmt.Errorf("watcher's Events channel is closed")
-				}
-				log.Printf("%s %s", event.Op, event.Name)
-
-				if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") {
-					continue
-				}
-
-				if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename {
-
-					info, err := os.Stat(event.Name)
-					if err != nil {
-						continue
-					}
-					ignore := false
-					if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) {
-						ignore = true
-					}
-
-					// exact match first
-					target := ""
-					for fnName, fnPath := range handlerMap {
-						if event.Name == fnPath {
-							target = fnName
-						}
-					}
-
-					// fuzzy match after, if none matched exactly
-					if target == "" {
-						for fnName, fnPath := range handlerMap {
-							log.Printf("Checking %s against %s", event.Name, fnPath)
-
-							if strings.HasPrefix(event.Name, fnPath) {
-								target = fnName
-							}
-						}
-					}
-
-					// New sub-directory added for a function, start tracking it
-					if event.Op == fsnotify.Create && info.IsDir() && target != "" {
-						if err := addPath(watcher, event.Name); err != nil {
-							return err
-						}
-					}
-
-					if !ignore {
-						if target == "" {
-							fmt.Printf("Rebuilding %d functions reason: %s to %s\n", len(fnNames), event.Op, event.Name)
-						} else {
-							fmt.Printf("Reloading %s reason: %s to %s\n", target, event.Op, event.Name)
-						}
-					}
-
-					if !ignore {
-						d(func() {
-							filter = target
-
-							go func() {
-								// Assign --filter to "" for all functions if we can't determine the
-								// changed function to direct the calls to build/push/deploy
-
-								if err := onChange(cmd, args); err != nil {
-									fmt.Println("Error rebuilding: ", err)
-									os.Exit(1)
-								}
-							}()
-						})
-					}
-
-				}
-
-			case err, ok := <-watcher.Errors:
-				if !ok {
-					return fmt.Errorf("watcher's Errors channel is closed")
-				}
-				return err
-
-			case <-signalChannel:
-				watcher.Close()
-				return nil
-			}
-		}
+		return watchLoop(cmd, args, func(cmd *cobra.Command, args []string, ctx context.Context) error {
+			return upRunner(cmd, args, ctx)
+		})
 	}
-	return nil
-}
-
-func addPath(watcher *fsnotify.Watcher, rootPath string) error {
-	debug := os.Getenv("FAAS_DEBUG")
-
-	return filepath.WalkDir(rootPath, func(subPath string, d os.DirEntry, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if d.IsDir() {
-			if err := watcher.Add(subPath); err != nil {
-				return fmt.Errorf("unable to watch %s: %s", subPath, err)
-			}
-
-			if debug == "1" {
-				fmt.Printf("[Watch] added: %s\n", subPath)
-			}
-		}
-
-		return nil
-	})
 
+	ctx := context.Background()
+	return upRunner(cmd, args, ctx)
 }
 
-func upRunner(cmd *cobra.Command, args []string) error {
+func upRunner(cmd *cobra.Command, args []string, ctx context.Context) error {
 	if err := runBuild(cmd, args); err != nil {
 		return err
 	}
diff --git a/commands/watch.go b/commands/watch.go
new file mode 100644
index 000000000..813af36f5
--- /dev/null
+++ b/commands/watch.go
@@ -0,0 +1,248 @@
+package commands
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"path"
+	"path/filepath"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/bep/debounce"
+	"github.com/fsnotify/fsnotify"
+	"github.com/go-git/go-git/v5/plumbing/format/gitignore"
+	"github.com/openfaas/faas-cli/stack"
+	"github.com/spf13/cobra"
+)
+
+// watchLoop will watch for changes to function handler files and the stack.yml
+// then call onChange when a change is detected
+func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Command, args []string, ctx context.Context) error) error {
+
+	var services stack.Services
+	if len(yamlFile) > 0 {
+		parsedServices, err := stack.ParseYAMLFile(yamlFile, regex, filter, envsubst)
+		if err != nil {
+			return err
+		}
+
+		if parsedServices != nil {
+			services = *parsedServices
+		}
+	}
+
+	fnNames := []string{}
+	for name, _ := range services.Functions {
+		fnNames = append(fnNames, name)
+	}
+
+	fmt.Printf("[Watch] monitoring %d functions: %s\n", len(fnNames), strings.Join(fnNames, ", "))
+
+	canceller := Cancel{}
+
+	if watch {
+		watcher, err := fsnotify.NewWatcher()
+		if err != nil {
+			return err
+		}
+		defer watcher.Close()
+
+		patterns, err := ignorePatterns()
+		if err != nil {
+			return err
+		}
+
+		matcher := gitignore.NewMatcher(patterns)
+
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		yamlPath := path.Join(cwd, yamlFile)
+
+		debug := os.Getenv("FAAS_DEBUG")
+
+		if debug == "1" {
+			fmt.Printf("[Watch] added: %s\n", yamlPath)
+		}
+
+		watcher.Add(yamlPath)
+
+		// map to determine which function belongs to changed files
+		// when responding to events
+		handlerMap := make(map[string]string)
+
+		for serviceName, service := range services.Functions {
+			handlerMap[serviceName] = path.Join(cwd, service.Handler)
+
+			handlerFullPath := path.Join(cwd, service.Handler)
+
+			if err := addPath(watcher, handlerFullPath); err != nil {
+				return err
+			}
+		}
+
+		signalChannel := make(chan os.Signal, 1)
+
+		// Exit on Ctrl+C or kill
+		signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
+
+		bounce := debounce.New(1500 * time.Second)
+
+		var cancel context.CancelFunc
+		var ctx context.Context
+
+		// An initial build is usually done on first load with
+		// live reloaders
+		go bounce(func() {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+			canceller.Set(ctx, cancel)
+
+			if err := onChange(cmd, args, ctx); err != nil {
+				fmt.Println("Error rebuilding: ", err)
+				os.Exit(1)
+			}
+		})
+
+		for {
+			select {
+			case event, ok := <-watcher.Events:
+				if !ok {
+					return fmt.Errorf("watcher's Events channel is closed")
+				}
+
+				if debug == "1" {
+					log.Printf("[Watch] event: %s on: %s", event.Op, event.Name)
+				}
+				if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") {
+					continue
+				}
+
+				if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename {
+
+					info, err := os.Stat(event.Name)
+					if err != nil {
+						continue
+					}
+					ignore := false
+					if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) {
+						ignore = true
+					}
+
+					// exact match first
+					target := ""
+					for fnName, fnPath := range handlerMap {
+						if event.Name == fnPath {
+							target = fnName
+						}
+					}
+
+					// fuzzy match after, if none matched exactly
+					if target == "" {
+						for fnName, fnPath := range handlerMap {
+
+							if strings.HasPrefix(event.Name, fnPath) {
+								target = fnName
+							}
+						}
+					}
+
+					// New sub-directory added for a function, start tracking it
+					if event.Op == fsnotify.Create && info.IsDir() && target != "" {
+						if err := addPath(watcher, event.Name); err != nil {
+							return err
+						}
+					}
+
+					if !ignore {
+						if target == "" {
+							fmt.Printf("[Watch] Rebuilding %d functions reason: %s to %s\n", len(fnNames), event.Op, event.Name)
+						} else {
+							fmt.Printf("[Watch] Reloading %s reason: %s to %s\n", target, event.Op, event.Name)
+						}
+					}
+
+					if !ignore {
+						bounce(func() {
+
+							canceller.Cancel()
+
+							ctx, cancel = context.WithCancel(context.Background())
+							canceller.Set(ctx, cancel)
+
+							// Assign --filter to "" for all functions if we can't determine the
+							// changed function to direct the calls to build/push/deploy
+							filter = target
+
+							go func() {
+								if err := onChange(cmd, args, ctx); err != nil {
+									fmt.Println("Error rebuilding: ", err)
+									os.Exit(1)
+								}
+							}()
+						})
+					}
+
+				}
+
+			case err, ok := <-watcher.Errors:
+				if !ok {
+					return fmt.Errorf("watcher's Errors channel is closed")
+				}
+				return err
+
+			case <-signalChannel:
+				watcher.Close()
+				return nil
+			}
+		}
+	}
+	return nil
+}
+
+func addPath(watcher *fsnotify.Watcher, rootPath string) error {
+	debug := os.Getenv("FAAS_DEBUG")
+
+	return filepath.WalkDir(rootPath, func(subPath string, d os.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if d.IsDir() {
+			if err := watcher.Add(subPath); err != nil {
+				return fmt.Errorf("unable to watch %s: %s", subPath, err)
+			}
+
+			if debug == "1" {
+				fmt.Printf("[Watch] added: %s\n", subPath)
+			}
+		}
+
+		return nil
+	})
+
+}
+
+// Cancel is a struct to hold a reference to a context and
+// cancellation function between closures
+type Cancel struct {
+	cancel context.CancelFunc
+	ctx    context.Context
+}
+
+func (c *Cancel) Set(ctx context.Context, cancel context.CancelFunc) {
+	c.cancel = cancel
+	c.ctx = ctx
+}
+
+func (c *Cancel) Cancel() {
+	if c.cancel != nil {
+		c.cancel()
+	}
+
+}

From d230460862596112ae7601af778fb0f5c3c0a774 Mon Sep 17 00:00:00 2001
From: "Alex Ellis (OpenFaaS Ltd)" <alexellis2@gmail.com>
Date: Fri, 30 Jun 2023 16:10:30 +0100
Subject: [PATCH 2/5] Fix debounce from 1500s to 1500ms

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
---
 commands/up.go    |   8 +-
 commands/watch.go | 223 ++++++++++++++++++++++------------------------
 2 files changed, 111 insertions(+), 120 deletions(-)

diff --git a/commands/up.go b/commands/up.go
index 49e1d3c0e..9dbe44282 100644
--- a/commands/up.go
+++ b/commands/up.go
@@ -76,6 +76,9 @@ func preRunUp(cmd *cobra.Command, args []string) error {
 func upHandler(cmd *cobra.Command, args []string) error {
 	if watch {
 		return watchLoop(cmd, args, func(cmd *cobra.Command, args []string, ctx context.Context) error {
+
+			fmt.Println("[Watch] Change a file to trigger a rebuild...")
+
 			return upRunner(cmd, args, ctx)
 		})
 	}
@@ -99,11 +102,6 @@ func upRunner(cmd *cobra.Command, args []string, ctx context.Context) error {
 		if err := runDeploy(cmd, args); err != nil {
 			return err
 		}
-
-		if watch {
-			fmt.Println("[Watch] Change a file to trigger a rebuild...")
-		}
-
 	}
 
 	return nil
diff --git a/commands/watch.go b/commands/watch.go
index 813af36f5..a6ab122a2 100644
--- a/commands/watch.go
+++ b/commands/watch.go
@@ -44,164 +44,157 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma
 
 	canceller := Cancel{}
 
-	if watch {
-		watcher, err := fsnotify.NewWatcher()
-		if err != nil {
-			return err
-		}
-		defer watcher.Close()
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		return err
+	}
+	defer watcher.Close()
 
-		patterns, err := ignorePatterns()
-		if err != nil {
-			return err
-		}
+	patterns, err := ignorePatterns()
+	if err != nil {
+		return err
+	}
 
-		matcher := gitignore.NewMatcher(patterns)
+	matcher := gitignore.NewMatcher(patterns)
 
-		cwd, err := os.Getwd()
-		if err != nil {
-			return err
-		}
-		yamlPath := path.Join(cwd, yamlFile)
+	cwd, err := os.Getwd()
+	if err != nil {
+		return err
+	}
+	yamlPath := path.Join(cwd, yamlFile)
 
-		debug := os.Getenv("FAAS_DEBUG")
+	debug := os.Getenv("FAAS_DEBUG")
 
-		if debug == "1" {
-			fmt.Printf("[Watch] added: %s\n", yamlPath)
-		}
+	if debug == "1" {
+		fmt.Printf("[Watch] added: %s\n", yamlPath)
+	}
 
-		watcher.Add(yamlPath)
+	watcher.Add(yamlPath)
 
-		// map to determine which function belongs to changed files
-		// when responding to events
-		handlerMap := make(map[string]string)
+	// map to determine which function belongs to changed files
+	// when responding to events
+	handlerMap := make(map[string]string)
 
-		for serviceName, service := range services.Functions {
-			handlerMap[serviceName] = path.Join(cwd, service.Handler)
+	for serviceName, service := range services.Functions {
+		handlerMap[serviceName] = path.Join(cwd, service.Handler)
 
-			handlerFullPath := path.Join(cwd, service.Handler)
+		handlerFullPath := path.Join(cwd, service.Handler)
 
-			if err := addPath(watcher, handlerFullPath); err != nil {
-				return err
-			}
+		if err := addPath(watcher, handlerFullPath); err != nil {
+			return err
 		}
+	}
+
+	signalChannel := make(chan os.Signal, 1)
 
-		signalChannel := make(chan os.Signal, 1)
+	// Exit on Ctrl+C or kill
+	signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
 
-		// Exit on Ctrl+C or kill
-		signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM)
+	bounce := debounce.New(1500 * time.Millisecond)
 
-		bounce := debounce.New(1500 * time.Second)
+	// An initial build is usually done on first load with
+	// live reloaders
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	canceller.Set(ctx, cancel)
 
-		var cancel context.CancelFunc
-		var ctx context.Context
+	if err := onChange(cmd, args, ctx); err != nil {
+		fmt.Println("Error rebuilding: ", err)
+		os.Exit(1)
+	}
 
-		// An initial build is usually done on first load with
-		// live reloaders
-		go bounce(func() {
-			ctx, cancel := context.WithCancel(context.Background())
-			defer cancel()
-			canceller.Set(ctx, cancel)
+	for {
+		select {
+		case event, ok := <-watcher.Events:
+			if !ok {
+				return fmt.Errorf("watcher's Events channel is closed")
+			}
 
-			if err := onChange(cmd, args, ctx); err != nil {
-				fmt.Println("Error rebuilding: ", err)
-				os.Exit(1)
+			if debug == "1" {
+				log.Printf("[Watch] event: %s on: %s", strings.ToLower(event.Op.String()), event.Name)
+			}
+			if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") {
+				continue
 			}
-		})
 
-		for {
-			select {
-			case event, ok := <-watcher.Events:
-				if !ok {
-					return fmt.Errorf("watcher's Events channel is closed")
-				}
+			if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename {
 
-				if debug == "1" {
-					log.Printf("[Watch] event: %s on: %s", event.Op, event.Name)
-				}
-				if strings.HasSuffix(event.Name, ".swp") || strings.HasSuffix(event.Name, "~") || strings.HasSuffix(event.Name, ".swx") {
+				info, err := os.Stat(event.Name)
+				if err != nil {
 					continue
 				}
+				ignore := false
+				if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) {
+					ignore = true
+				}
 
-				if event.Op == fsnotify.Write || event.Op == fsnotify.Create || event.Op == fsnotify.Remove || event.Op == fsnotify.Rename {
-
-					info, err := os.Stat(event.Name)
-					if err != nil {
-						continue
-					}
-					ignore := false
-					if matcher.Match(strings.Split(event.Name, "/"), info.IsDir()) {
-						ignore = true
+				// exact match first
+				target := ""
+				for fnName, fnPath := range handlerMap {
+					if event.Name == fnPath {
+						target = fnName
 					}
+				}
 
-					// exact match first
-					target := ""
+				// fuzzy match after, if none matched exactly
+				if target == "" {
 					for fnName, fnPath := range handlerMap {
-						if event.Name == fnPath {
-							target = fnName
-						}
-					}
 
-					// fuzzy match after, if none matched exactly
-					if target == "" {
-						for fnName, fnPath := range handlerMap {
-
-							if strings.HasPrefix(event.Name, fnPath) {
-								target = fnName
-							}
+						if strings.HasPrefix(event.Name, fnPath) {
+							target = fnName
 						}
 					}
+				}
 
-					// New sub-directory added for a function, start tracking it
-					if event.Op == fsnotify.Create && info.IsDir() && target != "" {
-						if err := addPath(watcher, event.Name); err != nil {
-							return err
-						}
+				// New sub-directory added for a function, start tracking it
+				if event.Op == fsnotify.Create && info.IsDir() && target != "" {
+					if err := addPath(watcher, event.Name); err != nil {
+						return err
 					}
+				}
 
-					if !ignore {
-						if target == "" {
-							fmt.Printf("[Watch] Rebuilding %d functions reason: %s to %s\n", len(fnNames), event.Op, event.Name)
-						} else {
-							fmt.Printf("[Watch] Reloading %s reason: %s to %s\n", target, event.Op, event.Name)
-						}
+				if !ignore {
+					if target == "" {
+						fmt.Printf("[Watch] Rebuilding %d functions reason: %s to %s\n", len(fnNames), strings.ToLower(event.Op.String()), event.Name)
+					} else {
+						fmt.Printf("[Watch] Reloading %s reason: %s %s\n", target, strings.ToLower(event.Op.String()), event.Name)
 					}
 
-					if !ignore {
-						bounce(func() {
+					bounce(func() {
+						log.Printf("[Watch] Cancel previous build")
 
-							canceller.Cancel()
+						canceller.Cancel()
+						log.Printf("[Watch] Cancelled previous build")
+						ctx, cancel = context.WithCancel(context.Background())
+						canceller.Set(ctx, cancel)
 
-							ctx, cancel = context.WithCancel(context.Background())
-							canceller.Set(ctx, cancel)
-
-							// Assign --filter to "" for all functions if we can't determine the
-							// changed function to direct the calls to build/push/deploy
-							filter = target
-
-							go func() {
-								if err := onChange(cmd, args, ctx); err != nil {
-									fmt.Println("Error rebuilding: ", err)
-									os.Exit(1)
-								}
-							}()
-						})
-					}
+						// Assign --filter to "" for all functions if we can't determine the
+						// changed function to direct the calls to build/push/deploy
+						filter = target
 
+						go func() {
+							if err := onChange(cmd, args, ctx); err != nil {
+								fmt.Println("Error rebuilding: ", err)
+								os.Exit(1)
+							}
+						}()
+					})
 				}
 
-			case err, ok := <-watcher.Errors:
-				if !ok {
-					return fmt.Errorf("watcher's Errors channel is closed")
-				}
-				return err
+			}
 
-			case <-signalChannel:
-				watcher.Close()
-				return nil
+		case err, ok := <-watcher.Errors:
+			if !ok {
+				return fmt.Errorf("watcher's Errors channel is closed")
 			}
+			return err
+
+		case <-signalChannel:
+			watcher.Close()
+			return nil
 		}
 	}
+
 	return nil
 }
 

From bd295b0cfdcdb41885d9da3980865d653bd4cf61 Mon Sep 17 00:00:00 2001
From: "Alex Ellis (OpenFaaS Ltd)" <alexellis2@gmail.com>
Date: Fri, 30 Jun 2023 16:17:57 +0100
Subject: [PATCH 3/5] Move reload message for up command

This change moves the reload message to where it can be seen
instead of having it printed out mid-flow during the up
sequence.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
---
 commands/up.go | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/commands/up.go b/commands/up.go
index 9dbe44282..df14fe96b 100644
--- a/commands/up.go
+++ b/commands/up.go
@@ -77,9 +77,11 @@ func upHandler(cmd *cobra.Command, args []string) error {
 	if watch {
 		return watchLoop(cmd, args, func(cmd *cobra.Command, args []string, ctx context.Context) error {
 
+			if err := upRunner(cmd, args, ctx); err != nil {
+				return err
+			}
 			fmt.Println("[Watch] Change a file to trigger a rebuild...")
-
-			return upRunner(cmd, args, ctx)
+			return nil
 		})
 	}
 

From a4895028b7ed33d3ca5c63af8f0ea5c6d1dfe0c3 Mon Sep 17 00:00:00 2001
From: "Alex Ellis (OpenFaaS Ltd)" <alexellis2@gmail.com>
Date: Fri, 30 Jun 2023 16:44:42 +0100
Subject: [PATCH 4/5] Run initial command in a go routine

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
---
 commands/watch.go | 30 +++++++++++++++++-------------
 1 file changed, 17 insertions(+), 13 deletions(-)

diff --git a/commands/watch.go b/commands/watch.go
index a6ab122a2..80012af12 100644
--- a/commands/watch.go
+++ b/commands/watch.go
@@ -92,17 +92,20 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma
 
 	bounce := debounce.New(1500 * time.Millisecond)
 
-	// An initial build is usually done on first load with
-	// live reloaders
-	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
-	canceller.Set(ctx, cancel)
-
-	if err := onChange(cmd, args, ctx); err != nil {
-		fmt.Println("Error rebuilding: ", err)
-		os.Exit(1)
-	}
+	go func() {
+		// An initial build is usually done on first load with
+		// live reloaders
+		ctx, cancel := context.WithCancel(context.Background())
+		defer cancel()
+		canceller.Set(ctx, cancel)
+
+		if err := onChange(cmd, args, ctx); err != nil {
+			fmt.Println("Error rebuilding: ", err)
+			os.Exit(1)
+		}
+	}()
 
+	log.Printf("[Watch] Started")
 	for {
 		select {
 		case event, ok := <-watcher.Events:
@@ -161,11 +164,12 @@ func watchLoop(cmd *cobra.Command, args []string, onChange func(cmd *cobra.Comma
 					}
 
 					bounce(func() {
-						log.Printf("[Watch] Cancel previous build")
+						log.Printf("[Watch] Cancelling")
 
 						canceller.Cancel()
-						log.Printf("[Watch] Cancelled previous build")
-						ctx, cancel = context.WithCancel(context.Background())
+
+						log.Printf("[Watch] Cancelled")
+						ctx, cancel := context.WithCancel(context.Background())
 						canceller.Set(ctx, cancel)
 
 						// Assign --filter to "" for all functions if we can't determine the

From 0f8ccb8668a6901bbf5230342164fd8393e718fb Mon Sep 17 00:00:00 2001
From: "Alex Ellis (OpenFaaS Ltd)" <alexellis2@gmail.com>
Date: Fri, 30 Jun 2023 17:00:11 +0100
Subject: [PATCH 5/5] Enable switching out to publish + deploy

Publish and deploy is quicker than build push and deploy
because it doesn't run two commands and doesn't put data into
the local library first.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
---
 commands/up.go | 36 ++++++++++++++++++++++++++++--------
 1 file changed, 28 insertions(+), 8 deletions(-)

diff --git a/commands/up.go b/commands/up.go
index df14fe96b..6c4544bd7 100644
--- a/commands/up.go
+++ b/commands/up.go
@@ -18,12 +18,15 @@ import (
 var (
 	skipPush   bool
 	skipDeploy bool
+	usePublish bool
 	watch      bool
 )
 
 func init() {
 
 	upFlagset := pflag.NewFlagSet("up", pflag.ExitOnError)
+	upFlagset.BoolVar(&usePublish, "publish", false, "Use faas-cli publish instead of faas-cli build followed by faas-cli push")
+
 	upFlagset.BoolVar(&skipPush, "skip-push", false, "Skip pushing function to remote registry")
 	upFlagset.BoolVar(&skipDeploy, "skip-deploy", false, "Skip function deployment")
 	upFlagset.StringVar(&remoteBuilder, "remote-builder", "", "URL to the builder")
@@ -57,8 +60,19 @@ and the deploy step with --skip-deploy.
 
 Note: All flags from the build, push and deploy flags are valid and can be combined,
 see the --help text for those commands for details.`,
-	Example: `  faas-cli up -f myfn.yaml
-faas-cli up --filter "*gif*" --secret dockerhuborg`,
+	Example: `  # Deploy everything
+  faas-cli up
+
+  # Deploy a named function
+  faas-cli up --filter echo
+
+  # Deploy but skip the push step
+  faas-cli up --skip-push
+
+  # Build but skip pushing and use a build-arg
+  faas-cli up --skip-push \
+  	--build-arg GO111MODULE=on
+	`,
 	PreRunE: preRunUp,
 	RunE:    upHandler,
 }
@@ -90,14 +104,20 @@ func upHandler(cmd *cobra.Command, args []string) error {
 }
 
 func upRunner(cmd *cobra.Command, args []string, ctx context.Context) error {
-	if err := runBuild(cmd, args); err != nil {
-		return err
-	}
-
-	if !skipPush && remoteBuilder == "" {
-		if err := runPush(cmd, args); err != nil {
+	if usePublish {
+		if err := runPublish(cmd, args); err != nil {
 			return err
 		}
+	} else {
+		if err := runBuild(cmd, args); err != nil {
+			return err
+		}
+
+		if !skipPush && remoteBuilder == "" {
+			if err := runPush(cmd, args); err != nil {
+				return err
+			}
+		}
 	}
 
 	if !skipDeploy {
