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]