github.com/nektos/act@v0.2.83/pkg/schema/schema.go (about) 1 package schema 2 3 import ( 4 _ "embed" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "math" 9 "regexp" 10 "strconv" 11 "strings" 12 13 "github.com/rhysd/actionlint" 14 "gopkg.in/yaml.v3" 15 ) 16 17 //go:embed workflow_schema.json 18 var workflowSchema string 19 20 //go:embed action_schema.json 21 var actionSchema string 22 23 var functions = regexp.MustCompile(`^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$`) 24 25 type Schema struct { 26 Definitions map[string]Definition 27 } 28 29 func (s *Schema) GetDefinition(name string) Definition { 30 def, ok := s.Definitions[name] 31 if !ok { 32 switch name { 33 case "any": 34 return Definition{OneOf: &[]string{"sequence", "mapping", "number", "boolean", "string", "null"}} 35 case "sequence": 36 return Definition{Sequence: &SequenceDefinition{ItemType: "any"}} 37 case "mapping": 38 return Definition{Mapping: &MappingDefinition{LooseKeyType: "any", LooseValueType: "any"}} 39 case "number": 40 return Definition{Number: &NumberDefinition{}} 41 case "string": 42 return Definition{String: &StringDefinition{}} 43 case "boolean": 44 return Definition{Boolean: &BooleanDefinition{}} 45 case "null": 46 return Definition{Null: &NullDefinition{}} 47 } 48 } 49 return def 50 } 51 52 type Definition struct { 53 Context []string 54 Mapping *MappingDefinition 55 Sequence *SequenceDefinition 56 OneOf *[]string `json:"one-of"` 57 AllowedValues *[]string `json:"allowed-values"` 58 String *StringDefinition 59 Number *NumberDefinition 60 Boolean *BooleanDefinition 61 Null *NullDefinition 62 } 63 64 type MappingDefinition struct { 65 Properties map[string]MappingProperty 66 LooseKeyType string `json:"loose-key-type"` 67 LooseValueType string `json:"loose-value-type"` 68 } 69 70 type MappingProperty struct { 71 Type string 72 Required bool 73 } 74 75 func (s *MappingProperty) UnmarshalJSON(data []byte) error { 76 if json.Unmarshal(data, &s.Type) != nil { 77 type MProp MappingProperty 78 return json.Unmarshal(data, (*MProp)(s)) 79 } 80 return nil 81 } 82 83 type SequenceDefinition struct { 84 ItemType string `json:"item-type"` 85 } 86 87 type StringDefinition struct { 88 Constant string 89 IsExpression bool `json:"is-expression"` 90 } 91 92 type NumberDefinition struct { 93 } 94 95 type BooleanDefinition struct { 96 } 97 98 type NullDefinition struct { 99 } 100 101 func GetWorkflowSchema() *Schema { 102 sh := &Schema{} 103 _ = json.Unmarshal([]byte(workflowSchema), sh) 104 return sh 105 } 106 107 func GetActionSchema() *Schema { 108 sh := &Schema{} 109 _ = json.Unmarshal([]byte(actionSchema), sh) 110 return sh 111 } 112 113 type Node struct { 114 Definition string 115 Schema *Schema 116 Context []string 117 } 118 119 type FunctionInfo struct { 120 name string 121 min int 122 max int 123 } 124 125 func (s *Node) checkSingleExpression(exprNode actionlint.ExprNode) error { 126 if len(s.Context) == 0 { 127 switch exprNode.Token().Kind { 128 case actionlint.TokenKindInt: 129 case actionlint.TokenKindFloat: 130 case actionlint.TokenKindString: 131 return nil 132 default: 133 return fmt.Errorf("expressions are not allowed here") 134 } 135 } 136 137 funcs := s.GetFunctions() 138 139 var err error 140 actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) { 141 if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok { 142 for _, v := range *funcs { 143 if strings.EqualFold(funcCallNode.Callee, v.name) { 144 if v.min > len(funcCallNode.Args) { 145 err = errors.Join(err, fmt.Errorf("Missing parameters for %s expected >= %v got %v", funcCallNode.Callee, v.min, len(funcCallNode.Args))) 146 } 147 if v.max < len(funcCallNode.Args) { 148 err = errors.Join(err, fmt.Errorf("Too many parameters for %s expected <= %v got %v", funcCallNode.Callee, v.max, len(funcCallNode.Args))) 149 } 150 return 151 } 152 } 153 err = errors.Join(err, fmt.Errorf("Unknown Function Call %s", funcCallNode.Callee)) 154 } 155 if varNode, ok := node.(*actionlint.VariableNode); entering && ok { 156 for _, v := range s.Context { 157 if strings.EqualFold(varNode.Name, v) { 158 return 159 } 160 } 161 err = errors.Join(err, fmt.Errorf("Unknown Variable Access %s", varNode.Name)) 162 } 163 }) 164 return err 165 } 166 167 func (s *Node) GetFunctions() *[]FunctionInfo { 168 funcs := &[]FunctionInfo{} 169 AddFunction(funcs, "contains", 2, 2) 170 AddFunction(funcs, "endsWith", 2, 2) 171 AddFunction(funcs, "format", 1, 255) 172 AddFunction(funcs, "join", 1, 2) 173 AddFunction(funcs, "startsWith", 2, 2) 174 AddFunction(funcs, "toJson", 1, 1) 175 AddFunction(funcs, "fromJson", 1, 1) 176 for _, v := range s.Context { 177 i := strings.Index(v, "(") 178 if i == -1 { 179 continue 180 } 181 smatch := functions.FindStringSubmatch(v) 182 if len(smatch) > 0 { 183 functionName := smatch[1] 184 minParameters, _ := strconv.ParseInt(smatch[2], 10, 32) 185 maxParametersRaw := smatch[3] 186 var maxParameters int64 187 if strings.EqualFold(maxParametersRaw, "MAX") { 188 maxParameters = math.MaxInt32 189 } else { 190 maxParameters, _ = strconv.ParseInt(maxParametersRaw, 10, 32) 191 } 192 *funcs = append(*funcs, FunctionInfo{ 193 name: functionName, 194 min: int(minParameters), 195 max: int(maxParameters), 196 }) 197 } 198 } 199 return funcs 200 } 201 202 func (s *Node) checkExpression(node *yaml.Node) (bool, error) { 203 val := node.Value 204 hadExpr := false 205 var err error 206 for { 207 if i := strings.Index(val, "${{"); i != -1 { 208 val = val[i+3:] 209 } else { 210 return hadExpr, err 211 } 212 hadExpr = true 213 214 parser := actionlint.NewExprParser() 215 lexer := actionlint.NewExprLexer(val) 216 exprNode, parseErr := parser.Parse(lexer) 217 if parseErr != nil { 218 err = errors.Join(err, fmt.Errorf("%sFailed to parse: %s", formatLocation(node), parseErr.Message)) 219 continue 220 } 221 val = val[lexer.Offset():] 222 cerr := s.checkSingleExpression(exprNode) 223 if cerr != nil { 224 err = errors.Join(err, fmt.Errorf("%s%w", formatLocation(node), cerr)) 225 } 226 } 227 } 228 229 func AddFunction(funcs *[]FunctionInfo, s string, i1, i2 int) { 230 *funcs = append(*funcs, FunctionInfo{ 231 name: s, 232 min: i1, 233 max: i2, 234 }) 235 } 236 237 func (s *Node) UnmarshalYAML(node *yaml.Node) error { 238 if node != nil && node.Kind == yaml.DocumentNode { 239 return s.UnmarshalYAML(node.Content[0]) 240 } 241 def := s.Schema.GetDefinition(s.Definition) 242 if s.Context == nil { 243 s.Context = def.Context 244 } 245 246 isExpr, err := s.checkExpression(node) 247 if err != nil { 248 return err 249 } 250 if isExpr { 251 return nil 252 } 253 if def.Mapping != nil { 254 return s.checkMapping(node, def) 255 } else if def.Sequence != nil { 256 return s.checkSequence(node, def) 257 } else if def.OneOf != nil { 258 return s.checkOneOf(def, node) 259 } 260 261 if node.Kind != yaml.ScalarNode { 262 return fmt.Errorf("%sExpected a scalar got %v", formatLocation(node), getStringKind(node.Kind)) 263 } 264 265 if def.String != nil { 266 return s.checkString(node, def) 267 } else if def.Number != nil { 268 var num float64 269 return node.Decode(&num) 270 } else if def.Boolean != nil { 271 var b bool 272 return node.Decode(&b) 273 } else if def.AllowedValues != nil { 274 s := node.Value 275 for _, v := range *def.AllowedValues { 276 if s == v { 277 return nil 278 } 279 } 280 return fmt.Errorf("%sExpected one of %s got %s", formatLocation(node), strings.Join(*def.AllowedValues, ","), s) 281 } else if def.Null != nil { 282 var myNull *byte 283 return node.Decode(&myNull) 284 } 285 return errors.ErrUnsupported 286 } 287 288 func (s *Node) checkString(node *yaml.Node, def Definition) error { 289 val := node.Value 290 if def.String.Constant != "" && def.String.Constant != val { 291 return fmt.Errorf("%sExpected %s got %s", formatLocation(node), def.String.Constant, val) 292 } 293 if def.String.IsExpression { 294 parser := actionlint.NewExprParser() 295 lexer := actionlint.NewExprLexer(val + "}}") 296 exprNode, parseErr := parser.Parse(lexer) 297 if parseErr != nil { 298 return fmt.Errorf("%sFailed to parse: %s", formatLocation(node), parseErr.Message) 299 } 300 cerr := s.checkSingleExpression(exprNode) 301 if cerr != nil { 302 return fmt.Errorf("%s%w", formatLocation(node), cerr) 303 } 304 } 305 return nil 306 } 307 308 func (s *Node) checkOneOf(def Definition, node *yaml.Node) error { 309 var allErrors error 310 for _, v := range *def.OneOf { 311 sub := &Node{ 312 Definition: v, 313 Schema: s.Schema, 314 Context: append(append([]string{}, s.Context...), s.Schema.GetDefinition(v).Context...), 315 } 316 317 err := sub.UnmarshalYAML(node) 318 if err == nil { 319 return nil 320 } 321 allErrors = errors.Join(allErrors, fmt.Errorf("%sFailed to match %s: %w", formatLocation(node), v, err)) 322 } 323 return allErrors 324 } 325 326 func getStringKind(k yaml.Kind) string { 327 switch k { 328 case yaml.DocumentNode: 329 return "document" 330 case yaml.SequenceNode: 331 return "sequence" 332 case yaml.MappingNode: 333 return "mapping" 334 case yaml.ScalarNode: 335 return "scalar" 336 case yaml.AliasNode: 337 return "alias" 338 default: 339 return "unknown" 340 } 341 } 342 343 func (s *Node) checkSequence(node *yaml.Node, def Definition) error { 344 if node.Kind != yaml.SequenceNode { 345 return fmt.Errorf("%sExpected a sequence got %v", formatLocation(node), getStringKind(node.Kind)) 346 } 347 var allErrors error 348 for _, v := range node.Content { 349 allErrors = errors.Join(allErrors, (&Node{ 350 Definition: def.Sequence.ItemType, 351 Schema: s.Schema, 352 Context: append(append([]string{}, s.Context...), s.Schema.GetDefinition(def.Sequence.ItemType).Context...), 353 }).UnmarshalYAML(v)) 354 } 355 return allErrors 356 } 357 358 func formatLocation(node *yaml.Node) string { 359 return fmt.Sprintf("Line: %v Column %v: ", node.Line, node.Column) 360 } 361 362 func (s *Node) checkMapping(node *yaml.Node, def Definition) error { 363 if node.Kind != yaml.MappingNode { 364 return fmt.Errorf("%sExpected a mapping got %v", formatLocation(node), getStringKind(node.Kind)) 365 } 366 insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`) 367 var allErrors error 368 for i, k := range node.Content { 369 if i%2 == 0 { 370 if insertDirective.MatchString(k.Value) { 371 if len(s.Context) == 0 { 372 allErrors = errors.Join(allErrors, fmt.Errorf("%sinsert is not allowed here", formatLocation(k))) 373 } 374 continue 375 } 376 377 isExpr, err := s.checkExpression(k) 378 if err != nil { 379 allErrors = errors.Join(allErrors, err) 380 continue 381 } 382 if isExpr { 383 continue 384 } 385 vdef, ok := def.Mapping.Properties[k.Value] 386 if !ok { 387 if def.Mapping.LooseValueType == "" { 388 allErrors = errors.Join(allErrors, fmt.Errorf("%sUnknown Property %v", formatLocation(k), k.Value)) 389 continue 390 } 391 vdef = MappingProperty{Type: def.Mapping.LooseValueType} 392 } 393 394 if err := (&Node{ 395 Definition: vdef.Type, 396 Schema: s.Schema, 397 Context: append(append([]string{}, s.Context...), s.Schema.GetDefinition(vdef.Type).Context...), 398 }).UnmarshalYAML(node.Content[i+1]); err != nil { 399 allErrors = errors.Join(allErrors, err) 400 continue 401 } 402 } 403 } 404 return allErrors 405 }