main 0.0.1
andrew 3 weeks ago
parent de17fff9a4
commit 41a912a007

@ -3,13 +3,12 @@ package errors
// ErrorCode — тип для кода ошибки.
type ErrorCode string
// Перечисление кодов ошибок для валидации.
const (
// Общие ошибки
ErrEmptyFile ErrorCode = "EMPTY_FILE" // файл пуст или отсутствует
ErrYamlSyntax ErrorCode = "YAML_SYNTAX_ERROR" // ошибка синтаксического анализа YAML
// Ошибки, связанные с job
ErrJobNameBlank ErrorCode = "JOB_NAME_BLANK" // имя задачи пустое
ErrJobNameTooLong ErrorCode = "JOB_NAME_TOO_LONG"
ErrMissingScript ErrorCode = "MISSING_SCRIPT" // отсутствует обязательное поле script (или run)
ErrBothScriptAndRun ErrorCode = "BOTH_SCRIPT_RUN" // одновременно присутствуют script и run
ErrJobStageNotExist ErrorCode = "JOB_STAGE_NOT_EXIST" // указан stage, которого нет в списке разрешённых
@ -58,6 +57,7 @@ const (
ErrServiceInvalid ErrorCode = "SERVICE_INVALID"
ErrStageInvalid ErrorCode = "STAGE_INVALID"
ErrVariableInvalid ErrorCode = "VARIABLE_INVALID"
ErrVariableNameTooLong ErrorCode = "VARIABLE_NAME_TOO_LONG"
ErrInvalidStagesOrder ErrorCode = "INVALID_STAGES_ORDER"
ErrVariablesInvalid ErrorCode = "VARIABLES_INVALID"
ErrVariablesInvalidKey ErrorCode = "VARIABLES_INVALID_KEY"
@ -66,3 +66,53 @@ const (
ErrBooleanValue ErrorCode = "BOOLEAN_VALUE" // значение должно быть булевым
ErrIncludeRulesInvalid ErrorCode = "INCLUDE_RULES_INVALID" // значение exists или changes не является строкой или массивом строк
)
// ErrorSeverity maps each error code to a severity level.
var ErrorSeverity = map[ErrorCode]SeverityLevel{
// Critical errors
ErrEmptyFile: Critical,
ErrYamlSyntax: Critical,
ErrMissingJobs: Critical,
ErrJobNameBlank: Critical,
ErrMissingScript: Critical,
ErrBothScriptAndRun: Critical,
ErrMissingStage: Critical,
ErrJobStageNotExist: Critical,
ErrUndefinedDependency: Critical,
ErrInvalidStageOrder: Critical,
ErrUndefinedNeed: Critical,
ErrCyclicDependency: Critical,
ErrStartInMissing: Critical,
ErrStartInInvalid: Critical,
ErrStartInTooLong: Critical,
ErrStartInMustBeBlank: Critical,
ErrVariableInvalid: Critical,
ErrVariablesInvalid: Critical,
ErrVariablesInvalidKey: Critical,
ErrServiceInvalid: Critical,
ErrBeforeScriptInvalid: Critical,
ErrChangesNotArrayOfStrings: Critical,
ErrChangesInvalidType: Critical,
ErrChangesTooManyEntries: Critical,
ErrChangesMissingPaths: Critical,
ErrPathsNotArrayOfStrings: Critical,
// Warnings
ErrDuplicateNeeds: Warning,
ErrNoVisibleJob: Warning,
ErrUnknownRootKey: Warning,
ErrUnknownKey: Warning,
ErrInvalidWhen: Warning,
ErrInvalidOnly: Warning,
ErrInvalidRulesFormat: Warning,
ErrRulesOnlyExcept: Warning,
ErrRulesOnly: Warning,
ErrRulesExcept: Warning,
ErrInvalidExpressionSyntax: Warning,
ErrUnknownRulesKey: Warning,
// Notes (if needed, add informational codes here)
ErrJobNameTooLong: Note,
ErrVariableNameTooLong: Note,
ErrNeedNameTooLong: Critical,
}

@ -5,14 +5,27 @@ import (
"strings"
)
// ValidationErrorDetail хранит данные об одной ошибке.
// SeverityLevel represents the severity of an error.
type SeverityLevel string
const (
// Critical errors prevent the configuration from being valid.
Critical SeverityLevel = "CRITICAL"
// Warning are issues that are problematic but may not break the configuration.
Warning SeverityLevel = "WARNING"
// Note are non-critical messages or hints.
Note SeverityLevel = "NOTE"
)
// ValidationErrorDetail stores data about one error
type ValidationErrorDetail struct {
Code ErrorCode // Код ошибки
Message string // Текст сообщения
Line int // Номер строки, где возникла ошибка
Column int // Позиция в строке
EndLine int // Конечная строка
EndColumn int // Конечный столбец
Code ErrorCode // Error code
Message string // Text of error
Line int // Number of line, where error appeared
Column int // Column
EndLine int // Endline
EndColumn int // EndColumn
Severity SeverityLevel // Severity leevel of an error
}
type UnknownKeyError struct {
@ -23,31 +36,34 @@ type UnknownKeyError struct {
EndColumn int
}
// ValidationError представляет совокупность ошибок валидации.
// ValidationError represents all errors
type ValidationError struct {
Errors []ValidationErrorDetail
}
// Error возвращает строковое представление всех ошибок.
// Error returns string of all errors
func (ve *ValidationError) Error() string {
var sb strings.Builder
sb.WriteString("Найдены ошибки валидации:\n")
sb.WriteString("Validation errors found:\n")
for i, errDetail := range ve.Errors {
sb.WriteString(fmt.Sprintf("%d) [%s] (Line %d, Col %d): %s\n", i+1, errDetail.Code, errDetail.Line, errDetail.Column, errDetail.Message))
sb.WriteString(fmt.Sprintf("%d) [%s] (%s) (Line %d, Col %d): %s\n",
i+1, errDetail.Code, errDetail.Severity, errDetail.Line, errDetail.Column, errDetail.Message))
}
return sb.String()
}
// AddDetail добавляет новую ошибку в список.
// AddDetail adds new error in a list
func (ve *ValidationError) AddDetail(code ErrorCode, message string, line, column int) {
ve.Errors = append(ve.Errors, ValidationErrorDetail{
Code: code,
Message: message,
Line: line,
Column: column,
Severity: ErrorSeverity[code],
})
}
// AddDetailWithSpan adds a new error detail with a span.
func (ve *ValidationError) AddDetailWithSpan(code ErrorCode, message string, line, col, endLine, endCol int) {
ve.Errors = append(ve.Errors, ValidationErrorDetail{
Code: code,
@ -56,10 +72,11 @@ func (ve *ValidationError) AddDetailWithSpan(code ErrorCode, message string, lin
Column: col,
EndLine: endLine,
EndColumn: endCol,
Severity: ErrorSeverity[code],
})
}
// IsEmpty проверяет, имеются ли ошибки.
// IsEmpty check if error exists
func (ve *ValidationError) IsEmpty() bool {
return len(ve.Errors) == 0
}

@ -4,10 +4,12 @@ package errors
var ErrorMessages = map[ErrorCode]string{
ErrEmptyFile: "Please provide content of .gitflame-ci.yml",
ErrYamlSyntax: "YAML syntax error: %v",
ErrUnknownRootKey: "unexpected key: '%s'",
ErrUnknownRootKey: "unexpected key: '%s'. Currently we not support it.",
ErrBeforeScriptInvalid: "before_script config should be a string or a nested array of strings up to 10 levels deep",
ErrJobNameTooLong: "job name is too long, probably should reduce number of symbols",
ErrServiceInvalid: "config should be a hash or a string",
ErrStageInvalid: "stage at index %d is empty; stage config should be a string",
ErrVariableNameTooLong: "variable name %s is too long, probably should reduce number of symbols",
ErrVariablesInvalid: "variables config should be a map",
ErrVariablesInvalidKey: "variable '%s' uses invalid data keys: %s",
ErrMissingJobs: "no jobs found in the configuration",
@ -27,7 +29,7 @@ var ErrorMessages = map[ErrorCode]string{
ErrInvalidOnly: "job '%s': 'only' must be a non-empty string or an array of non-empty strings",
ErrNoVisibleJob: "no visible jobs found in the jobs section",
ErrCyclicDependency: "cycle detection: a cycle was detected among jobs: %v",
ErrArtifactsPathsBlank: "job '%s': artifacts paths can't be blank",
ErrArtifactsPathsBlank: "job '%s': artifacts paths can't be blank. Currently we supporting `paths' syntax only.",
ErrChangesTooManyEntries: "has too many entries (maximum 50)",
ErrChangesNotArrayOfStrings: "changes config should be an array of strings",
ErrChangesMissingPaths: "changes config hash must contain key 'paths'",

@ -44,8 +44,6 @@ type Job struct {
Rules interface{} `yaml:"rules,omitempty"` // для динамических правил
Artifacts *Artifacts `yaml:"artifacts,omitempty"` // блок artifacts
// ... могут быть и другие поля
When string `yaml:"when,omitempty"`
StartIn string `yaml:"start_in,omitempty"`
Only interface{} `yaml:"only,omitempty"`
@ -68,5 +66,6 @@ type Job struct {
// MaxJobNameLength здесь зададим некоторые константы, используемые для валидации
const (
MaxJobNameLength = 63 // Примерно, как Ci::BuildNeed::MAX_JOB_NAME_LENGTH
MaxJobNameLength = 63
MaxVariableNameLength = 63
)

@ -2,7 +2,7 @@ package gitlabcivalidator
import (
"fmt"
"gitflame.ru/CI_VLDR/errors"
"gitflame.ru/Azaki/CI_VLDR/errors"
"gopkg.in/yaml.v3"
)
@ -11,64 +11,74 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf
var config GitLabCIConfig
var root yaml.Node
// Parse YAML into the root 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 root is empty, the YAML file is empty or invalid
if len(root.Content) == 0 {
return nil, fmt.Errorf(".gitlab-ci.yml file is empty or does not contain valid data")
}
// Expect the root node to be a YAML document
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
return nil, fmt.Errorf("invalid YAML format: expected a root object")
}
// The main object is the first child
mainNode := root.Content[0]
// Decode the root object into the configuration structure
// 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)
}
// Manually parse the 'jobs' node
for i := 0; i < len(mainNode.Content)-1; i += 2 {
keyNode := mainNode.Content[i]
valNode := mainNode.Content[i+1]
// 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": {},
}
if keyNode.Value == "jobs" && valNode.Kind == yaml.MappingNode {
// 'jobs' found, parsing manually
// Initialize config.Jobs if not already set.
if config.Jobs == nil {
config.Jobs = make(map[string]*Job)
}
for j := 0; j < len(valNode.Content)-1; j += 2 {
jobKeyNode := valNode.Content[j] // job name
jobValNode := valNode.Content[j+1] // job definition (mapping or otherwise)
// Create a new job with its name and positional info.
// 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 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: jobKeyNode.Value,
Line: jobKeyNode.Line,
Column: jobKeyNode.Column,
Name: keyNode.Value,
Line: keyNode.Line,
Column: keyNode.Column,
DependencyLines: make(map[string]struct{ Line, Column int }),
}
// If the job value is a mapping node, decode its content.
if jobValNode.Kind == yaml.MappingNode {
err := jobValNode.Decode(job)
// 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)
}
validateJobMapping(jobValNode, ve)
// Process inner keys (dependencies, needs, only, etc.)
for d := 0; d < len(jobValNode.Content)-1; d += 2 {
depKey := jobValNode.Content[d]
depVal := jobValNode.Content[d+1]
// 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]
if depKey.Value == "dependencies" && depVal.Kind == yaml.SequenceNode {
for k := 0; k < len(depVal.Content); k++ {
depNode := depVal.Content[k]
@ -79,7 +89,6 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf
}{depNode.Line, depNode.Column}
}
}
if depKey.Value == "needs" && depVal.Kind == yaml.SequenceNode {
job.NeedLines = make(map[string]struct {
Line int
@ -94,7 +103,6 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf
}{needNode.Line, needNode.Column}
}
}
if depKey.Value == "only" {
if depVal.Kind == yaml.ScalarNode {
job.Only = depVal.Value
@ -108,10 +116,12 @@ func ParseGitLabCIConfig(data []byte, ve *errors.ValidationError) (*GitLabCIConf
job.OnlyLine = depKey.Line
job.OnlyColumn = depKey.Column
}
// You can add additional manual processing here.
}
} else {
// If the job value is not a mapping, leave job fields as default.
}
// If jobValNode is not a mapping, leave the job with default (nil) fields.
// Now add the job to the configuration regardless.
// Save the job in config.
config.Jobs[job.Name] = job
}
}
@ -125,7 +135,7 @@ func findUnknownRootKeysWithSpan(mainNode *yaml.Node) []errors.UnknownKeyError {
allowedKeys := map[string]struct{}{
"default": {},
"include": {},
"before_script": {},
//"before_script": {},
"image": {},
"services": {},
"after_script": {},
@ -145,7 +155,7 @@ func findUnknownRootKeysWithSpan(mainNode *yaml.Node) []errors.UnknownKeyError {
for i := 0; i < len(mainNode.Content)-1; i += 2 {
keyNode := mainNode.Content[i]
if _, ok := allowedKeys[keyNode.Value]; !ok {
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,

@ -2,7 +2,7 @@ package gitlabcivalidator
import (
"fmt"
"gitflame.ru/CI_VLDR/errors"
"gitflame.ru/Azaki/CI_VLDR/errors"
"gopkg.in/yaml.v3"
"strings"
"time"
@ -50,7 +50,7 @@ func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
// Получаем корневой узел
mainNode := root.Content[0]
// Проверяем неизвестные ключи и добавляем ошибки в объект валидации
//Проверяем неизвестные ключи и добавляем ошибки в объект валидации
unknowns := findUnknownRootKeysWithSpan(mainNode)
for _, unk := range unknowns {
validationErr.AddDetailWithSpan(errors.ErrUnknownRootKey,
@ -170,7 +170,11 @@ func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) {
}
for varName, val := range varsMap {
if isScalar(val) {
// ok
if len(varName) > MaxVariableNameLength {
ve.AddDetailWithSpan(errors.ErrVariableNameTooLong, fmt.Sprintf(errors.ErrorMessages[errors.ErrVariableNameTooLong], varName),
0, 0, 0, 0,
)
}
} else if nested, ok := val.(map[string]interface{}); ok {
invalidKeys := []string{}
for k := range nested {
@ -220,8 +224,13 @@ func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) {
ve.AddDetailWithSpan(errors.ErrJobNameBlank,
errors.ErrorMessages[errors.ErrJobNameBlank],
job.Line, job.Column, job.Line, job.Column+len(job.Name))
continue
}
if len(job.Name) > MaxJobNameLength {
ve.AddDetailWithSpan(errors.ErrJobNameTooLong, errors.ErrorMessages[errors.ErrJobNameTooLong],
job.Line, job.Column, job.Line, job.Column+len(job.Name))
}
if job.Name[0] != '.' {
visibleCount++
if job.Stage == "" {
@ -580,6 +589,9 @@ func validateJobMapping(jobNode *yaml.Node, ve *errors.ValidationError) {
"start_in": {},
"only": {},
"except": {},
"image": {},
"variables": {},
"services": {},
// add any additional allowed keys for a job...
}
validateMappingKeys(jobNode, allowedKeys, "job", ve)

@ -1,4 +1,4 @@
module gitflame.ru/CI_VLDR
module gitflame.ru/Azaki/CI_VLDR
go 1.18

@ -1,60 +1,71 @@
package main
import (
"bufio"
"fmt"
"log"
"os"
"gitflame.ru/CI_VLDR/gitlabcivalidator"
"gitflame.ru/Azaki/CI_VLDR/gitlabcivalidator"
)
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..."
`
// 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
`
// invalidYAML := `
//stages:
// - build
// - test
//jobs:
// teaaa:
// build_job:
// validateJobMapping(jobValNode, ve):
// script:
// - c
// needs: ["test_job2"]
// test_job2:
// stage: test
//`
fmt.Println("=== Пример 1: Корректный YAML ===")
err := gitlabcivalidator.ValidateGitLabCIFile(validYAML, gitlabcivalidator.ValidationOptions{})
if err != nil {
log.Fatalf("Ожидали, что ошибок не будет, но возникли: %v\n", err)
} else {
fmt.Println("Валидация успешна! Ошибок не найдено.")
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
}
fmt.Println("\n=== Пример 2: Некорректный YAML ===")
err2 := gitlabcivalidator.ValidateGitLabCIFile(invalidYAML, gitlabcivalidator.ValidationOptions{})
if err2 != nil {
fmt.Println("Ошибка валидации (ожидаемо для данного файла):")
fmt.Println(err2.Error())
err := gitlabcivalidator.ValidateGitLabCIFile(inputYAML, gitlabcivalidator.ValidationOptions{})
if err != nil {
fmt.Println(err.Error())
} else {
log.Fatal("Ожидали ошибку, но её не возникло")
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