github.com/nektos/act@v0.2.63/pkg/workflowpattern/workflow_pattern.go (about) 1 package workflowpattern 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 ) 8 9 type WorkflowPattern struct { 10 Pattern string 11 Negative bool 12 Regex *regexp.Regexp 13 } 14 15 func CompilePattern(rawpattern string) (*WorkflowPattern, error) { 16 negative := false 17 pattern := rawpattern 18 if strings.HasPrefix(rawpattern, "!") { 19 negative = true 20 pattern = rawpattern[1:] 21 } 22 rpattern, err := PatternToRegex(pattern) 23 if err != nil { 24 return nil, err 25 } 26 regex, err := regexp.Compile(rpattern) 27 if err != nil { 28 return nil, err 29 } 30 return &WorkflowPattern{ 31 Pattern: pattern, 32 Negative: negative, 33 Regex: regex, 34 }, nil 35 } 36 37 //nolint:gocyclo 38 func PatternToRegex(pattern string) (string, error) { 39 var rpattern strings.Builder 40 rpattern.WriteString("^") 41 pos := 0 42 errors := map[int]string{} 43 for pos < len(pattern) { 44 switch pattern[pos] { 45 case '*': 46 if pos+1 < len(pattern) && pattern[pos+1] == '*' { 47 if pos+2 < len(pattern) && pattern[pos+2] == '/' { 48 rpattern.WriteString("(.+/)?") 49 pos += 3 50 } else { 51 rpattern.WriteString(".*") 52 pos += 2 53 } 54 } else { 55 rpattern.WriteString("[^/]*") 56 pos++ 57 } 58 case '+', '?': 59 if pos > 0 { 60 rpattern.WriteByte(pattern[pos]) 61 } else { 62 rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) 63 } 64 pos++ 65 case '[': 66 rpattern.WriteByte(pattern[pos]) 67 pos++ 68 if pos < len(pattern) && pattern[pos] == ']' { 69 errors[pos] = "Unexpected empty brackets '[]'" 70 pos++ 71 break 72 } 73 validChar := func(a, b, test byte) bool { 74 return test >= a && test <= b 75 } 76 startPos := pos 77 for pos < len(pattern) && pattern[pos] != ']' { 78 switch pattern[pos] { 79 case '-': 80 if pos <= startPos || pos+1 >= len(pattern) { 81 errors[pos] = "Invalid range" 82 pos++ 83 break 84 } 85 validRange := func(a, b byte) bool { 86 return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1] 87 } 88 if !validRange('A', 'z') && !validRange('0', '9') { 89 errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9" 90 pos++ 91 break 92 } 93 rpattern.WriteString(pattern[pos : pos+2]) 94 pos += 2 95 default: 96 if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) { 97 errors[pos] = "Ranges can only include a-z, A-Z and 0-9" 98 pos++ 99 break 100 } 101 rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) 102 pos++ 103 } 104 } 105 if pos >= len(pattern) || pattern[pos] != ']' { 106 errors[pos] = "Missing closing bracket ']' after '['" 107 pos++ 108 } 109 rpattern.WriteString("]") 110 pos++ 111 case '\\': 112 if pos+1 >= len(pattern) { 113 errors[pos] = "Missing symbol after \\" 114 pos++ 115 break 116 } 117 rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]}))) 118 pos += 2 119 default: 120 rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]}))) 121 pos++ 122 } 123 } 124 if len(errors) > 0 { 125 var errorMessage strings.Builder 126 for position, err := range errors { 127 if errorMessage.Len() > 0 { 128 errorMessage.WriteString(", ") 129 } 130 errorMessage.WriteString(fmt.Sprintf("Position: %d Error: %s", position, err)) 131 } 132 return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String()) 133 } 134 rpattern.WriteString("$") 135 return rpattern.String(), nil 136 } 137 138 func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { 139 ret := []*WorkflowPattern{} 140 for _, pattern := range patterns { 141 cp, err := CompilePattern(pattern) 142 if err != nil { 143 return nil, err 144 } 145 ret = append(ret, cp) 146 } 147 return ret, nil 148 } 149 150 // returns true if the workflow should be skipped paths/branches 151 func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { 152 if len(sequence) == 0 { 153 return false 154 } 155 for _, file := range input { 156 matched := false 157 for _, item := range sequence { 158 if item.Regex.MatchString(file) { 159 pattern := item.Pattern 160 if item.Negative { 161 matched = false 162 traceWriter.Info("%s excluded by pattern %s", file, pattern) 163 } else { 164 matched = true 165 traceWriter.Info("%s included by pattern %s", file, pattern) 166 } 167 } 168 } 169 if matched { 170 return false 171 } 172 } 173 return true 174 } 175 176 // returns true if the workflow should be skipped paths-ignore/branches-ignore 177 func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { 178 if len(sequence) == 0 { 179 return false 180 } 181 for _, file := range input { 182 matched := false 183 for _, item := range sequence { 184 if item.Regex.MatchString(file) == !item.Negative { 185 pattern := item.Pattern 186 traceWriter.Info("%s ignored by pattern %s", file, pattern) 187 matched = true 188 break 189 } 190 } 191 if !matched { 192 return false 193 } 194 } 195 return true 196 }