Skip to content

Commit 679d058

Browse files
committed
feat: add wildcard suport for repository
1 parent f05bcd0 commit 679d058

4 files changed

Lines changed: 317 additions & 6 deletions

File tree

pkg/matcher/repo_runinfo_matcher.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"regexp"
78
"strings"
89

910
"github.com/gobwas/glob"
@@ -25,11 +26,14 @@ func MatchEventURLRepo(ctx context.Context, cs *params.Run, event *info.Event, n
2526
}
2627
for _, repo := range repositories.Items {
2728
repo.Spec.URL = strings.TrimSuffix(repo.Spec.URL, "/")
28-
if repo.Spec.URL == event.URL {
29+
match, err := matchRepo(event.URL, repo.Spec.URL)
30+
if err != nil {
31+
return nil, err
32+
}
33+
if match {
2934
return &repo, nil
3035
}
3136
}
32-
3337
return nil, nil
3438
}
3539

@@ -89,3 +93,26 @@ func matchTarget(branch, target string) (bool, error) {
8993

9094
return g.Match(branch), nil
9195
}
96+
97+
// matchTarget checks if a branch matches a target pattern using glob matching.
98+
// Supports both exact string matching and glob patterns.
99+
func matchRepo(repo, target string) (bool, error) {
100+
if target == repo {
101+
return true, nil
102+
}
103+
// Check unix glob match
104+
globPattern, err := glob.Compile(target)
105+
if err != nil {
106+
return false, err
107+
}
108+
if globPattern.Match(repo) {
109+
return true, nil
110+
}
111+
// Check regex match
112+
if reMatch, err := regexp.MatchString(target, repo); err != nil {
113+
return false, err
114+
} else if reMatch {
115+
return true, nil
116+
}
117+
return false, nil
118+
}

pkg/matcher/repo_runinfo_matcher_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,153 @@ func TestIncomingWebhookRule(t *testing.T) {
468468
}
469469
}
470470

471+
func TestMatchRepo(t *testing.T) {
472+
tests := []struct {
473+
name string
474+
eventURL string
475+
repoURL string
476+
wantMatch bool
477+
wantError bool
478+
errorCheck string
479+
}{
480+
// Exact matching
481+
{
482+
name: "exact match",
483+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
484+
repoURL: "https://github.com/tektoncd/pipelines-as-code",
485+
wantMatch: true,
486+
},
487+
{
488+
name: "no substring match",
489+
eventURL: "https://github.com/forked/pipelines-as-code",
490+
repoURL: "https://github.com/tektoncd/pipelines-as-code",
491+
wantMatch: false,
492+
},
493+
// Wildcard * - matches zero or more characters
494+
{
495+
name: "glob * - prefix pattern",
496+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
497+
repoURL: "https://github.com/tektoncd/*",
498+
wantMatch: true,
499+
},
500+
{
501+
name: "glob * - must match from start",
502+
eventURL: "https://gitlab.com/tektoncd/pipelines-as-code",
503+
repoURL: "https://github.com/tektoncd/*",
504+
wantMatch: false,
505+
},
506+
{
507+
name: "glob * - substring match with wildcards",
508+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
509+
repoURL: "*/tektoncd/*",
510+
wantMatch: true,
511+
},
512+
{
513+
name: "glob * - catch-all",
514+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
515+
repoURL: "*",
516+
wantMatch: true,
517+
},
518+
// Wildcard ? - matches exactly one character
519+
{
520+
name: "glob ? - single char match",
521+
eventURL: "https://github.com/tektoncd/pipelines-as-code-v2",
522+
repoURL: "https://github.com/tektoncd/pipelines-as-code-v?",
523+
wantMatch: true,
524+
},
525+
// Character classes [...]
526+
{
527+
name: "glob [range] - character class",
528+
eventURL: "https://gitlab.com/tektoncd/pipelines-as-code",
529+
repoURL: "https://[a-z]*.com/tektoncd/pipelines-as-code",
530+
wantMatch: true,
531+
},
532+
// Alternation {...}
533+
{
534+
name: "glob {a,b,c} - alternation",
535+
eventURL: "https://gitlab.com/tektoncd/pipelines-as-code",
536+
repoURL: "https://{github,gitlab}.com/tektoncd/pipelines-as-code",
537+
wantMatch: true,
538+
},
539+
{
540+
name: "glob {a,b,c} - alternation",
541+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
542+
repoURL: "https://{github,gitlab}.com/tektoncd/pipelines-as-code",
543+
wantMatch: true,
544+
},
545+
// Error handling
546+
{
547+
name: "invalid glob - unclosed bracket",
548+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
549+
repoURL: "https://[github.com/tektoncd/pipelines-as-code",
550+
wantMatch: false,
551+
wantError: true,
552+
errorCheck: "unexpected end of input",
553+
},
554+
// Regex matches
555+
{
556+
name: "regex wildcard at end",
557+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
558+
repoURL: "https://github.com/tektoncd/.*",
559+
wantMatch: true,
560+
},
561+
{
562+
name: "regex group or first group",
563+
eventURL: "https://github.com/tektoncd/pipelines-as-code",
564+
repoURL: "https://github.com/(tektoncd|fork)/pipelines-as-code",
565+
wantMatch: true,
566+
},
567+
{
568+
name: "regex group or second group ",
569+
eventURL: "https://github.com/fork/pipelines-as-code",
570+
repoURL: "https://github.com/(tektoncd|fork)/pipelines-as-code",
571+
wantMatch: true,
572+
},
573+
{
574+
name: "ASCII character class ",
575+
eventURL: "https://github.com/fork/pipelines-as-code",
576+
repoURL: "https://github.com/[[:alpha:]]+/pipelines-as-code",
577+
wantMatch: true,
578+
},
579+
{
580+
name: "match group and repo",
581+
eventURL: "https://github.com/fork/pipelines-as-code",
582+
repoURL: "https://github.com/(/?[[:alpha:]-]+){2}$",
583+
wantMatch: true,
584+
},
585+
{
586+
name: "unmatch group and repo",
587+
eventURL: "https://github.com/fork/pipelines-as-code/test",
588+
repoURL: "https://github.com/(/?[[:alpha:]-]+){2}$",
589+
wantMatch: false,
590+
},
591+
}
592+
593+
for _, tt := range tests {
594+
t.Run(tt.name, func(t *testing.T) {
595+
gotMatch, err := matchRepo(tt.eventURL, tt.repoURL)
596+
597+
if tt.wantError {
598+
if err == nil {
599+
t.Errorf("matchRepo() expected error but got nil")
600+
return
601+
}
602+
if tt.errorCheck != "" {
603+
assert.ErrorContains(t, err, tt.errorCheck)
604+
}
605+
} else {
606+
if err != nil {
607+
t.Errorf("matchRepo() unexpected error = %v", err)
608+
return
609+
}
610+
if gotMatch != tt.wantMatch {
611+
t.Errorf("matchRepo() gotMatch = %v, want %v for eventURL=%q, repoURL=%q", gotMatch, tt.wantMatch, tt.eventURL, tt.repoURL)
612+
}
613+
}
614+
})
615+
}
616+
}
617+
471618
func TestMatchTarget(t *testing.T) {
472619
tests := []struct {
473620
name string

pkg/webhook/validation.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import (
66
"net/http"
77
"net/url"
88
"os"
9+
"regexp"
910
"strings"
1011
"time"
1112

13+
"github.com/gobwas/glob"
1214
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1315
pac "github.com/openshift-pipelines/pipelines-as-code/pkg/generated/listers/pipelinesascode/v1alpha1"
1416
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
17+
"go.uber.org/multierr"
1518
v1 "k8s.io/api/admission/v1"
1619
"k8s.io/apimachinery/pkg/labels"
1720
"k8s.io/apimachinery/pkg/runtime"
@@ -99,9 +102,30 @@ func checkIfRepoExist(pac pac.RepositoryLister, repo *v1alpha1.Repository, ns st
99102
}
100103

101104
func validateRepositoryURL(repoURL, gitProviderType string) error {
102-
parsedURL, err := url.Parse(repoURL)
103-
if err != nil {
104-
return fmt.Errorf("invalid URL format: %w", err)
105+
parsedURL, rawUrlErr := url.Parse(repoURL)
106+
hasGlobOrRegex := false
107+
for _, r := range []rune{'*', '?', '[', '(', '{'} {
108+
if strings.ContainsRune(repoURL, r) {
109+
hasGlobOrRegex = true
110+
_, globErr := glob.Compile(repoURL)
111+
_, regexErr := regexp.Compile(repoURL)
112+
if globErr != nil && rawUrlErr != nil && regexErr != nil {
113+
return fmt.Errorf("invalid URL format: %w", multierr.Combine(rawUrlErr, globErr, regexErr))
114+
}
115+
// There is at least one of glob or regex that match the repoUrl.
116+
// if parsedURL is nil it means that itś not needed to go further as it will fail
117+
if parsedURL == nil {
118+
return nil
119+
}
120+
}
121+
}
122+
if rawUrlErr != nil {
123+
return fmt.Errorf("invalid URL format: %w", rawUrlErr)
124+
}
125+
126+
// SafeGuard Here
127+
if parsedURL == nil {
128+
return fmt.Errorf("RepoURL is invalid as neither raw url, glob nor regex: %s", repoURL)
105129
}
106130

107131
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
@@ -117,7 +141,7 @@ func validateRepositoryURL(repoURL, gitProviderType string) error {
117141
repoPath := strings.Trim(parsedURL.Path, "/")
118142

119143
split := strings.Split(repoPath, "/")
120-
if len(split) != 2 {
144+
if len(split) != 2 && !hasGlobOrRegex {
121145
return fmt.Errorf("github repository URL must follow https://github.com/org/repo format without subgroups (found %d path segments, expected 2): %s", len(split), repoURL)
122146
}
123147
}

pkg/webhook/validation_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,116 @@ func TestReconcilerAdmit(t *testing.T) {
220220
})
221221
}
222222
}
223+
224+
func TestMatchRepo(t *testing.T) {
225+
tests := []struct {
226+
name string
227+
url string
228+
wantError bool
229+
errorCheck string
230+
}{
231+
// Exact matching
232+
{
233+
name: "std https",
234+
url: "https://github.com/tektoncd/pipelines-as-code",
235+
wantError: false,
236+
},
237+
{
238+
name: "std http",
239+
url: "http://github.com/tektoncd/pipelines-as-code",
240+
wantError: false,
241+
},
242+
// Wildcard * - matches zero or more characters
243+
{
244+
name: "glob * - prefix pattern",
245+
url: "https://github.com/tektoncd/*",
246+
wantError: false,
247+
},
248+
{
249+
name: "glob * - domain",
250+
url: "https://*/tektoncd/pipelines-as-code",
251+
wantError: false,
252+
},
253+
{
254+
name: "glob ? on protocol",
255+
url: "http?://github.com/tektoncd/pipelines-as-code",
256+
wantError: true,
257+
},
258+
{
259+
name: "glob * on protocol",
260+
url: "*://github.com/tektoncd/pipelines-as-code",
261+
wantError: false,
262+
},
263+
// Wildcard ? - matches exactly one character
264+
{
265+
name: "glob ? - domain",
266+
url: "https://??????.com/tektoncd/pipelines-as-code-v2",
267+
wantError: false,
268+
},
269+
// Character classes [...]
270+
{
271+
name: "glob [range] - character class",
272+
url: "https://[a-z]*.com/tektoncd/pipelines-as-code",
273+
wantError: false,
274+
},
275+
// Alternation {...}
276+
{
277+
name: "glob {a,b,c} - alternation",
278+
url: "https://{github,gitlab}.com/tektoncd/pipelines-as-code",
279+
wantError: false,
280+
},
281+
// Error handling
282+
{
283+
name: "invalid glob - unclosed bracket",
284+
url: "https://[github.com/tektoncd/pipelines-as-code",
285+
wantError: true,
286+
},
287+
// Regex matches
288+
{
289+
name: "regex wildcard at end",
290+
url: "https://github.com/tektoncd/.*",
291+
wantError: false,
292+
},
293+
{
294+
name: "regex group or first group",
295+
url: "https://github.com/(tektoncd|fork)/pipelines-as-code",
296+
wantError: false,
297+
},
298+
{
299+
name: "regex group or second group ",
300+
url: "https://github.com/(tektoncd|fork)/pipelines-as-code",
301+
wantError: false,
302+
},
303+
{
304+
name: "ASCII character class ",
305+
url: "https://github.com/[[:alpha:]]+/pipelines-as-code",
306+
wantError: false,
307+
},
308+
{
309+
name: "match group and repo",
310+
url: "https://github.com/(/?[[:alpha:]-]+){2}$",
311+
wantError: false,
312+
},
313+
}
314+
315+
for _, tt := range tests {
316+
t.Run(tt.name, func(t *testing.T) {
317+
err := validateRepositoryURL(tt.url, "")
318+
319+
if tt.wantError {
320+
if err == nil {
321+
t.Errorf("validateRepositoryURL() expected error but got nil")
322+
return
323+
}
324+
if tt.errorCheck != "" {
325+
assert.ErrorContains(t, err, tt.errorCheck)
326+
}
327+
} else {
328+
if err != nil {
329+
t.Errorf("validateRepositoryURL() unexpected error = %v", err)
330+
return
331+
}
332+
}
333+
})
334+
}
335+
}

0 commit comments

Comments
 (0)