From 39791356e716dc9ef830ca1b8b80c1bd28f7a322 Mon Sep 17 00:00:00 2001 From: Eric Case Date: Mon, 1 Jun 2026 13:07:21 -0700 Subject: [PATCH 1/2] fix(compute): ignore attribute-only (Chmod) events in serve --watch --- pkg/commands/compute/serve.go | 13 +++++++++ pkg/commands/compute/serve_internal_test.go | 32 +++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 pkg/commands/compute/serve_internal_test.go diff --git a/pkg/commands/compute/serve.go b/pkg/commands/compute/serve.go index 66fe3f72e..7229c9fca 100644 --- a/pkg/commands/compute/serve.go +++ b/pkg/commands/compute/serve.go @@ -1295,6 +1295,10 @@ func watchFiles(root string, gi *ignore.GitIgnore, verbose bool, s *fstexec.Stre if !ok { return } + // Ignore attribute-only (Chmod) events; on macOS an atime bump alone would loop. + if !isContentChange(event.Op) { + continue + } debounced(func() { eventHandler(event.Name, event.Op) }) @@ -1393,6 +1397,15 @@ func readIgnoreFile(path string) (lines []string) { return strings.Split(string(bs), "\n") } +// isContentChange reports whether op is a content or existence change +// (Create/Write/Remove/Rename) and not an attribute-only Chmod. fsnotify +// recommends ignoring Chmod; see its "Why do I get many Chmod events?" FAQ: +// https://github.com/fsnotify/fsnotify#why-do-i-get-many-chmod-events +func isContentChange(op fsnotify.Op) bool { + return op.Has(fsnotify.Create) || op.Has(fsnotify.Write) || + op.Has(fsnotify.Remove) || op.Has(fsnotify.Rename) +} + func watchFile(path string, watcher *fsnotify.Watcher, verbose bool, out io.Writer) { absolute, err := filepath.Abs(path) if err != nil && verbose { diff --git a/pkg/commands/compute/serve_internal_test.go b/pkg/commands/compute/serve_internal_test.go new file mode 100644 index 000000000..e2007d64f --- /dev/null +++ b/pkg/commands/compute/serve_internal_test.go @@ -0,0 +1,32 @@ +package compute + +import ( + "testing" + + "github.com/fsnotify/fsnotify" +) + +// TestIsContentChange checks that content/existence events trigger a rebuild and +// attribute-only (Chmod) events do not. +func TestIsContentChange(t *testing.T) { + scenarios := []struct { + name string + op fsnotify.Op + want bool + }{ + {name: "create", op: fsnotify.Create, want: true}, + {name: "write", op: fsnotify.Write, want: true}, + {name: "remove", op: fsnotify.Remove, want: true}, + {name: "rename", op: fsnotify.Rename, want: true}, + {name: "chmod only", op: fsnotify.Chmod, want: false}, + {name: "write combined with chmod", op: fsnotify.Write | fsnotify.Chmod, want: true}, + {name: "no op", op: 0, want: false}, + } + for _, testcase := range scenarios { + t.Run(testcase.name, func(t *testing.T) { + if got := isContentChange(testcase.op); got != testcase.want { + t.Errorf("isContentChange(%s) = %v, want %v", testcase.op, got, testcase.want) + } + }) + } +} From 06157c924334bb0e94d58ea258fe9b8a3fbde248 Mon Sep 17 00:00:00 2001 From: Eric Case Date: Mon, 1 Jun 2026 13:24:08 -0700 Subject: [PATCH 2/2] Changelog entry for PR #1808 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cae91809..cd9886744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Bug Fixes: - fix(docs): corrected stale and missing API reference links in usage.json metadata([#1803](https://github.com/fastly/cli/pull/1803)) +- fix(compute): `serve --watch` no longer rebuilds on attribute-only (Chmod) filesystem events, preventing an endless rebuild loop when another process changes a watched file's metadata such as its access time ([#1808](https://github.com/fastly/cli/pull/1808)) ### Enhancements: