Compare commits

..

No commits in common. 'main' and 'v0.0.2' have entirely different histories.
main ... v0.0.2

1
.gitignore vendored

@ -18,4 +18,3 @@
/tests/.env /tests/.env
.vscode/settings.json .vscode/settings.json
.devdbrc .devdbrc
main.go

@ -4,74 +4,67 @@ package errors
type ErrorCode string type ErrorCode string
const ( const (
// General Errors ErrEmptyFile ErrorCode = "EMPTY_FILE" // файл пуст или отсутствует
ErrEmptyFile ErrorCode = "FileIsEmpty" // The file is empty or missing ErrYamlSyntax ErrorCode = "YAML_SYNTAX_ERROR" // ошибка синтаксического анализа YAML
ErrYamlSyntax ErrorCode = "YAMLSyntaxError" // YAML syntax error
// Job-related Errors ErrJobNameBlank ErrorCode = "JOB_NAME_BLANK" // имя задачи пустое
ErrJobNameBlank ErrorCode = "JobNameBlank" // The job name is blank ErrJobNameTooLong ErrorCode = "JOB_NAME_TOO_LONG"
ErrJobNameTooLong ErrorCode = "JobNameTooLong" // The job name exceeds the maximum length ErrMissingScript ErrorCode = "MISSING_SCRIPT" // отсутствует обязательное поле script (или run)
ErrMissingScript ErrorCode = "MissingScript" // The required field script (or run) is missing ErrBothScriptAndRun ErrorCode = "BOTH_SCRIPT_RUN" // одновременно присутствуют script и run
ErrBothScriptAndRun ErrorCode = "BothScriptAndRun" // Both script and run are specified simultaneously ErrJobStageNotExist ErrorCode = "JOB_STAGE_NOT_EXIST" // указан stage, которого нет в списке разрешённых
ErrJobStageNotExist ErrorCode = "JobStageNotExist" // The specified stage does not exist in the allowed list ErrUndefinedDependency ErrorCode = "UNDEFINED_DEPENDENCY" // зависимость не определена в списке job'ов
ErrUndefinedDependency ErrorCode = "UndefinedDependency" // A dependency is not defined among the jobs ErrInvalidStageOrder ErrorCode = "INVALID_STAGE_ORDER" // зависимость имеет stage, который идёт позже, чем у задачи
ErrInvalidStageOrder ErrorCode = "InvalidStageOrder" // A dependency has a stage that occurs later than the job's stage ErrDuplicateNeeds ErrorCode = "DUPLICATE_NEEDS" // дублирующиеся записи в needs
ErrDuplicateNeeds ErrorCode = "DuplicateNeeds" // Duplicate entries found in needs ErrUndefinedNeed ErrorCode = "UNDEFINED_NEED" // need ссылается на несуществующую задачу
ErrUndefinedNeed ErrorCode = "UndefinedNeed" // A need refers to a non-existent job ErrNeedNameTooLong ErrorCode = "NEED_NAME_TOO_LONG" // имя need превышает допустимую длину
ErrNeedNameTooLong ErrorCode = "NeedNameTooLong" // The need name exceeds the allowed length ErrNoVisibleJob ErrorCode = "NO_VISIBLE_JOB" // нет ни одной видимой задачи
ErrNoVisibleJob ErrorCode = "NoVisibleJob" // There are no visible jobs ErrMissingJobs ErrorCode = "MISSING_JOBS"
ErrMissingJobs ErrorCode = "MissingJobs" // No jobs defined ErrArtifactsPathsBlank ErrorCode = "ARTIFACTS_PATHS_BLANK" // отсутствует или пустой блок paths в artifacts
ErrArtifactsPathsBlank ErrorCode = "ArtifactsPathsBlank" // The artifacts paths block is missing or empty ErrUnknownRootKey ErrorCode = "UNKNOWN_ROOT_KEY"
ErrUnknownRootKey ErrorCode = "UnknownRootKey" // An unknown key was found at the root level ErrInvalidWhen ErrorCode = "INVALID_WHEN"
ErrInvalidWhen ErrorCode = "InvalidWhen" // The value of 'when' is invalid ErrInvalidOnly ErrorCode = "INVALID_ONLY"
ErrInvalidOnly ErrorCode = "InvalidOnly" // The value of 'only' is invalid ErrUnknownKey ErrorCode = "UNKNOWN_KEY"
ErrUnknownKey ErrorCode = "UnknownKey" // An unknown key was found ErrMissingStage ErrorCode = "MISSING_STAGE"
ErrMissingStage ErrorCode = "MissingStage" // The stage is missing ErrInvalidChanges ErrorCode = "INVALID_CHANGES"
ErrInvalidChanges ErrorCode = "InvalidChanges" // The changes configuration is invalid ErrInvalidRulesFormat ErrorCode = "INVALID_RULES_FORMAT"
ErrInvalidRulesFormat ErrorCode = "InvalidRulesFormat" // The rules format is invalid
// Changes-related Errors // Changes
ErrChangesNotArrayOfStrings ErrorCode = "ChangesNotArrayOfStrings" // Changes config should be an array of strings ErrChangesNotArrayOfStrings ErrorCode = "CHANGES_NOT_ARRAY_OF_STRINGS"
ErrChangesInvalidType ErrorCode = "ChangesInvalidType" // Changes config is of an invalid type ErrChangesInvalidType ErrorCode = "CHANGES_INVALID_TYPE"
ErrChangesTooManyEntries ErrorCode = "ChangesTooManyEntries" // Changes config has too many entries (maximum 50) ErrChangesTooManyEntries ErrorCode = "CHANGES_TOO_MANY_ENTRIES"
ErrChangesMissingPaths ErrorCode = "ChangesMissingPaths" // The changes config hash must contain the key 'paths' ErrChangesMissingPaths ErrorCode = "CHANGES_MISSING_PATHS"
// Paths-related Errors // Paths
ErrPathsNotArrayOfStrings ErrorCode = "PathsNotArrayOfStrings" // Paths config should be an array of strings ErrPathsNotArrayOfStrings ErrorCode = "PATHS_NOT_ARRAY_OF_STRINGS"
// Job Delayed Parameters // Job delayed parameters
ErrStartInMissing ErrorCode = "StartInMissing" // For delayed jobs, start_in is missing ErrStartInMissing ErrorCode = "START_IN_MISSING"
ErrStartInInvalid ErrorCode = "StartInInvalid" // start_in is not a valid duration ErrStartInInvalid ErrorCode = "START_IN_INVALID"
ErrStartInTooLong ErrorCode = "StartInTooLong" // start_in exceeds the allowed limit ErrStartInTooLong ErrorCode = "START_IN_TOO_LONG"
ErrStartInMustBeBlank ErrorCode = "StartInMustBeBlank" // For non-delayed jobs, start_in must be blank ErrStartInMustBeBlank ErrorCode = "START_IN_MUST_BE_BLANK"
// Dependencies / Needs Consistency // Dependencies / Needs consistency
ErrDependencyNotInNeeds ErrorCode = "DependencyNotInNeeds" // A dependency is not included in needs ErrDependencyNotInNeeds ErrorCode = "DEPENDENCY_NOT_IN_NEEDS"
// Rules Validation // Rules validation
ErrRulesOnlyExcept ErrorCode = "RulesOnlyExcept" // Only and except cannot be used with rules ErrRulesOnlyExcept ErrorCode = "RULES_ONLY_EXCEPT"
ErrRulesOnly ErrorCode = "RulesOnly" // Only cannot be used with rules ErrRulesOnly ErrorCode = "RULES_ONLY"
ErrRulesExcept ErrorCode = "RulesExcept" // Except cannot be used with rules ErrRulesExcept ErrorCode = "RULES_EXCEPT"
ErrInvalidExpressionSyntax ErrorCode = "InvalidExpressionSyntax" // The expression syntax is invalid ErrInvalidExpressionSyntax ErrorCode = "INVALID_EXPRESSION_SYNTAX"
ErrUnknownRulesKey ErrorCode = "UnknownRulesKey" // An unknown key was found in rules ErrUnknownRulesKey ErrorCode = "UNKNOWN_RULES_KEY"
// Other Job-related Errors ErrBeforeScriptInvalid ErrorCode = "BEFORE_SCRIPT_INVALID"
ErrBeforeScriptInvalid ErrorCode = "BeforeScriptInvalid" // The before_script configuration is invalid ErrServiceInvalid ErrorCode = "SERVICE_INVALID"
ErrServiceInvalid ErrorCode = "ServiceInvalid" // The service configuration is invalid ErrStageInvalid ErrorCode = "STAGE_INVALID"
ErrStageInvalid ErrorCode = "StageInvalid" // The stage configuration is invalid ErrVariableInvalid ErrorCode = "VARIABLE_INVALID"
ErrVariableInvalid ErrorCode = "VariableInvalid" // The variable is invalid ErrVariableNameTooLong ErrorCode = "VARIABLE_NAME_TOO_LONG"
ErrVariableNameTooLong ErrorCode = "VariableNameTooLong" // The variable name exceeds the maximum length ErrInvalidStagesOrder ErrorCode = "INVALID_STAGES_ORDER"
ErrInvalidStagesOrder ErrorCode = "InvalidStagesOrder" // The stage order is invalid ErrVariablesInvalid ErrorCode = "VARIABLES_INVALID"
ErrVariablesInvalid ErrorCode = "VariablesInvalid" // The variables configuration is invalid ErrVariablesInvalidKey ErrorCode = "VARIABLES_INVALID_KEY"
ErrVariablesInvalidKey ErrorCode = "VariablesInvalidKey" // The variable uses invalid keys
// Additional Errors ErrCyclicDependency ErrorCode = "CYCLIC_DEPENDENCY" // обнаружена циклическая зависимость
ErrCyclicDependency ErrorCode = "CyclicDependency" // A cyclic dependency was detected ErrBooleanValue ErrorCode = "BOOLEAN_VALUE" // значение должно быть булевым
ErrBooleanValue ErrorCode = "BooleanValue" // The value must be boolean ErrIncludeRulesInvalid ErrorCode = "INCLUDE_RULES_INVALID" // значение exists или changes не является строкой или массивом строк
ErrIncludeRulesInvalid ErrorCode = "IncludeRulesInvalid" // The 'exists' or 'changes' value is not a string or an array of strings
// Style Note (Informational/Note)
ErrMissingFinalNewline ErrorCode = "MissingFinalNewline" // The file is missing a final empty newline
) )
// ErrorSeverity maps each error code to a severity level. // ErrorSeverity maps each error code to a severity level.
@ -119,9 +112,7 @@ var ErrorSeverity = map[ErrorCode]SeverityLevel{
ErrUnknownRulesKey: Warning, ErrUnknownRulesKey: Warning,
// Notes (if needed, add informational codes here) // Notes (if needed, add informational codes here)
ErrJobNameTooLong: Note, ErrJobNameTooLong: Note,
ErrVariableNameTooLong: Note, ErrVariableNameTooLong: Note,
ErrNeedNameTooLong: Note, ErrNeedNameTooLong: Critical,
ErrMissingFinalNewline: Note,
ErrDependencyNotInNeeds: Note,
} }

@ -46,4 +46,4 @@ var ErrorMessages = map[ErrorCode]string{
ErrIncludeRulesInvalid: "include rule key '%s' should be a string or an array of strings", ErrIncludeRulesInvalid: "include rule key '%s' should be a string or an array of strings",
ErrVariableInvalid: "variable '%s' must be a scalar or a map", ErrVariableInvalid: "variable '%s' must be a scalar or a map",
ErrInvalidStagesOrder: "job '%s' (Line %d, Col %d) has need '%s' (Line %d, Col %d-%d) with a stage occurring later than '%s'", ErrInvalidStagesOrder: "job '%s' (Line %d, Col %d) has need '%s' (Line %d, Col %d-%d) with a stage occurring later than '%s'",
ErrMissingFinalNewline: "File should end with an empty line for better style"} }

@ -1,6 +1,4 @@
package gitflamecivalidator package gitlabcivalidator
import "gopkg.in/yaml.v3"
// ValidationOptions описывает настройки валидации. // ValidationOptions описывает настройки валидации.
type ValidationOptions struct { type ValidationOptions struct {
@ -10,18 +8,17 @@ type ValidationOptions struct {
// GitLabCIConfig описывает структуру корневого объекта .gitlab-ci.yml // GitLabCIConfig описывает структуру корневого объекта .gitlab-ci.yml
type GitLabCIConfig struct { type GitLabCIConfig struct {
Default interface{} `yaml:"default,omitempty"` Default interface{} `yaml:"default,omitempty"`
Include interface{} `yaml:"include,omitempty"` Include interface{} `yaml:"include,omitempty"`
BeforeScript interface{} `yaml:"before_script,omitempty"` BeforeScript interface{} `yaml:"before_script,omitempty"`
Image interface{} `yaml:"image,omitempty"` Image interface{} `yaml:"image,omitempty"`
Services interface{} `yaml:"services,omitempty"` Services interface{} `yaml:"services,omitempty"`
AfterScript interface{} `yaml:"after_script,omitempty"` AfterScript interface{} `yaml:"after_script,omitempty"`
Variables interface{} `yaml:"variables,omitempty"` Variables interface{} `yaml:"variables,omitempty"`
VariablesNode *yaml.Node `yaml:"-"` Stages []string `yaml:"stages,omitempty"`
Stages []string `yaml:"stages,omitempty"` Cache interface{} `yaml:"cache,omitempty"`
Cache interface{} `yaml:"cache,omitempty"` Workflow interface{} `yaml:"workflow,omitempty"`
Workflow interface{} `yaml:"workflow,omitempty"` Jobs map[string]*Job `yaml:"jobs,omitempty"`
Jobs map[string]*Job `yaml:"jobs,omitempty"`
Changes interface{} `yaml:"changes,omitempty"` Changes interface{} `yaml:"changes,omitempty"`
Paths interface{} `yaml:"paths,omitempty"` Paths interface{} `yaml:"paths,omitempty"`
@ -54,9 +51,6 @@ type Job struct {
OnlyColumn int `yaml:"-"` OnlyColumn int `yaml:"-"`
Except interface{} `yaml:"except,omitempty"` Except interface{} `yaml:"except,omitempty"`
Variables interface{} `yaml:"variables,omitempty"`
VariablesNode *yaml.Node `yaml:"-"`
Line int `yaml:"-"` // Строка в файле Line int `yaml:"-"` // Строка в файле
Column int `yaml:"-"` // Позиция в строке Column int `yaml:"-"` // Позиция в строке
DependencyLines map[string]struct { DependencyLines map[string]struct {

@ -1,9 +1,9 @@
package gitflamecivalidator package gitlabcivalidator
import ( import (
"fmt" "fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"tea.gitpark.ru/Azaki/ci_validator/errors" "tea.gitpark.ru/Azaki/CI_VALIDATOR/errors"
) )
// ParseGitLabCIConfig parses the YAML and returns the GitLabCIConfig structure. // ParseGitLabCIConfig parses the YAML and returns the GitLabCIConfig structure.
@ -56,10 +56,6 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf
for i := 0; i < len(mainNode.Content)-1; i += 2 { for i := 0; i < len(mainNode.Content)-1; i += 2 {
keyNode := mainNode.Content[i] keyNode := mainNode.Content[i]
valNode := mainNode.Content[i+1] 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 the key is not one of the allowed top-level keys, treat it as a job.
if _, ok := allowedKeys[keyNode.Value]; !ok { if _, ok := allowedKeys[keyNode.Value]; !ok {
// Create a new Job. // Create a new Job.
@ -83,33 +79,31 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf
for d := 0; d < len(valNode.Content)-1; d += 2 { for d := 0; d < len(valNode.Content)-1; d += 2 {
depKey := valNode.Content[d] depKey := valNode.Content[d]
depVal := valNode.Content[d+1] depVal := valNode.Content[d+1]
switch depKey.Value { if depKey.Value == "dependencies" && depVal.Kind == yaml.SequenceNode {
case "dependencies": for k := 0; k < len(depVal.Content); k++ {
if depVal.Kind == yaml.SequenceNode { depNode := depVal.Content[k]
for k := 0; k < len(depVal.Content); k++ { job.Dependencies = append(job.Dependencies, depNode.Value)
depNode := depVal.Content[k] job.DependencyLines[depNode.Value] = struct {
job.DependencyLines[depNode.Value] = struct { Line int
Line int Column int
Column int }{depNode.Line, depNode.Column}
}{depNode.Line, depNode.Column}
}
} }
case "needs": }
if depVal.Kind == yaml.SequenceNode { if depKey.Value == "needs" && depVal.Kind == yaml.SequenceNode {
job.NeedLines = make(map[string]struct { 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 Line int
Column int Column int
}) }{needNode.Line, needNode.Column}
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 depKey.Value == "only" {
if depVal.Kind == yaml.ScalarNode { if depVal.Kind == yaml.ScalarNode {
job.Only = depVal.Value job.Only = depVal.Value
} else if depVal.Kind == yaml.SequenceNode { } else if depVal.Kind == yaml.SequenceNode {
@ -121,15 +115,8 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf
} }
job.OnlyLine = depKey.Line job.OnlyLine = depKey.Line
job.OnlyColumn = depKey.Column 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
} }
// You can add additional manual processing here.
} }
} else { } else {
// If the job value is not a mapping, leave job fields as default. // If the job value is not a mapping, leave job fields as default.

@ -1,4 +1,4 @@
package gitflamecivalidator package gitlabcivalidator
import ( import (
"fmt" "fmt"

@ -1,10 +1,10 @@
package gitflamecivalidator package gitlabcivalidator
import ( import (
"fmt" "fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"strings" "strings"
"tea.gitpark.ru/Azaki/ci_validator/errors" "tea.gitpark.ru/Azaki/CI_VALIDATOR/errors"
"time" "time"
) )
@ -31,19 +31,19 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
err := yaml.Unmarshal([]byte(content), &root) err := yaml.Unmarshal([]byte(content), &root)
if err != nil { if err != nil {
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
fmt.Sprintf("YAML parsing error: %v", err), fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err),
0, 0, 0, 0) 0, 0, 0, 0)
return validationErr return validationErr
} }
if len(root.Content) == 0 { if len(root.Content) == 0 {
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
".gitflame-ci.yml file is empty or does not contain valid data", "файл .gitlab-ci.yml пуст или не содержит допустимых данных",
0, 0, 0, 0) 0, 0, 0, 0)
return validationErr return validationErr
} }
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 { if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax, validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
"invalid YAML format: a root object was expected", "некорректный формат YAML: ожидался корневой объект",
0, 0, 0, 0) 0, 0, 0, 0)
return validationErr return validationErr
} }
@ -75,7 +75,6 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
validateStages(cfg, validationErr) validateStages(cfg, validationErr)
validateVariables(cfg, validationErr) validateVariables(cfg, validationErr)
validateIncludeRules(cfg, validationErr) validateIncludeRules(cfg, validationErr)
checkFinalEmptyLine(content, validationErr)
if !validationErr.IsEmpty() { if !validationErr.IsEmpty() {
return validationErr return validationErr
@ -154,123 +153,46 @@ func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) {
} }
} }
// validateVariablesMap проверяет карту переменных varsMap, используя данные из YAMLузла varsNode для получения позиций. func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) {
func validateVariablesMap(varsMap map[string]interface{}, varsNode *yaml.Node, ve *errors.ValidationError, posGetter func(varName string) (int, int)) { if cfg.Variables == nil {
return
}
varsMap, ok := cfg.Variables.(map[string]interface{})
if !ok {
ve.AddDetailWithSpan(errors.ErrVariablesInvalid, errors.ErrorMessages[errors.ErrVariablesInvalid], 0, 0, 0, 0)
return
}
allowedKeys := map[string]struct{}{ allowedKeys := map[string]struct{}{
"value": {}, "value": {},
"description": {}, "description": {},
"expand": {}, "expand": {},
"options": {}, "options": {},
} }
for varName, val := range varsMap {
// Проходим по всем ключам, определённым в YAMLузле if isScalar(val) {
for i := 0; i < len(varsNode.Content)-1; i += 2 { if len(varName) > MaxVariableNameLength {
keyNode := varsNode.Content[i] ve.AddDetailWithSpan(errors.ErrVariableNameTooLong, fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableNameTooLong], varName),
varName := keyNode.Value 0, 0, 0, 0,
// Получаем позицию для этой переменной через posGetter.
line, col := posGetter(varName)
// Проверяем длину имени переменной
if len(varName) > MaxVariableNameLength {
ve.AddDetailWithSpan(
errors.ErrVariableNameTooLong,
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableNameTooLong], varName),
line, col, line, col+len(varName),
)
}
// Проверяем, что значение переменной присутствует и корректно
if val, exists := varsMap[varName]; exists {
if !isScalar(val) {
ve.AddDetailWithSpan(
errors.ErrVariableInvalid,
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableInvalid], varName),
line, col, line, col+len(varName),
) )
} }
if nested, ok := val.(map[string]interface{}); ok { } else if nested, ok := val.(map[string]interface{}); ok {
var invalidKeys []string invalidKeys := []string{}
for k := range nested { for k := range nested {
if _, allowed := allowedKeys[k]; !allowed { if _, allowed := allowedKeys[k]; !allowed {
invalidKeys = append(invalidKeys, k) invalidKeys = append(invalidKeys, k)
}
}
if len(invalidKeys) > 0 {
ve.AddDetailWithSpan(
errors.ErrVariablesInvalidKey,
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariablesInvalidKey], varName, strings.Join(invalidKeys, ", ")),
line, col, line, col+len(varName),
)
} }
} }
} if len(invalidKeys) > 0 {
} ve.AddDetailWithSpan(errors.ErrVariablesInvalidKey,
} fmt.Sprintf(errors.ErrorMessages[errors.ErrVariablesInvalidKey], varName, strings.Join(invalidKeys, ", ")),
0, 0, 0, 0)
func validateJobVariables(job *Job, ve *errors.ValidationError) {
if job.VariablesNode == nil {
return
}
if job.VariablesNode.Kind != yaml.MappingNode {
ve.AddDetailWithSpan(
errors.ErrVariablesInvalid,
"job variables should be a map",
job.Line, job.Column, job.Line, job.Column+1,
)
return
}
varsMap, ok := job.Variables.(map[string]interface{})
if !ok {
ve.AddDetailWithSpan(
errors.ErrVariablesInvalid,
"job variables should be a map",
job.Line, job.Column, job.Line, job.Column+1,
)
return
}
validateVariablesMap(varsMap, job.VariablesNode, ve, func(varName string) (int, int) {
// Ищем переменную в узле job.VariablesNode
for i := 0; i < len(job.VariablesNode.Content)-1; i += 2 {
keyNode := job.VariablesNode.Content[i]
if keyNode.Value == varName {
return keyNode.Line, keyNode.Column
} }
} else {
ve.AddDetailWithSpan(errors.ErrVariableInvalid,
fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableInvalid], varName),
0, 0, 0, 0)
} }
return job.Line, job.Column // если не найдено, используем позицию job
})
}
func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) {
if cfg.VariablesNode == nil {
return
}
if cfg.VariablesNode.Kind != yaml.MappingNode {
ve.AddDetailWithSpan(
errors.ErrVariablesInvalid,
errors.ErrorMessages[errors.ErrVariablesInvalid],
0, 0, 0, 0,
)
return
} }
varsMap, ok := cfg.Variables.(map[string]interface{})
if !ok {
ve.AddDetailWithSpan(
errors.ErrVariablesInvalid,
errors.ErrorMessages[errors.ErrVariablesInvalid],
0, 0, 0, 0,
)
return
}
validateVariablesMap(varsMap, cfg.VariablesNode, ve, func(varName string) (int, int) {
// Ищем в YAML узле переменной с именем varName
for i := 0; i < len(cfg.VariablesNode.Content)-1; i += 2 {
keyNode := cfg.VariablesNode.Content[i]
if keyNode.Value == varName {
return keyNode.Line, keyNode.Column
}
}
return 0, 0
})
} }
func isScalar(val interface{}) bool { func isScalar(val interface{}) bool {
@ -413,7 +335,6 @@ func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) {
validateJobRules(job, ve) validateJobRules(job, ve)
validateJobWhen(job, ve) validateJobWhen(job, ve)
validateJobOnly(job, ve) validateJobOnly(job, ve)
validateJobVariables(job, ve)
} }
if visibleCount == 0 { if visibleCount == 0 {
@ -877,20 +798,3 @@ func validateIncludeRuleMap(m map[string]interface{}, ve *errors.ValidationError
} }
} }
} }
// checkFinalEmptyLine проверяет, что файл заканчивается пустой строкой.
func checkFinalEmptyLine(content string, ve *errors.ValidationError) {
// Если содержимое не заканчивается символом новой строки...
if !strings.HasSuffix(content, "\n") {
lines := strings.Split(content, "\n")
lastLineIndex := len(lines)
lastLineContent := lines[lastLineIndex-1]
lineNum := lastLineIndex
colNum := len(lastLineContent) + 1
ve.AddDetailWithSpan(
errors.ErrMissingFinalNewline,
errors.ErrorMessages[errors.ErrMissingFinalNewline],
lineNum, colNum, lineNum, colNum,
)
}
}

@ -1,5 +1,5 @@
module tea.gitpark.ru/Azaki/ci_validator module tea.gitpark.ru/Azaki/CI_VALIDATOR
go 1.22 go 1.18
require gopkg.in/yaml.v3 v3.0.1 require gopkg.in/yaml.v3 v3.0.1

@ -1,5 +1,71 @@
package main package main
import (
"bufio"
"fmt"
"os"
"tea.gitpark.ru/Azaki/CI_VALIDATOR/gitlabcivalidator"
)
func main() { func main() {
// Корректный (упрощённый) YAML
// validYAML := `
//stages:
// - build
// - test
//jobs:
// build_job:
// stage: build
// script:
// - echo "Building..."
// test_job:
// stage: test
// dependencies: ["build_job"]
// script:
// - echo "Testing..."
//`
// Тип, ссылка и текст ошибки, показатели где нахоидтся ошибка...
// Некорректный YAML — отсутствует видимая задача, есть только скрытая
// invalidYAML := `
//stages:
// - build
// - test
//jobs:
// teaaa:
// build_job:
// validateJobMapping(jobValNode, ve):
// script:
// - c
// needs: ["test_job2"]
// test_job2:
// stage: test
//`
scanner := bufio.NewScanner(os.Stdin)
var inputYAML string
for scanner.Scan() {
inputYAML += scanner.Text() + "\n"
}
if err := scanner.Err(); err != nil {
fmt.Println("Ошибка чтения ввода:", err)
return
}
err := gitlabcivalidator.ValidateGitLabCIFile(inputYAML, gitlabcivalidator.ValidationOptions{})
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("no errors")
}
//fmt.Println("\n=== Пример 2: Некорректный YAML ===")
//err2 := gitlabcivalidator.ValidateGitLabCIFile(invalidYAML, gitlabcivalidator.ValidationOptions{})
//if err2 != nil {
// fmt.Println("Ошибка валидации (ожидаемо для данного файла):")
// fmt.Println(err2.Error())
//} else {
// log.Fatal("Ожидали ошибку, но её не возникло")
//}
} }

Loading…
Cancel
Save