From 9342b0f9261ef56c21b69c47efdf30c7d08ae7db Mon Sep 17 00:00:00 2001 From: Daniel Vianna <1708810+pasunboneleve@users.noreply.github.com> Date: Wed, 24 Jun 2026 06:26:41 +1000 Subject: [PATCH 1/2] fix(watch): literal directory patterns must match nested files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A trailing-slash directory pattern (e.g. `apps/api/src/`) is compiled into a *recursive* watch target, so the OS backend watches the directory and delivers events for files inside it. But the change→workflow matcher built the GlobSet from the raw pattern, and globset treats `apps/api/src/` as a literal that does not match `apps/api/src/foo.ts`. Result: the directory is watched, events arrive, classify_events drops them, and the workflow never fires — edits to a watched directory silently failed to restart the process. Expand a literal trailing-slash directory to `/**` when building the matcher, so it matches nested files — consistent with the recursive watch target and the documented behaviour. Adds regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/config.rs | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 07d05a1..c987acd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -195,7 +195,7 @@ impl WatchGroup { let mut builder = GlobSetBuilder::new(); for pattern in &self.paths { builder - .add(Glob::new(pattern).with_context(|| { + .add(Glob::new(&matcher_pattern(pattern)).with_context(|| { format!("invalid glob '{pattern}' in watch group '{name}'") })?); } @@ -221,7 +221,7 @@ impl CompiledWatchGroup { pub fn for_test(patterns: &[&str], workflow: &str) -> Result { let mut builder = GlobSetBuilder::new(); for pattern in patterns { - builder.add(Glob::new(pattern)?); + builder.add(Glob::new(&matcher_pattern(pattern))?); } Ok(Self { workflow: workflow.to_owned(), @@ -322,6 +322,22 @@ fn pattern_is_literal(pattern: &str) -> bool { !pattern.split('/').any(segment_has_glob_magic) && !pattern.contains("**") } +/// Glob used to match change events for a watch pattern. +/// +/// A literal directory target (trailing `/`) is registered as a *recursive* +/// watch target (see [`CompiledWatchTarget::from_pattern`]), so its matcher +/// must also match files nested inside it — not just the bare directory path. +/// Without this, the directory is watched and events arrive, but the bare +/// `apps/api/src/` glob never matches `apps/api/src/foo.ts`, so no workflow +/// fires. Expand such patterns to `apps/api/src/**`. +fn matcher_pattern(pattern: &str) -> String { + if pattern.ends_with('/') && pattern_is_literal(pattern) { + format!("{pattern}**") + } else { + pattern.to_owned() + } +} + fn segment_has_glob_magic(segment: &str) -> bool { segment.contains('*') || segment.contains('?') || segment.contains('[') || segment.contains('{') } @@ -1383,6 +1399,24 @@ mod tests { ); } + #[test] + fn literal_directory_pattern_matches_nested_files() { + // Regression: a trailing-slash directory is watched recursively, so its + // matcher must match files nested inside it — not just the bare dir. + let group = CompiledWatchGroup::for_test(&["apps/api/src/"], "api").unwrap(); + assert!(group.matches(Path::new("apps/api/src/dimensions.ts"))); + assert!(group.matches(Path::new("apps/api/src/nested/deep.ts"))); + assert!(!group.matches(Path::new("apps/web/src/page.tsx"))); + } + + #[test] + fn literal_file_pattern_matches_only_that_file() { + // A pattern without a trailing slash stays a single-file target. + let group = CompiledWatchGroup::for_test(&["Cargo.toml"], "rust").unwrap(); + assert!(group.matches(Path::new("Cargo.toml"))); + assert!(!group.matches(Path::new("src/Cargo.toml"))); + } + #[test] fn config_detects_notify_reload_in_nested_workflow() { let mut config = base_config(); From 5a4339dd071bf8bdd96e7c572ecfcdd2b918303e Mon Sep 17 00:00:00 2001 From: Daniel Vianna <1708810+pasunboneleve@users.noreply.github.com> Date: Wed, 24 Jun 2026 07:04:53 +1000 Subject: [PATCH 2/2] release: devloop 0.9.1 Finalize CHANGELOG [0.9.1] (literal trailing-slash directory watch fix + the gh-release action update) and bump the package version 0.9.0 -> 0.9.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 906303a..12188b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,21 @@ All notable changes to `devloop` will be recorded in this file. ## [Unreleased] +## [0.9.1] - 2026-06-24 + ### Changed - Updated the GitHub release publishing action to `softprops/action-gh-release@v3.0.0`, which uses the Node 24 action runtime. +### Fixed +- Literal trailing-slash directory watch patterns (for example + `content/`) now match files nested inside the directory, so edits + under a watched directory trigger its workflow. The directory was + already watched recursively, but the change-to-workflow matcher used + the bare pattern, which `globset` treats as a literal that never + matches nested files, so no workflow ran. + ## [0.9.0] - 2026-05-04 ### Added diff --git a/Cargo.lock b/Cargo.lock index 8a679e2..07a8b25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,7 +235,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "devloop" -version = "0.9.0" +version = "0.9.1" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index fb56773..34848f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "devloop" -version = "0.9.0" +version = "0.9.1" edition = "2024" [dependencies]