package gitflamecivalidator import ( "fmt" "gopkg.in/yaml.v3" "tea.gitpark.ru/Azaki/ci_validator/errors" ) // ParseGitLabCIConfig parses the YAML and returns the GitLabCIConfig structure. func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConfig, error) { var config GitLabCIConfig var root yaml.Node // Parse YAML into the root node. err := yaml.Unmarshal(data, &root) if err != nil { return nil, fmt.Errorf("YAML syntax error: %w", err) } if len(root.Content) == 0 { return nil, fmt.Errorf(".gitlab-ci.yml file is empty or does not contain valid data") } if root.Kind != yaml.DocumentNode || len(root.Content) == 0 { return nil, fmt.Errorf("invalid YAML format: expected a root object") } mainNode := root.Content[0] // Decode known top-level keys into config. err = mainNode.Decode(&config) if err != nil { return nil, fmt.Errorf("failed to decode GitLab CI configuration: %w", err) } // Allowed top-level keys that are NOT jobs. allowedKeys := map[string]struct{}{ "default": {}, "include": {}, "before_script": {}, "image": {}, "services": {}, "after_script": {}, "variables": {}, "stages": {}, "cache": {}, "workflow": {}, "changes": {}, "paths": {}, } // Initialize config.Jobs if not already set. if config.Jobs == nil { config.Jobs = make(map[string]*Job) } // Now, iterate over the top-level mapping keys. if mainNode.Kind == yaml.MappingNode { for i := 0; i < len(mainNode.Content)-1; i += 2 { keyNode := mainNode.Content[i] valNode := mainNode.Content[i+1] if keyNode.Value == "variables" { config.VariablesNode = valNode } // If the key is not one of the allowed top-level keys, treat it as a job. if _, ok := allowedKeys[keyNode.Value]; !ok { // Create a new Job. job := &Job{ Name: keyNode.Value, Line: keyNode.Line, Column: keyNode.Column, DependencyLines: make(map[string]struct{ Line, Column int }), } // Even if the job value is not a mapping node, add the job to config for further validation. if valNode.Kind == yaml.MappingNode { // Optionally, you can validate unknown keys inside the job mapping here. // For example: validateJobMapping(valNode, ve) // Decode the job mapping. err = valNode.Decode(job) if err != nil { return nil, fmt.Errorf("failed to decode job '%s': %w", job.Name, err) } // Process inner fields manually (e.g. dependencies, needs, only, etc.) for d := 0; d < len(valNode.Content)-1; d += 2 { depKey := valNode.Content[d] depVal := valNode.Content[d+1] switch depKey.Value { case "dependencies": if depVal.Kind == yaml.SequenceNode { for k := 0; k < len(depVal.Content); k++ { depNode := depVal.Content[k] job.DependencyLines[depNode.Value] = struct { Line int Column int }{depNode.Line, depNode.Column} } } case "needs": if depVal.Kind == yaml.SequenceNode { job.NeedLines = make(map[string]struct { Line int Column int }) for k := 0; k < len(depVal.Content); k++ { needNode := depVal.Content[k] job.Needs = append(job.Needs, needNode.Value) job.NeedLines[needNode.Value] = struct { Line int Column int }{needNode.Line, needNode.Column} } } case "only": if depVal.Kind == yaml.ScalarNode { job.Only = depVal.Value } else if depVal.Kind == yaml.SequenceNode { var arr []string for k := 0; k < len(depVal.Content); k++ { arr = append(arr, depVal.Content[k].Value) } job.Only = arr } job.OnlyLine = depKey.Line job.OnlyColumn = depKey.Column case "variables": // Заполняем поля переменных для job. job.VariablesNode = depVal var vars map[string]interface{} if err := depVal.Decode(&vars); err != nil { return nil, fmt.Errorf("failed to decode variables for job '%s': %w", job.Name, err) } job.Variables = vars } } } else { // If the job value is not a mapping, leave job fields as default. } // Save the job in config. config.Jobs[job.Name] = job } } } return &config, nil } // findUnknownRootKeysWithSpan scans the mainNode for unknown keys and returns their information. func findUnknownRootKeysWithSpan(mainNode *yaml.Node) []errors.UnknownKeyError { allowedKeys := map[string]struct{}{ "default": {}, "include": {}, //"before_script": {}, "image": {}, "services": {}, "after_script": {}, "variables": {}, "stages": {}, "cache": {}, "workflow": {}, "jobs": {}, "changes": {}, "paths": {}, } var unknowns []errors.UnknownKeyError if mainNode.Kind != yaml.MappingNode { return unknowns } for i := 0; i < len(mainNode.Content)-1; i += 2 { keyNode := mainNode.Content[i] if _, ok := allowedKeys[keyNode.Value]; !ok && keyNode.Value == "before_script" { endLine, endCol := keyNode.Line, keyNode.Column+len(keyNode.Value) unknowns = append(unknowns, errors.UnknownKeyError{ Key: keyNode.Value, Line: keyNode.Line, Column: keyNode.Column, EndLine: endLine, EndColumn: endCol, }) } } return unknowns }