summaryrefslogtreecommitdiffstats
path: root/pkg/workflowpattern
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-20 23:07:42 +0200
committerDaniel Baumann <daniel@debian.org>2024-11-09 15:38:42 +0100
commit714c83b2736d7e308bc33c49057952490eb98be2 (patch)
tree1d9ba7035798368569cd49056f4d596efc908cd8 /pkg/workflowpattern
parentInitial commit. (diff)
downloadforgejo-act-714c83b2736d7e308bc33c49057952490eb98be2.tar.xz
forgejo-act-714c83b2736d7e308bc33c49057952490eb98be2.zip
Adding upstream version 1.21.4.HEADupstream/1.21.4upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'pkg/workflowpattern')
-rw-r--r--pkg/workflowpattern/trace_writer.go18
-rw-r--r--pkg/workflowpattern/workflow_pattern.go196
-rw-r--r--pkg/workflowpattern/workflow_pattern_test.go414
3 files changed, 628 insertions, 0 deletions
diff --git a/pkg/workflowpattern/trace_writer.go b/pkg/workflowpattern/trace_writer.go
new file mode 100644
index 0000000..d5d990f
--- /dev/null
+++ b/pkg/workflowpattern/trace_writer.go
@@ -0,0 +1,18 @@
+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/pkg/workflowpattern/workflow_pattern.go b/pkg/workflowpattern/workflow_pattern.go
new file mode 100644
index 0000000..cc03e40
--- /dev/null
+++ b/pkg/workflowpattern/workflow_pattern.go
@@ -0,0 +1,196 @@
+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
+}
+
+//nolint:gocyclo
+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/pkg/workflowpattern/workflow_pattern_test.go b/pkg/workflowpattern/workflow_pattern_test.go
new file mode 100644
index 0000000..a62d529
--- /dev/null
+++ b/pkg/workflowpattern/workflow_pattern_test.go
@@ -0,0 +1,414 @@
+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")
+ })
+ }
+}