diff --git a/internal/config/triggers.go b/internal/config/triggers.go index 31d762eb..a56f0c77 100644 --- a/internal/config/triggers.go +++ b/internal/config/triggers.go @@ -1,7 +1,7 @@ package config import ( - "path/filepath" + "path" "strings" ) @@ -81,18 +81,18 @@ func MatchAnyTrigger(patterns []string, changedFiles []string) bool { // level by MatchTrigger. Supports "*" (single segment), "**" (any number of // segments) and "?" (single character), matching the grammar used by the // emitted GitHub Actions paths filter. -func MatchGlobPattern(pattern, path string) bool { - return matchGlobPattern(stripNegation(pattern), path) +func MatchGlobPattern(pattern, filePath string) bool { + return matchGlobPattern(stripNegation(pattern), filePath) } // matchGlobPattern matches a file path against a single (already // negation-stripped) glob pattern. -func matchGlobPattern(pattern, path string) bool { +func matchGlobPattern(pattern, filePath string) bool { if strings.Contains(pattern, "**") { - return matchDoublestarPattern(pattern, path) + return matchDoublestarPattern(pattern, filePath) } - matched, _ := filepath.Match(pattern, path) + matched, _ := path.Match(pattern, filePath) return matched } @@ -103,10 +103,10 @@ func matchDoublestarPattern(pattern, path string) bool { return matchGlobParts(patternParts, pathParts) } -func matchGlobParts(pattern, path []string) bool { +func matchGlobParts(pattern, pathParts []string) bool { pi, ppi := 0, 0 - for pi < len(pattern) && ppi < len(path) { + for pi < len(pattern) && ppi < len(pathParts) { if pattern[pi] == "**" { if pi == len(pattern)-1 { // "**" at the end matches everything remaining. @@ -114,15 +114,15 @@ func matchGlobParts(pattern, path []string) bool { } // Try matching the remaining pattern at each subsequent position. - for i := ppi; i <= len(path); i++ { - if matchGlobParts(pattern[pi+1:], path[i:]) { + for i := ppi; i <= len(pathParts); i++ { + if matchGlobParts(pattern[pi+1:], pathParts[i:]) { return true } } return false } - matched, _ := filepath.Match(pattern[pi], path[ppi]) + matched, _ := path.Match(pattern[pi], pathParts[ppi]) if !matched { return false } @@ -139,5 +139,5 @@ func matchGlobParts(pattern, path []string) bool { pi++ } - return ppi == len(path) + return ppi == len(pathParts) } diff --git a/internal/config/triggers_test.go b/internal/config/triggers_test.go index 16f0f8bc..e827161d 100644 --- a/internal/config/triggers_test.go +++ b/internal/config/triggers_test.go @@ -93,6 +93,38 @@ func TestIsNegationPattern(t *testing.T) { } } +// TestMatchGlobPattern_SlashBoundary locks in the slash-native matching +// semantics that path.Match provides: a single "*" matches within one path +// segment and never crosses a "/", matching the GitHub Actions paths filter +// grammar this evaluator is meant to mirror. These cases pass on Linux/macOS +// under either path.Match or filepath.Match (the separator is "/" on both), +// so they are guard/documentation tests that would catch a future regression +// to a matcher whose separator is platform-dependent; the Windows behavioral +// delta (filepath.Match treating "/" as an ordinary character) cannot be +// exercised on Linux CI. +func TestMatchGlobPattern_SlashBoundary(t *testing.T) { + tests := []struct { + name string + pattern string + file string + want bool + }{ + {"single star does not cross slash", "a/*/b", "a/x/y/b", false}, + {"single star matches one segment", "a/*/b", "a/x/b", true}, + {"dir star does not descend", "dir/*", "dir/sub/file", false}, + {"dir star matches direct child", "dir/*", "dir/file", true}, + {"doublestar still matches nested files", "**/*.go", "a/b/c.go", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MatchGlobPattern(tt.pattern, tt.file); got != tt.want { + t.Errorf("MatchGlobPattern(%q, %q) = %v, want %v", tt.pattern, tt.file, got, tt.want) + } + }) + } +} + func TestMatchGlobPattern_StripsNegation(t *testing.T) { // The bare-glob helper matches whether the glob (negation stripped) matches. if !MatchGlobPattern("!**/*.md", "docs/README.md") {