From b25bae0aba297eb141bfb6fb584542a684c1ef3f Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Tue, 7 Feb 2023 11:13:01 +0100 Subject: [PATCH 1/3] Fix use actions compatible globbing --- .../actions/workflowpattern/trace_writer.go | 21 + .../workflowpattern/workflow_pattern.go | 198 +++++++++ .../workflowpattern/workflow_pattern_test.go | 417 ++++++++++++++++++ modules/actions/workflows.go | 142 ++++-- 4 files changed, 743 insertions(+), 35 deletions(-) create mode 100644 modules/actions/workflowpattern/trace_writer.go create mode 100644 modules/actions/workflowpattern/workflow_pattern.go create mode 100644 modules/actions/workflowpattern/workflow_pattern_test.go diff --git a/modules/actions/workflowpattern/trace_writer.go b/modules/actions/workflowpattern/trace_writer.go new file mode 100644 index 0000000000000..f15635f6b3843 --- /dev/null +++ b/modules/actions/workflowpattern/trace_writer.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package workflowpattern + +import "fmt" + +type TraceWriter interface { + Info(string, ...interface{}) +} + +type EmptyTraceWriter struct{} + +func (*EmptyTraceWriter) Info(string, ...interface{}) { +} + +type StdOutTraceWriter struct{} + +func (*StdOutTraceWriter) Info(format string, args ...interface{}) { + fmt.Printf(format+"\n", args...) +} diff --git a/modules/actions/workflowpattern/workflow_pattern.go b/modules/actions/workflowpattern/workflow_pattern.go new file mode 100644 index 0000000000000..e2d663505e8d9 --- /dev/null +++ b/modules/actions/workflowpattern/workflow_pattern.go @@ -0,0 +1,198 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package workflowpattern + +import ( + "fmt" + "regexp" + "strings" +) + +type WorkflowPattern struct { + Pattern string + Negative bool + Regex *regexp.Regexp +} + +func CompilePattern(rawpattern string) (*WorkflowPattern, error) { + negative := false + pattern := rawpattern + if strings.HasPrefix(rawpattern, "!") { + negative = true + pattern = rawpattern[1:] + } + rpattern, err := PatternToRegex(pattern) + if err != nil { + return nil, err + } + regex, err := regexp.Compile(rpattern) + if err != nil { + return nil, err + } + return &WorkflowPattern{ + Pattern: pattern, + Negative: negative, + Regex: regex, + }, nil +} + +func PatternToRegex(pattern string) (string, error) { + var rpattern strings.Builder + rpattern.WriteString("^") + pos := 0 + errors := map[int]string{} + for pos < len(pattern) { + switch pattern[pos] { + case '*': + if pos+1 < len(pattern) && pattern[pos+1] == '*' { + if pos+2 < len(pattern) && pattern[pos+2] == '/' { + rpattern.WriteString("(.+/)?") + pos += 3 + } else { + rpattern.WriteString(".*") + pos += 2 + } + } else { + rpattern.WriteString("[^/]*") + pos++ + } + case '+', '?': + if pos > 0 { + rpattern.WriteByte(pattern[pos]) + } else { + rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) + } + pos++ + case '[': + rpattern.WriteByte(pattern[pos]) + pos++ + if pos < len(pattern) && pattern[pos] == ']' { + errors[pos] = "Unexpected empty brackets '[]'" + pos++ + break + } + validChar := func(a, b, test byte) bool { + return test >= a && test <= b + } + startPos := pos + for pos < len(pattern) && pattern[pos] != ']' { + switch pattern[pos] { + case '-': + if pos <= startPos || pos+1 >= len(pattern) { + errors[pos] = "Invalid range" + pos++ + break + } + validRange := func(a, b byte) bool { + return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1] + } + if !validRange('A', 'z') && !validRange('0', '9') { + errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9" + pos++ + break + } + rpattern.WriteString(pattern[pos : pos+2]) + pos += 2 + default: + if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) { + errors[pos] = "Ranges can only include a-z, A-Z and 0-9" + pos++ + break + } + rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) + pos++ + } + } + if pos >= len(pattern) || pattern[pos] != ']' { + errors[pos] = "Missing closing bracket ']' after '['" + pos++ + } + rpattern.WriteString("]") + pos++ + case '\\': + if pos+1 >= len(pattern) { + errors[pos] = "Missing symbol after \\" + pos++ + break + } + rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]}))) + pos += 2 + default: + rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) + pos++ + } + } + if len(errors) > 0 { + var errorMessage strings.Builder + for position, err := range errors { + if errorMessage.Len() > 0 { + errorMessage.WriteString(", ") + } + errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err)) + } + return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String()) + } + rpattern.WriteString("$") + return rpattern.String(), nil +} + +func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { + ret := []*WorkflowPattern{} + for _, pattern := range patterns { + cp, err := CompilePattern(pattern) + if err != nil { + return nil, err + } + ret = append(ret, cp) + } + return ret, nil +} + +// returns true if the workflow should be skipped paths/branches +func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { + if len(sequence) == 0 { + return false + } + for _, file := range input { + matched := false + for _, item := range sequence { + if item.Regex.MatchString(file) { + pattern := item.Pattern + if item.Negative { + matched = false + traceWriter.Info("%s excluded by pattern %s", file, pattern) + } else { + matched = true + traceWriter.Info("%s included by pattern %s", file, pattern) + } + } + } + if matched { + return false + } + } + return true +} + +// returns true if the workflow should be skipped paths-ignore/branches-ignore +func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { + if len(sequence) == 0 { + return false + } + for _, file := range input { + matched := false + for _, item := range sequence { + if item.Regex.MatchString(file) == !item.Negative { + pattern := item.Pattern + traceWriter.Info("%s ignored by pattern %s", file, pattern) + matched = true + break + } + } + if !matched { + return false + } + } + return true +} diff --git a/modules/actions/workflowpattern/workflow_pattern_test.go b/modules/actions/workflowpattern/workflow_pattern_test.go new file mode 100644 index 0000000000000..30213bd8c329c --- /dev/null +++ b/modules/actions/workflowpattern/workflow_pattern_test.go @@ -0,0 +1,417 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package workflowpattern + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMatchPattern(t *testing.T) { + kases := []struct { + inputs []string + patterns []string + skipResult bool + filterResult bool + }{ + { + patterns: []string{"*"}, + inputs: []string{"path/with/slash"}, + skipResult: true, + filterResult: false, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"meta", "path/b", "otherfile"}, + skipResult: false, + filterResult: false, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/b"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/c", "path/b"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/c", "path/b", "path/a"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"path/a", "path/b", "path/c"}, + inputs: []string{"path/c", "path/b", "path/d", "path/a"}, + skipResult: false, + filterResult: false, + }, + { + patterns: []string{}, + inputs: []string{}, + skipResult: false, + filterResult: false, + }, + { + patterns: []string{"\\!file"}, + inputs: []string{"!file"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"escape\\\\backslash"}, + inputs: []string{"escape\\backslash"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{".yml"}, + inputs: []string{"fyml"}, + skipResult: true, + filterResult: false, + }, + // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags + { + patterns: []string{"feature/*"}, + inputs: []string{"feature/my-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/*"}, + inputs: []string{"feature/your-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/**"}, + inputs: []string{"feature/beta-a/my-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/**"}, + inputs: []string{"feature/beta-a/my-branch"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"feature/**"}, + inputs: []string{"feature/mona/the/octocat"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"main", "releases/mona-the-octocat"}, + inputs: []string{"main"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"main", "releases/mona-the-octocat"}, + inputs: []string{"releases/mona-the-octocat"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*"}, + inputs: []string{"main"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*"}, + inputs: []string{"releases"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**"}, + inputs: []string{"all/the/branches"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**"}, + inputs: []string{"every/tag"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*feature"}, + inputs: []string{"mona-feature"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*feature"}, + inputs: []string{"feature"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*feature"}, + inputs: []string{"ver-10-feature"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v2*"}, + inputs: []string{"v2"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v2*"}, + inputs: []string{"v2.0"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v2*"}, + inputs: []string{"v2.9"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v[12].[0-9]+.[0-9]+"}, + inputs: []string{"v1.10.1"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"v[12].[0-9]+.[0-9]+"}, + inputs: []string{"v2.0.0"}, + skipResult: false, + filterResult: true, + }, + // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths + { + patterns: []string{"*"}, + inputs: []string{"README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*"}, + inputs: []string{"server.rb"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.jsx?"}, + inputs: []string{"page.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.jsx?"}, + inputs: []string{"page.jsx"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**"}, + inputs: []string{"all/the/files.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.js"}, + inputs: []string{"app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.js"}, + inputs: []string{"index.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**.js"}, + inputs: []string{"index.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**.js"}, + inputs: []string{"js/index.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**.js"}, + inputs: []string{"src/js/app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/*"}, + inputs: []string{"docs/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/*"}, + inputs: []string{"docs/file.txt"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**"}, + inputs: []string{"docs/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**"}, + inputs: []string{"docs/mona/octocat.txt"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**/*.md"}, + inputs: []string{"docs/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**/*.md"}, + inputs: []string{"docs/mona/hello-world.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"docs/**/*.md"}, + inputs: []string{"docs/a/markdown/file.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/docs/**"}, + inputs: []string{"docs/hello.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/docs/**"}, + inputs: []string{"dir/docs/my-file.txt"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/docs/**"}, + inputs: []string{"space/docs/plan/space.doc"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/README.md"}, + inputs: []string{"README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/README.md"}, + inputs: []string{"js/README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*src/**"}, + inputs: []string{"a/src/app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*src/**"}, + inputs: []string{"my-src/code/js/app.js"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*-post.md"}, + inputs: []string{"my-post.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/*-post.md"}, + inputs: []string{"path/their-post.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/migrate-*.sql"}, + inputs: []string{"migrate-10909.sql"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/migrate-*.sql"}, + inputs: []string{"db/migrate-v1.0.sql"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"**/migrate-*.sql"}, + inputs: []string{"db/sept/migrate-v1.sql"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md"}, + inputs: []string{"hello.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md"}, + inputs: []string{"README.md"}, + skipResult: true, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md"}, + inputs: []string{"docs/hello.md"}, + skipResult: true, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md", "README*"}, + inputs: []string{"hello.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md", "README*"}, + inputs: []string{"README.md"}, + skipResult: false, + filterResult: true, + }, + { + patterns: []string{"*.md", "!README.md", "README*"}, + inputs: []string{"README.doc"}, + skipResult: false, + filterResult: true, + }, + } + + for _, kase := range kases { + t.Run(strings.Join(kase.patterns, ","), func(t *testing.T) { + patterns, err := CompilePatterns(kase.patterns...) + assert.NoError(t, err) + + assert.EqualValues(t, kase.skipResult, Skip(patterns, kase.inputs, &StdOutTraceWriter{}), "skipResult") + assert.EqualValues(t, kase.filterResult, Filter(patterns, kase.inputs, &StdOutTraceWriter{}), "filterResult") + }) + } +} diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 7f0e6e456436b..686ab73961bc0 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -8,6 +8,7 @@ import ( "io" "strings" + "code.gitea.io/gitea/modules/actions/workflowpattern" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" @@ -100,40 +101,94 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType case webhook_module.HookEventPush: pushPayload := payload.(*api.PushPayload) matchTimes := 0 + hasBranchFilter := false + hasTagFilter := false + refName := git.RefName(pushPayload.Ref) // all acts conditions should be satisfied for cond, vals := range evt.Acts { switch cond { - case "branches", "tags": - refShortName := git.RefName(pushPayload.Ref).ShortName() - for _, val := range vals { - if glob.MustCompile(val, '/').Match(refShortName) { - matchTimes++ + case "branches": + hasBranchFilter = true + if !refName.IsBranch() { + break + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "branches-ignore": + hasBranchFilter = true + if !refName.IsBranch() { + break + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "tags": + hasTagFilter = true + if !refName.IsTag() { + break + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "tags-ignore": + hasTagFilter = true + if !refName.IsTag() { + break + } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "paths": + filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) + if err != nil { + log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) + } else { + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { break } + if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } } - case "paths": + case "paths-ignore": filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) } else { - for _, val := range vals { - matched := false - for _, file := range filesChanged { - if glob.MustCompile(val, '/').Match(file) { - matched = true - break - } - } - if matched { - matchTimes++ - break - } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ } } default: log.Warn("unsupported condition %q", cond) } } + // if both branch and tag filter are defined in the workflow only one needs to match + if hasBranchFilter && hasTagFilter { + matchTimes++ + } return matchTimes == len(evt.Acts) case webhook_module.HookEventIssues: @@ -160,30 +215,47 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType } } case "branches": - refShortName := git.RefName(prPayload.PullRequest.Base.Ref).ShortName() - for _, val := range vals { - if glob.MustCompile(val, '/').Match(refShortName) { - matchTimes++ + refName := git.RefName(prPayload.PullRequest.Base.Ref) + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "branches-ignore": + refName := git.RefName(prPayload.PullRequest.Base.Ref) + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, []string{refName.ShortName()}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "paths": + filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) + if err != nil { + log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) + } else { + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { break } + if !workflowpattern.Skip(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } } - case "paths": + case "paths-ignore": filesChanged, err := commit.GetFilesChangedSinceCommit(prPayload.PullRequest.Base.Ref) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) } else { - for _, val := range vals { - matched := false - for _, file := range filesChanged { - if glob.MustCompile(val, '/').Match(file) { - matched = true - break - } - } - if matched { - matchTimes++ - break - } + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, filesChanged, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ } } default: From 9d884e89fbf3dffb8d8620e390f1df707efe8411 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Wed, 15 Mar 2023 10:48:03 +0000 Subject: [PATCH 2/3] Remove invalid merge --- modules/actions/workflows.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 6396bf94ee89d..b45d0f863e30c 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -233,7 +233,6 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ } } - } default: log.Warn("push event unsupported condition %q", cond) } @@ -365,4 +364,4 @@ func matchIssueCommentEvent(commit *git.Commit, issueCommentPayload *api.IssueCo } } return matchTimes == len(evt.Acts) -} \ No newline at end of file +} From bfe30313851958917a0d2d10794beeaca6d2a693 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Thu, 16 Mar 2023 11:43:07 +0100 Subject: [PATCH 3/3] Remove workflowpattern package / upgrade gitea/act --- .../actions/workflowpattern/trace_writer.go | 21 - .../workflowpattern/workflow_pattern.go | 198 --------- .../workflowpattern/workflow_pattern_test.go | 417 ------------------ modules/actions/workflows.go | 2 +- 4 files changed, 1 insertion(+), 637 deletions(-) delete mode 100644 modules/actions/workflowpattern/trace_writer.go delete mode 100644 modules/actions/workflowpattern/workflow_pattern.go delete mode 100644 modules/actions/workflowpattern/workflow_pattern_test.go diff --git a/modules/actions/workflowpattern/trace_writer.go b/modules/actions/workflowpattern/trace_writer.go deleted file mode 100644 index f15635f6b3843..0000000000000 --- a/modules/actions/workflowpattern/trace_writer.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package workflowpattern - -import "fmt" - -type TraceWriter interface { - Info(string, ...interface{}) -} - -type EmptyTraceWriter struct{} - -func (*EmptyTraceWriter) Info(string, ...interface{}) { -} - -type StdOutTraceWriter struct{} - -func (*StdOutTraceWriter) Info(format string, args ...interface{}) { - fmt.Printf(format+"\n", args...) -} diff --git a/modules/actions/workflowpattern/workflow_pattern.go b/modules/actions/workflowpattern/workflow_pattern.go deleted file mode 100644 index e2d663505e8d9..0000000000000 --- a/modules/actions/workflowpattern/workflow_pattern.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package workflowpattern - -import ( - "fmt" - "regexp" - "strings" -) - -type WorkflowPattern struct { - Pattern string - Negative bool - Regex *regexp.Regexp -} - -func CompilePattern(rawpattern string) (*WorkflowPattern, error) { - negative := false - pattern := rawpattern - if strings.HasPrefix(rawpattern, "!") { - negative = true - pattern = rawpattern[1:] - } - rpattern, err := PatternToRegex(pattern) - if err != nil { - return nil, err - } - regex, err := regexp.Compile(rpattern) - if err != nil { - return nil, err - } - return &WorkflowPattern{ - Pattern: pattern, - Negative: negative, - Regex: regex, - }, nil -} - -func PatternToRegex(pattern string) (string, error) { - var rpattern strings.Builder - rpattern.WriteString("^") - pos := 0 - errors := map[int]string{} - for pos < len(pattern) { - switch pattern[pos] { - case '*': - if pos+1 < len(pattern) && pattern[pos+1] == '*' { - if pos+2 < len(pattern) && pattern[pos+2] == '/' { - rpattern.WriteString("(.+/)?") - pos += 3 - } else { - rpattern.WriteString(".*") - pos += 2 - } - } else { - rpattern.WriteString("[^/]*") - pos++ - } - case '+', '?': - if pos > 0 { - rpattern.WriteByte(pattern[pos]) - } else { - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) - } - pos++ - case '[': - rpattern.WriteByte(pattern[pos]) - pos++ - if pos < len(pattern) && pattern[pos] == ']' { - errors[pos] = "Unexpected empty brackets '[]'" - pos++ - break - } - validChar := func(a, b, test byte) bool { - return test >= a && test <= b - } - startPos := pos - for pos < len(pattern) && pattern[pos] != ']' { - switch pattern[pos] { - case '-': - if pos <= startPos || pos+1 >= len(pattern) { - errors[pos] = "Invalid range" - pos++ - break - } - validRange := func(a, b byte) bool { - return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1] - } - if !validRange('A', 'z') && !validRange('0', '9') { - errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9" - pos++ - break - } - rpattern.WriteString(pattern[pos : pos+2]) - pos += 2 - default: - if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) { - errors[pos] = "Ranges can only include a-z, A-Z and 0-9" - pos++ - break - } - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) - pos++ - } - } - if pos >= len(pattern) || pattern[pos] != ']' { - errors[pos] = "Missing closing bracket ']' after '['" - pos++ - } - rpattern.WriteString("]") - pos++ - case '\\': - if pos+1 >= len(pattern) { - errors[pos] = "Missing symbol after \\" - pos++ - break - } - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]}))) - pos += 2 - default: - rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) - pos++ - } - } - if len(errors) > 0 { - var errorMessage strings.Builder - for position, err := range errors { - if errorMessage.Len() > 0 { - errorMessage.WriteString(", ") - } - errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err)) - } - return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String()) - } - rpattern.WriteString("$") - return rpattern.String(), nil -} - -func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { - ret := []*WorkflowPattern{} - for _, pattern := range patterns { - cp, err := CompilePattern(pattern) - if err != nil { - return nil, err - } - ret = append(ret, cp) - } - return ret, nil -} - -// returns true if the workflow should be skipped paths/branches -func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { - if len(sequence) == 0 { - return false - } - for _, file := range input { - matched := false - for _, item := range sequence { - if item.Regex.MatchString(file) { - pattern := item.Pattern - if item.Negative { - matched = false - traceWriter.Info("%s excluded by pattern %s", file, pattern) - } else { - matched = true - traceWriter.Info("%s included by pattern %s", file, pattern) - } - } - } - if matched { - return false - } - } - return true -} - -// returns true if the workflow should be skipped paths-ignore/branches-ignore -func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { - if len(sequence) == 0 { - return false - } - for _, file := range input { - matched := false - for _, item := range sequence { - if item.Regex.MatchString(file) == !item.Negative { - pattern := item.Pattern - traceWriter.Info("%s ignored by pattern %s", file, pattern) - matched = true - break - } - } - if !matched { - return false - } - } - return true -} diff --git a/modules/actions/workflowpattern/workflow_pattern_test.go b/modules/actions/workflowpattern/workflow_pattern_test.go deleted file mode 100644 index 30213bd8c329c..0000000000000 --- a/modules/actions/workflowpattern/workflow_pattern_test.go +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package workflowpattern - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMatchPattern(t *testing.T) { - kases := []struct { - inputs []string - patterns []string - skipResult bool - filterResult bool - }{ - { - patterns: []string{"*"}, - inputs: []string{"path/with/slash"}, - skipResult: true, - filterResult: false, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"meta", "path/b", "otherfile"}, - skipResult: false, - filterResult: false, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/b"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/c", "path/b"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/c", "path/b", "path/a"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"path/a", "path/b", "path/c"}, - inputs: []string{"path/c", "path/b", "path/d", "path/a"}, - skipResult: false, - filterResult: false, - }, - { - patterns: []string{}, - inputs: []string{}, - skipResult: false, - filterResult: false, - }, - { - patterns: []string{"\\!file"}, - inputs: []string{"!file"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"escape\\\\backslash"}, - inputs: []string{"escape\\backslash"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{".yml"}, - inputs: []string{"fyml"}, - skipResult: true, - filterResult: false, - }, - // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags - { - patterns: []string{"feature/*"}, - inputs: []string{"feature/my-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/*"}, - inputs: []string{"feature/your-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/**"}, - inputs: []string{"feature/beta-a/my-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/**"}, - inputs: []string{"feature/beta-a/my-branch"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"feature/**"}, - inputs: []string{"feature/mona/the/octocat"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"main", "releases/mona-the-octocat"}, - inputs: []string{"main"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"main", "releases/mona-the-octocat"}, - inputs: []string{"releases/mona-the-octocat"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*"}, - inputs: []string{"main"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*"}, - inputs: []string{"releases"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**"}, - inputs: []string{"all/the/branches"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**"}, - inputs: []string{"every/tag"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*feature"}, - inputs: []string{"mona-feature"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*feature"}, - inputs: []string{"feature"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*feature"}, - inputs: []string{"ver-10-feature"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v2*"}, - inputs: []string{"v2"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v2*"}, - inputs: []string{"v2.0"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v2*"}, - inputs: []string{"v2.9"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v[12].[0-9]+.[0-9]+"}, - inputs: []string{"v1.10.1"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"v[12].[0-9]+.[0-9]+"}, - inputs: []string{"v2.0.0"}, - skipResult: false, - filterResult: true, - }, - // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths - { - patterns: []string{"*"}, - inputs: []string{"README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*"}, - inputs: []string{"server.rb"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.jsx?"}, - inputs: []string{"page.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.jsx?"}, - inputs: []string{"page.jsx"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**"}, - inputs: []string{"all/the/files.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.js"}, - inputs: []string{"app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.js"}, - inputs: []string{"index.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**.js"}, - inputs: []string{"index.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**.js"}, - inputs: []string{"js/index.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**.js"}, - inputs: []string{"src/js/app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/*"}, - inputs: []string{"docs/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/*"}, - inputs: []string{"docs/file.txt"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**"}, - inputs: []string{"docs/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**"}, - inputs: []string{"docs/mona/octocat.txt"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**/*.md"}, - inputs: []string{"docs/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**/*.md"}, - inputs: []string{"docs/mona/hello-world.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"docs/**/*.md"}, - inputs: []string{"docs/a/markdown/file.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/docs/**"}, - inputs: []string{"docs/hello.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/docs/**"}, - inputs: []string{"dir/docs/my-file.txt"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/docs/**"}, - inputs: []string{"space/docs/plan/space.doc"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/README.md"}, - inputs: []string{"README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/README.md"}, - inputs: []string{"js/README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*src/**"}, - inputs: []string{"a/src/app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*src/**"}, - inputs: []string{"my-src/code/js/app.js"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*-post.md"}, - inputs: []string{"my-post.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/*-post.md"}, - inputs: []string{"path/their-post.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/migrate-*.sql"}, - inputs: []string{"migrate-10909.sql"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/migrate-*.sql"}, - inputs: []string{"db/migrate-v1.0.sql"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"**/migrate-*.sql"}, - inputs: []string{"db/sept/migrate-v1.sql"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md"}, - inputs: []string{"hello.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md"}, - inputs: []string{"README.md"}, - skipResult: true, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md"}, - inputs: []string{"docs/hello.md"}, - skipResult: true, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md", "README*"}, - inputs: []string{"hello.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md", "README*"}, - inputs: []string{"README.md"}, - skipResult: false, - filterResult: true, - }, - { - patterns: []string{"*.md", "!README.md", "README*"}, - inputs: []string{"README.doc"}, - skipResult: false, - filterResult: true, - }, - } - - for _, kase := range kases { - t.Run(strings.Join(kase.patterns, ","), func(t *testing.T) { - patterns, err := CompilePatterns(kase.patterns...) - assert.NoError(t, err) - - assert.EqualValues(t, kase.skipResult, Skip(patterns, kase.inputs, &StdOutTraceWriter{}), "skipResult") - assert.EqualValues(t, kase.filterResult, Filter(patterns, kase.inputs, &StdOutTraceWriter{}), "filterResult") - }) - } -} diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index b45d0f863e30c..688fd169ab01c 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -8,7 +8,6 @@ import ( "io" "strings" - "code.gitea.io/gitea/modules/actions/workflowpattern" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" @@ -17,6 +16,7 @@ import ( "github.com/gobwas/glob" "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" + "github.com/nektos/act/pkg/workflowpattern" ) func ListWorkflows(commit *git.Commit) (git.Entries, error) {