|
|
package gitlabcivalidator
|
|
|
|
|
|
import (
|
|
|
"fmt"
|
|
|
"gitflame.ru/CI_VLDR/errors"
|
|
|
"gopkg.in/yaml.v3"
|
|
|
"strings"
|
|
|
"time"
|
|
|
)
|
|
|
|
|
|
// computeEndPosition возвращает конечные координаты для фрагмента, предполагая, что текст находится на одной строке.
|
|
|
func computeEndPosition(startLine, startColumn int, text string) (endLine, endColumn int) {
|
|
|
// Если текст пустой, выделим хотя бы один символ
|
|
|
if len(text) == 0 {
|
|
|
return startLine, startColumn + 1
|
|
|
}
|
|
|
return startLine, startColumn + len(text)
|
|
|
}
|
|
|
|
|
|
// ValidateGitLabCIFile – главная точка входа в библиотеку.
|
|
|
func ValidateGitLabCIFile(content string, opts ValidationOptions) error {
|
|
|
validationErr := &errors.ValidationError{}
|
|
|
|
|
|
if len(content) == 0 {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrEmptyFile, "Please provide content of .gitflame-ci.yml", 0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
// Повторно распарсить YAML, чтобы получить узел и позиционную информацию
|
|
|
var root yaml.Node
|
|
|
err := yaml.Unmarshal([]byte(content), &root)
|
|
|
if err != nil {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err),
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
if len(root.Content) == 0 {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
"файл .gitlab-ci.yml пуст или не содержит допустимых данных",
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
if root.Kind != yaml.DocumentNode || len(root.Content) == 0 {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
"некорректный формат YAML: ожидался корневой объект",
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
// Получаем корневой узел
|
|
|
mainNode := root.Content[0]
|
|
|
// Проверяем неизвестные ключи и добавляем ошибки в объект валидации
|
|
|
unknowns := findUnknownRootKeysWithSpan(mainNode)
|
|
|
for _, unk := range unknowns {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrUnknownRootKey,
|
|
|
fmt.Sprintf("unexpected key: '%s'", unk.Key),
|
|
|
unk.Line, unk.Column, unk.EndLine, unk.EndColumn)
|
|
|
}
|
|
|
|
|
|
// Парсим YAML в структуру конфигурации
|
|
|
cfg, err := ParseGitLabCIConfig([]byte(content))
|
|
|
if err != nil {
|
|
|
validationErr.AddDetailWithSpan(errors.ErrYamlSyntax,
|
|
|
fmt.Sprintf("ошибка синтаксического анализа YAML: %v", err),
|
|
|
0, 0, 0, 0)
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
validateRootObject(cfg, validationErr)
|
|
|
validateJobs(cfg, validationErr)
|
|
|
validateChanges(cfg, validationErr)
|
|
|
validatePaths(cfg, validationErr)
|
|
|
validateService(cfg, validationErr)
|
|
|
validateStages(cfg, validationErr)
|
|
|
validateVariables(cfg, validationErr)
|
|
|
validateIncludeRules(cfg, validationErr)
|
|
|
|
|
|
if !validationErr.IsEmpty() {
|
|
|
return validationErr
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func checkSHAExistsInBranchesOrTags(sha string) bool {
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
func validateRootObject(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
validateBeforeScript(cfg.BeforeScript, ve)
|
|
|
}
|
|
|
|
|
|
func validateBeforeScript(before interface{}, ve *errors.ValidationError) {
|
|
|
if before == nil {
|
|
|
return
|
|
|
}
|
|
|
if _, ok := before.(string); ok {
|
|
|
return
|
|
|
}
|
|
|
if arr, ok := before.([]interface{}); ok {
|
|
|
if !validateNestedStringArray(arr, 1, 10) {
|
|
|
ve.AddDetailWithSpan(errors.ErrBeforeScriptInvalid,
|
|
|
"before_script config should be a string or a nested array of strings up to 10 levels deep",
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrBeforeScriptInvalid,
|
|
|
"before_script config should be a string or a nested array of strings up to 10 levels deep",
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateNestedStringArray(arr []interface{}, currentLevel, maxLevel int) bool {
|
|
|
if currentLevel > maxLevel {
|
|
|
return false
|
|
|
}
|
|
|
for _, item := range arr {
|
|
|
switch item.(type) {
|
|
|
case string:
|
|
|
case []interface{}:
|
|
|
if !validateNestedStringArray(item.([]interface{}), currentLevel+1, maxLevel) {
|
|
|
return false
|
|
|
}
|
|
|
default:
|
|
|
return false
|
|
|
}
|
|
|
}
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
func validateService(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Services == nil {
|
|
|
return
|
|
|
}
|
|
|
switch cfg.Services.(type) {
|
|
|
case string:
|
|
|
case map[string]interface{}:
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrServiceInvalid,
|
|
|
"config should be a hash or a string",
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateStages(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
for i, st := range cfg.Stages {
|
|
|
if strings.TrimSpace(st) == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrStageInvalid,
|
|
|
fmt.Sprintf("stage at index %d is empty; stage config should be a string", i),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateVariables(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Variables == nil {
|
|
|
return
|
|
|
}
|
|
|
varsMap, ok := cfg.Variables.(map[string]interface{})
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrVariablesInvalid, "variables config should be a map", 0, 0, 0, 0)
|
|
|
return
|
|
|
}
|
|
|
allowedKeys := map[string]struct{}{
|
|
|
"value": {},
|
|
|
"description": {},
|
|
|
"expand": {},
|
|
|
"options": {},
|
|
|
}
|
|
|
for varName, val := range varsMap {
|
|
|
if isScalar(val) {
|
|
|
// ok
|
|
|
} else if nested, ok := val.(map[string]interface{}); ok {
|
|
|
invalidKeys := []string{}
|
|
|
for k := range nested {
|
|
|
if _, allowed := allowedKeys[k]; !allowed {
|
|
|
invalidKeys = append(invalidKeys, k)
|
|
|
}
|
|
|
}
|
|
|
if len(invalidKeys) > 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrVariablesInvalidKey,
|
|
|
fmt.Sprintf("variable '%s' uses invalid data keys: %s", varName, strings.Join(invalidKeys, ", ")),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrVariablesInvalid,
|
|
|
fmt.Sprintf("variable '%s' must be a scalar or a map", varName),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func isScalar(val interface{}) bool {
|
|
|
switch val.(type) {
|
|
|
case string, bool, int, int64, float64, float32:
|
|
|
return true
|
|
|
default:
|
|
|
return false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobs(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Jobs == nil || len(cfg.Jobs) == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrMissingJobs, "отсутствуют job'ы (jobs) в конфигурации", 0, 0, 0, 0)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
stagesAllowed := make(map[string]struct{})
|
|
|
for _, st := range cfg.Stages {
|
|
|
stagesAllowed[st] = struct{}{}
|
|
|
}
|
|
|
|
|
|
visibleCount := 0
|
|
|
dependencyGraph := make(map[string][]string)
|
|
|
|
|
|
for _, job := range cfg.Jobs {
|
|
|
// a) Проверка имени задачи
|
|
|
if job.Name == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrJobNameBlank,
|
|
|
"job name can't be blank",
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
continue
|
|
|
}
|
|
|
if job.Name[0] != '.' {
|
|
|
visibleCount++
|
|
|
}
|
|
|
|
|
|
// d) Проверяем наличие хотя бы одного из script, run или trigger
|
|
|
if job.Script == nil && job.Run == nil && job.Trigger == nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrMissingScript,
|
|
|
fmt.Sprintf("задача '%s' не содержит script, run или trigger", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
|
|
|
// e) Проверка stage
|
|
|
if job.Stage != "" {
|
|
|
if _, ok := stagesAllowed[job.Stage]; !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrJobStageNotExist,
|
|
|
fmt.Sprintf("задача '%s': выбранный stage '%s' не существует; доступные stage: %v", job.Name, job.Stage, cfg.Stages),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(job.Stage))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// f) Проверка dependencies
|
|
|
dependencyGraph[job.Name] = append(dependencyGraph[job.Name], job.Dependencies...)
|
|
|
for _, dep := range job.Dependencies {
|
|
|
depPos, _ := job.DependencyLines[dep]
|
|
|
if _, ok := cfg.Jobs[dep]; !ok {
|
|
|
endLine, endCol := computeEndPosition(depPos.Line, depPos.Column, dep)
|
|
|
ve.AddDetailWithSpan(errors.ErrUndefinedDependency,
|
|
|
fmt.Sprintf("задача '%s' (Line %d, Col %d) имеет зависимость '%s' (Line %d, Col %d-%d), которой нет среди job'ов",
|
|
|
job.Name, job.Line, job.Column, dep, depPos.Line, depPos.Column, endCol),
|
|
|
job.Line, job.Column, endLine, endCol)
|
|
|
} else {
|
|
|
if !checkStageOrder(cfg, dep, job.Name) {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidStageOrder,
|
|
|
fmt.Sprintf("задача '%s' имеет зависимость '%s' со стадией, идущей позже, чем '%s'",
|
|
|
job.Name, dep, job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(dep))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// g) Проверка блока needs
|
|
|
if len(job.Needs) > 0 {
|
|
|
needNames := make(map[string]struct{})
|
|
|
for _, nd := range job.Needs {
|
|
|
// Проверка дублирования
|
|
|
if _, exist := needNames[nd]; exist {
|
|
|
ndStartLine, ndStartCol := job.NeedLines[nd].Line, job.NeedLines[nd].Column
|
|
|
ndEndLine, ndEndCol := computeEndPosition(ndStartLine, ndStartCol, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrDuplicateNeeds,
|
|
|
fmt.Sprintf("задача '%s' содержит дублирующуюся зависимость '%s' в needs", job.Name, nd),
|
|
|
ndStartLine, ndStartCol, ndEndLine, ndEndCol)
|
|
|
} else {
|
|
|
needNames[nd] = struct{}{}
|
|
|
}
|
|
|
|
|
|
// Если имя пустое
|
|
|
if nd == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrJobNameBlank,
|
|
|
fmt.Sprintf("задача '%s' имеет needs с пустым именем задачи", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
|
|
|
// Проверка максимальной длины
|
|
|
if len(nd) > MaxJobNameLength {
|
|
|
ndStartLine, ndStartCol := job.NeedLines[nd].Line, job.NeedLines[nd].Column
|
|
|
ndEndLine, ndEndCol := computeEndPosition(ndStartLine, ndStartCol, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrNeedNameTooLong,
|
|
|
fmt.Sprintf("задача '%s' содержит needs '%s', длина имени задачи превышает %d", job.Name, nd, MaxJobNameLength),
|
|
|
ndStartLine, ndStartCol, ndEndLine, ndEndCol)
|
|
|
}
|
|
|
|
|
|
// Проверяем, что задача из needs существует
|
|
|
needPos, posExists := job.NeedLines[nd]
|
|
|
if !posExists {
|
|
|
needPos = struct{ Line, Column int }{Line: job.Line, Column: job.Column}
|
|
|
}
|
|
|
if _, ok := cfg.Jobs[nd]; !ok {
|
|
|
ndEndLine, ndEndCol := computeEndPosition(needPos.Line, needPos.Column, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrUndefinedNeed,
|
|
|
fmt.Sprintf("задача '%s' (Line %d, Col %d-%d) ссылается на несуществующую задачу '%s'", job.Name, needPos.Line, needPos.Column, ndEndCol, nd),
|
|
|
needPos.Line, needPos.Column, ndEndLine, ndEndCol)
|
|
|
} else if !checkStageOrder(cfg, nd, job.Name) {
|
|
|
ndEndLine, ndEndCol := computeEndPosition(needPos.Line, needPos.Column, nd)
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidStageOrder,
|
|
|
fmt.Sprintf("задача '%s' (Line %d, Col %d) имеет need '%s' (Line %d, Col %d-%d) со стадией, идущей позже, чем '%s'",
|
|
|
job.Name, job.Line, job.Column, nd, needPos.Line, needPos.Column, ndEndCol, job.Name),
|
|
|
needPos.Line, needPos.Column, ndEndLine, ndEndCol)
|
|
|
}
|
|
|
|
|
|
dependencyGraph[job.Name] = append(dependencyGraph[job.Name], nd)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
validateJobArtifacts(job, ve)
|
|
|
validateJobDelayedParameters(job, ve)
|
|
|
validateJobDependenciesNeedsConsistency(job, ve)
|
|
|
validateJobRules(job, ve)
|
|
|
}
|
|
|
|
|
|
if visibleCount == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrNoVisibleJob,
|
|
|
"в разделе jobs не обнаружено ни одной «видимой» задачи",
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
|
|
|
cycle := detectCycles(dependencyGraph)
|
|
|
if len(cycle) > 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrCyclicDependency,
|
|
|
fmt.Sprintf("проверка циклических зависимостей: обнаружен цикл между job: %v", cycle),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobArtifacts(job *Job, ve *errors.ValidationError) {
|
|
|
if job.Artifacts != nil {
|
|
|
if len(job.Artifacts.Paths) == 0 {
|
|
|
ve.AddDetailWithSpan(errors.ErrArtifactsPathsBlank,
|
|
|
fmt.Sprintf("задача '%s': artifacts paths can't be blank", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func checkStageOrder(cfg *GitLabCIConfig, jobDep, jobName string) bool {
|
|
|
depStage := cfg.Jobs[jobDep].Stage
|
|
|
jobStage := cfg.Jobs[jobName].Stage
|
|
|
if depStage == "" || jobStage == "" {
|
|
|
return true
|
|
|
}
|
|
|
allStages := cfg.Stages
|
|
|
depIndex, jobIndex := -1, -1
|
|
|
for i, st := range allStages {
|
|
|
if st == depStage {
|
|
|
depIndex = i
|
|
|
}
|
|
|
if st == jobStage {
|
|
|
jobIndex = i
|
|
|
}
|
|
|
}
|
|
|
if depIndex == -1 || jobIndex == -1 {
|
|
|
return true
|
|
|
}
|
|
|
return depIndex <= jobIndex
|
|
|
}
|
|
|
|
|
|
func validateChanges(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Changes == nil {
|
|
|
return
|
|
|
}
|
|
|
switch v := cfg.Changes.(type) {
|
|
|
case []interface{}:
|
|
|
if len(v) > 50 {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0, 0, 0)
|
|
|
}
|
|
|
for _, item := range v {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
case map[string]interface{}:
|
|
|
paths, ok := v["paths"]
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesMissingPaths, "changes config hash must contain key 'paths'", 0, 0, 0, 0)
|
|
|
} else {
|
|
|
arr, ok := paths.([]interface{})
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0, 0, 0)
|
|
|
} else {
|
|
|
if len(arr) > 50 {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesTooManyEntries, "has too many entries (maximum 50)", 0, 0, 0, 0)
|
|
|
}
|
|
|
for _, item := range arr {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesNotArrayOfStrings, "changes config should be an array of strings", 0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
case string:
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0, 0, 0)
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrChangesInvalidType, "should be an array or a hash", 0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validatePaths(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Paths == nil {
|
|
|
return
|
|
|
}
|
|
|
arr, ok := cfg.Paths.([]interface{})
|
|
|
if !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0, 0, 0)
|
|
|
return
|
|
|
}
|
|
|
for _, item := range arr {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrPathsNotArrayOfStrings, "paths config should be an array of strings", 0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobDelayedParameters(job *Job, ve *errors.ValidationError) {
|
|
|
if job.When == "delayed" {
|
|
|
if job.StartIn == "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInMissing,
|
|
|
fmt.Sprintf("задача '%s': start_in should be specified for delayed job", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
} else {
|
|
|
d, err := time.ParseDuration(job.StartIn)
|
|
|
if err != nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInInvalid,
|
|
|
fmt.Sprintf("задача '%s': job start in should be a duration", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
} else if d > 7*24*time.Hour {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInTooLong,
|
|
|
fmt.Sprintf("задача '%s': job start in should not exceed the limit", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
if job.StartIn != "" {
|
|
|
ve.AddDetailWithSpan(errors.ErrStartInMustBeBlank,
|
|
|
fmt.Sprintf("задача '%s': job start in must be blank when not delayed", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobDependenciesNeedsConsistency(job *Job, ve *errors.ValidationError) {
|
|
|
if len(job.Dependencies) > 0 && len(job.Needs) > 0 {
|
|
|
needNames := make(map[string]struct{})
|
|
|
for _, nd := range job.Needs {
|
|
|
needNames[nd] = struct{}{}
|
|
|
}
|
|
|
for _, dep := range job.Dependencies {
|
|
|
if _, ok := needNames[dep]; !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrDependencyNotInNeeds,
|
|
|
fmt.Sprintf("задача '%s': dependency '%s' should be part of needs", job.Name, dep),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(dep))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateJobRules(job *Job, ve *errors.ValidationError) {
|
|
|
if job.Rules != nil {
|
|
|
if job.Only != nil && job.Except != nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrRulesOnlyExcept,
|
|
|
fmt.Sprintf("задача '%s': may not be used with rules: only, except", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
} else if job.Only != nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrRulesOnly,
|
|
|
fmt.Sprintf("задача '%s': may not be used with rules: only", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
} else if job.Except != nil {
|
|
|
ve.AddDetailWithSpan(errors.ErrRulesExcept,
|
|
|
fmt.Sprintf("задача '%s': may not be used with rules: except", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
if rulesMap, ok := job.Rules.(map[string]interface{}); ok {
|
|
|
if ifVal, exists := rulesMap["if"]; exists {
|
|
|
if ifStr, ok := ifVal.(string); ok {
|
|
|
if !validateExpressionSyntax(ifStr) {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidExpressionSyntax,
|
|
|
fmt.Sprintf("задача '%s': invalid expression syntax", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(ifStr))
|
|
|
}
|
|
|
} else {
|
|
|
ve.AddDetailWithSpan(errors.ErrInvalidExpressionSyntax,
|
|
|
fmt.Sprintf("задача '%s': 'if' in rules should be a string", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
}
|
|
|
if af, exists := rulesMap["allow_failure"]; exists {
|
|
|
if _, ok := af.(bool); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrBooleanValue,
|
|
|
fmt.Sprintf("задача '%s': allow_failure in rules should be a boolean value", job.Name),
|
|
|
job.Line, job.Column, job.Line, job.Column+1)
|
|
|
}
|
|
|
}
|
|
|
allowed := map[string]struct{}{
|
|
|
"if": {},
|
|
|
"when": {},
|
|
|
"start_in": {},
|
|
|
"allow_failure": {},
|
|
|
"changes": {},
|
|
|
"exists": {},
|
|
|
}
|
|
|
for key := range rulesMap {
|
|
|
if _, ok := allowed[key]; !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrUnknownRulesKey,
|
|
|
fmt.Sprintf("задача '%s': unknown key in rules: %s", job.Name, key),
|
|
|
job.Line, job.Column, job.Line, job.Column+len(key))
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateExpressionSyntax(expr string) bool {
|
|
|
return expr != ""
|
|
|
}
|
|
|
|
|
|
func detectCycles(dependencyGraph map[string][]string) []string {
|
|
|
visited := make(map[string]bool)
|
|
|
recStack := make(map[string]bool)
|
|
|
var cycle []string
|
|
|
|
|
|
var dfs func(string) bool
|
|
|
dfs = func(node string) bool {
|
|
|
if recStack[node] {
|
|
|
cycle = append(cycle, node)
|
|
|
return true
|
|
|
}
|
|
|
if visited[node] {
|
|
|
return false
|
|
|
}
|
|
|
visited[node] = true
|
|
|
recStack[node] = true
|
|
|
|
|
|
for _, neighbor := range dependencyGraph[node] {
|
|
|
if dfs(neighbor) {
|
|
|
cycle = append(cycle, node)
|
|
|
return true
|
|
|
}
|
|
|
}
|
|
|
recStack[node] = false
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
for node := range dependencyGraph {
|
|
|
if !visited[node] {
|
|
|
if dfs(node) {
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return cycle
|
|
|
}
|
|
|
|
|
|
func validateIncludeRules(cfg *GitLabCIConfig, ve *errors.ValidationError) {
|
|
|
if cfg.Include == nil {
|
|
|
return
|
|
|
}
|
|
|
switch v := cfg.Include.(type) {
|
|
|
case map[string]interface{}:
|
|
|
validateIncludeRuleMap(v, ve)
|
|
|
case []interface{}:
|
|
|
for _, item := range v {
|
|
|
if m, ok := item.(map[string]interface{}); ok {
|
|
|
validateIncludeRuleMap(m, ve)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func validateIncludeRuleMap(m map[string]interface{}, ve *errors.ValidationError) {
|
|
|
keysToCheck := []string{"exists", "changes"}
|
|
|
for _, key := range keysToCheck {
|
|
|
if val, exists := m[key]; exists {
|
|
|
switch val.(type) {
|
|
|
case string:
|
|
|
// ok
|
|
|
case []interface{}:
|
|
|
for _, item := range val.([]interface{}) {
|
|
|
if _, ok := item.(string); !ok {
|
|
|
ve.AddDetailWithSpan(errors.ErrIncludeRulesInvalid,
|
|
|
fmt.Sprintf("include rule key '%s' should be an array of strings", key),
|
|
|
0, 0, 0, 0)
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
default:
|
|
|
ve.AddDetailWithSpan(errors.ErrIncludeRulesInvalid,
|
|
|
fmt.Sprintf("include rule key '%s' should be a string or an array of strings", key),
|
|
|
0, 0, 0, 0)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|