From 4273734e1236ec4058ba1c8fc0a448662518a4c0 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Sat, 4 Jul 2026 15:29:17 -0400 Subject: [PATCH] fix(orchestrate): correct trigger glob matching for recursive and single-star patterns detectChanges reused a hand-rolled matchGlob that matched a recursive suffix glob (`**/*.go`, `pkg/**/*.ts`) against the literal suffix, so it never matched a real file and a needed build/deploy was silently skipped. The single-`*` branch used an unanchored substring search, so `*` crossed `/` and `foo*bar` matched `xfooybar`, over-triggering runs. Delegate matchGlob to the shared config.MatchGlobPattern evaluator (already used by internal/changes) so CLI-side change detection agrees with the emitted GitHub Actions paths filter: single `*` stays within one segment and `**` expands across any number of segments. Add table cases for the recursive-glob and anchoring behaviour. Signed-off-by: Joshua Temple --- internal/orchestrate/orchestrator.go | 45 ++++------------------- internal/orchestrate/orchestrator_test.go | 22 +++++++++-- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/internal/orchestrate/orchestrator.go b/internal/orchestrate/orchestrator.go index 7d9df8e9..4cb5c640 100644 --- a/internal/orchestrate/orchestrator.go +++ b/internal/orchestrate/orchestrator.go @@ -574,46 +574,15 @@ func indexOf(slice []string, item string) int { return -1 } -// matchGlob performs simple glob matching (supports * and **). +// matchGlob reports whether path matches the glob pattern. It delegates to the +// shared config.MatchGlobPattern evaluator so CLI-side change detection agrees +// with the emitted GitHub Actions paths filter, which is generated verbatim from +// the same pattern list. That evaluator anchors single "*" within one segment +// (it does not cross "/") and expands "**" across any number of segments, so a +// recursive glob such as "**/*.go" or "pkg/**/*.ts" matches files at any depth. func matchGlob(path, pattern string) bool { - // Simple implementation - handle common cases if pattern == "" { return false } - - // Handle ** (recursive match) - if strings.Contains(pattern, "**") { - parts := strings.Split(pattern, "**") - if len(parts) == 2 { - prefix := strings.TrimSuffix(parts[0], "/") - suffix := strings.TrimPrefix(parts[1], "/") - if prefix != "" && !strings.HasPrefix(path, prefix) { - return false - } - if suffix != "" && !strings.HasSuffix(path, suffix) { - return false - } - return true - } - } - - // Handle single * (match any characters except /) - if strings.Contains(pattern, "*") { - parts := strings.Split(pattern, "*") - pos := 0 - for _, part := range parts { - if part == "" { - continue - } - idx := strings.Index(path[pos:], part) - if idx < 0 { - return false - } - pos += idx + len(part) - } - return true - } - - // Exact match - return path == pattern + return config.MatchGlobPattern(pattern, path) } diff --git a/internal/orchestrate/orchestrator_test.go b/internal/orchestrate/orchestrator_test.go index e6dad8c2..f07a3053 100644 --- a/internal/orchestrate/orchestrator_test.go +++ b/internal/orchestrate/orchestrator_test.go @@ -134,16 +134,32 @@ func TestMatchGlob(t *testing.T) { {"exact match", "src/main.go", "src/main.go", true}, {"exact no match", "src/main.go", "src/other.go", false}, - // Single star patterns - {"star extension", "src/main.go", "*.go", true}, + // Single star patterns. A single "*" matches within one path segment and + // does not cross "/", matching the emitted GitHub Actions paths filter. + {"star extension root only", "main.go", "*.go", true}, + {"star extension does not cross slash", "src/main.go", "*.go", false}, {"star extension no match", "src/main.go", "*.ts", false}, {"star prefix", "test_main.go", "test_*", true}, + {"star single segment", "a/x/b", "a/*/b", true}, + {"star single segment no cross slash", "a/x/y/b", "a/*/b", false}, - // Double star patterns (our implementation handles prefix/** patterns) + // F12: a single "*" match is anchored, not an unanchored substring search. + {"star anchored no leading garbage", "xfooybar", "foo*bar", false}, + {"star anchored match", "fooybar", "foo*bar", true}, + + // Double star patterns. {"double star", "src/pkg/main.go", "src/**", true}, {"double star nested", "src/a/b/c/main.go", "src/**", true}, {"double star no match", "docs/readme.md", "src/**", false}, + // F01: a leading "**/" recursive glob followed by a segment glob must + // match files at any depth, not only the literal suffix. + {"recursive glob extension", "internal/foo.go", "**/*.go", true}, + {"recursive glob extension deep", "a/b/c/foo.go", "**/*.go", true}, + {"recursive glob extension no match", "internal/foo.ts", "**/*.go", false}, + {"recursive glob mid pattern", "pkg/a/b/c.ts", "pkg/**/*.ts", true}, + {"recursive glob mid pattern wrong prefix", "src/a/b/c.ts", "pkg/**/*.ts", false}, + // Common CI/CD patterns {"infra pattern", "infra/cdk/stack.ts", "infra/**", true}, {"go.mod pattern", "go.mod", "go.mod", true},