2026-06-05 01:29:07 +02:00
|
|
|
|
package linter
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
2026-06-10 22:40:42 +02:00
|
|
|
|
"git.k3nny.fr/glint/internal/model"
|
2026-06-05 01:29:07 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var validJobWhen = map[string]bool{
|
|
|
|
|
|
"on_success": true,
|
|
|
|
|
|
"on_failure": true,
|
|
|
|
|
|
"always": true,
|
|
|
|
|
|
"manual": true,
|
|
|
|
|
|
"delayed": true,
|
|
|
|
|
|
"never": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var validRuleWhen = map[string]bool{
|
|
|
|
|
|
"on_success": true,
|
|
|
|
|
|
"on_failure": true,
|
|
|
|
|
|
"always": true,
|
|
|
|
|
|
"manual": true,
|
|
|
|
|
|
"delayed": true,
|
|
|
|
|
|
"never": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var validWorkflowRuleWhen = map[string]bool{
|
|
|
|
|
|
"always": true,
|
|
|
|
|
|
"never": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var validArtifactsWhen = map[string]bool{
|
|
|
|
|
|
"on_success": true,
|
|
|
|
|
|
"on_failure": true,
|
|
|
|
|
|
"always": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var validCacheWhen = map[string]bool{
|
|
|
|
|
|
"on_success": true,
|
|
|
|
|
|
"on_failure": true,
|
|
|
|
|
|
"always": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var validCachePolicy = map[string]bool{
|
|
|
|
|
|
"pull": true,
|
|
|
|
|
|
"push": true,
|
|
|
|
|
|
"pull-push": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var validRetryWhen = map[string]bool{
|
|
|
|
|
|
"always": true,
|
|
|
|
|
|
"unknown_failure": true,
|
|
|
|
|
|
"script_failure": true,
|
|
|
|
|
|
"api_failure": true,
|
|
|
|
|
|
"stuck_or_timeout_failure": true,
|
|
|
|
|
|
"runner_system_failure": true,
|
|
|
|
|
|
"runner_unsupported": true,
|
|
|
|
|
|
"stale_schedule": true,
|
|
|
|
|
|
"job_execution_timeout": true,
|
|
|
|
|
|
"archived_failure": true,
|
|
|
|
|
|
"unmet_prerequisites": true,
|
|
|
|
|
|
"scheduler_failure": true,
|
|
|
|
|
|
"data_integrity_failure": true,
|
|
|
|
|
|
"forward_deployment_failure": true,
|
|
|
|
|
|
"insufficient_bridge_permissions": true,
|
|
|
|
|
|
"downstream_bridge_project_not_found": true,
|
|
|
|
|
|
"invalid_bridge_trigger": true,
|
|
|
|
|
|
"upstream_bridge_project_not_found": true,
|
|
|
|
|
|
"protected_environment_failure": true,
|
|
|
|
|
|
"pipeline_loop_detected": true,
|
|
|
|
|
|
"excluded_by_rules": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var validEnvironmentAction = map[string]bool{
|
|
|
|
|
|
"start": true,
|
|
|
|
|
|
"stop": true,
|
|
|
|
|
|
"prepare": true,
|
|
|
|
|
|
"verify": true,
|
|
|
|
|
|
"access": true,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkJobKeywords(name string, job model.Job) []Finding {
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
findings = append(findings, checkWhen(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkParallel(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkRetry(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkAllowFailure(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkInterruptible(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkTrigger(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkCoverage(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkRelease(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkEnvironment(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkArtifacts(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkCache(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkRules(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkImage(name, job)...)
|
|
|
|
|
|
findings = append(findings, checkInherit(name, job)...)
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkWhen(name string, job model.Job) []Finding {
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
if job.When != "" && !validJobWhen[job.When] {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'when' has invalid value %q; valid: on_success, on_failure, always, manual, delayed, never", job.When),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if job.When == "delayed" && job.StartIn == "" {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'when: delayed' requires 'start_in' (e.g. 'start_in: 30 minutes')",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if job.When != "delayed" && job.StartIn != "" {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'start_in' is only valid when 'when: delayed'",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkParallel(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Parallel == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
switch v := job.Parallel.(type) {
|
|
|
|
|
|
case int:
|
|
|
|
|
|
if v < 2 || v > 200 {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'parallel' must be between 2 and 200, got %d", v),
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
case map[string]any:
|
|
|
|
|
|
if _, ok := v["matrix"]; !ok {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'parallel' map form must have a 'matrix' key",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'parallel' must be an integer (2–200) or a map with 'matrix'",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkRetry(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Retry == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
switch v := job.Retry.(type) {
|
|
|
|
|
|
case int:
|
|
|
|
|
|
if v < 0 || v > 2 {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'retry' must be 0, 1, or 2; got %d", v),
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
case map[string]any:
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
if maxVal, ok := v["max"]; ok {
|
|
|
|
|
|
if n, ok := maxVal.(int); ok && (n < 0 || n > 2) {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'retry.max' must be 0, 1, or 2; got %d", n),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if whenVal, ok := v["when"]; ok {
|
|
|
|
|
|
findings = append(findings, validateRetryWhen(name, whenVal)...)
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
default:
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'retry' must be an integer (0–2) or a map with 'max'/'when'",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func validateRetryWhen(name string, val any) []Finding {
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
check := func(s string) {
|
|
|
|
|
|
if !validRetryWhen[s] {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'retry.when' has invalid value %q", s),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
switch v := val.(type) {
|
|
|
|
|
|
case string:
|
|
|
|
|
|
check(v)
|
|
|
|
|
|
case []any:
|
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
|
if s, ok := item.(string); ok {
|
|
|
|
|
|
check(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkAllowFailure(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Allow == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
switch v := job.Allow.(type) {
|
|
|
|
|
|
case bool:
|
|
|
|
|
|
// valid
|
|
|
|
|
|
case map[string]any:
|
|
|
|
|
|
if _, ok := v["exit_codes"]; !ok {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'allow_failure' map form must contain 'exit_codes'",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
|
|
|
_ = v
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'allow_failure' must be a boolean or a map with 'exit_codes'",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkInterruptible(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Interruptible == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, ok := job.Interruptible.(bool); !ok {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'interruptible' must be a boolean",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkTrigger(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Trigger == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
if len(job.Script) > 0 {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "jobs with 'trigger' cannot use 'script'",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if m, ok := job.Trigger.(map[string]any); ok {
|
|
|
|
|
|
_, hasProject := m["project"]
|
|
|
|
|
|
_, hasInclude := m["include"]
|
|
|
|
|
|
if !hasProject && !hasInclude {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'trigger' map must specify 'project' or 'include'",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var coveragePattern = regexp.MustCompile(`^/.*/$`)
|
|
|
|
|
|
|
|
|
|
|
|
func checkCoverage(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Coverage == "" {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if !coveragePattern.MatchString(job.Coverage) {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'coverage' must be a regex pattern wrapped in '/' (e.g. '/\\d+\\.?\\d*%%/'), got %q", job.Coverage),
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkRelease(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Release == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
m, ok := job.Release.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'release' must be a map",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
tagName, exists := m["tag_name"]
|
|
|
|
|
|
if !exists || tagName == "" || tagName == nil {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'release' requires 'tag_name'",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkEnvironment(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Environment == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
m, ok := job.Environment.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
// String form: just the environment name — valid.
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
envName, _ := m["name"]
|
|
|
|
|
|
_, hasURL := m["url"]
|
|
|
|
|
|
if (envName == nil || envName == "") && hasURL {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'environment.url' requires 'environment.name' to be set",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if action, ok := m["action"].(string); ok && !validEnvironmentAction[action] {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'environment.action' has invalid value %q; valid: start, stop, prepare, verify, access", action),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkArtifacts(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Artifacts == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
m, ok := job.Artifacts.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
if w, ok := m["when"].(string); ok && !validArtifactsWhen[w] {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'artifacts.when' has invalid value %q; valid: on_success, on_failure, always", w),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, hasExposeAs := m["expose_as"]; hasExposeAs {
|
|
|
|
|
|
paths, _ := m["paths"]
|
|
|
|
|
|
if paths == nil {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'artifacts.expose_as' requires 'artifacts.paths'",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Pages job should publish to public/
|
|
|
|
|
|
if name == "pages" {
|
|
|
|
|
|
if paths, ok := m["paths"].([]any); ok {
|
|
|
|
|
|
found := false
|
|
|
|
|
|
for _, p := range paths {
|
|
|
|
|
|
if s, ok := p.(string); ok && (s == "public" || strings.HasPrefix(s, "public/")) {
|
|
|
|
|
|
found = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !found {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Warning,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "the 'pages' job should include 'public' in 'artifacts.paths' for GitLab Pages to deploy",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkCache(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Cache == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
var maps []map[string]any
|
|
|
|
|
|
switch v := job.Cache.(type) {
|
|
|
|
|
|
case map[string]any:
|
|
|
|
|
|
maps = []map[string]any{v}
|
|
|
|
|
|
case []any:
|
|
|
|
|
|
for _, item := range v {
|
|
|
|
|
|
if m, ok := item.(map[string]any); ok {
|
|
|
|
|
|
maps = append(maps, m)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
for _, m := range maps {
|
|
|
|
|
|
if w, ok := m["when"].(string); ok && !validCacheWhen[w] {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'cache.when' has invalid value %q; valid: on_success, on_failure, always", w),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if p, ok := m["policy"].(string); ok && !validCachePolicy[p] {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'cache.policy' has invalid value %q; valid: pull, push, pull-push", p),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkRules(name string, job model.Job) []Finding {
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
for i, rule := range job.Rules {
|
|
|
|
|
|
if rule.When != "" && !validRuleWhen[rule.When] {
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("rules[%d].when has invalid value %q; valid: on_success, on_failure, always, manual, delayed, never", i, rule.When),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkImage(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Image == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
m, ok := job.Image.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil // String form is valid.
|
|
|
|
|
|
}
|
|
|
|
|
|
imgName, _ := m["name"]
|
|
|
|
|
|
if imgName == nil || imgName == "" {
|
|
|
|
|
|
return []Finding{{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: "'image' map form requires a 'name' key",
|
|
|
|
|
|
}}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func checkInherit(name string, job model.Job) []Finding {
|
|
|
|
|
|
if job.Inherit == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
m, ok := job.Inherit.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
var findings []Finding
|
|
|
|
|
|
for _, key := range []string{"default", "variables"} {
|
|
|
|
|
|
val, exists := m[key]
|
|
|
|
|
|
if !exists {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
switch val.(type) {
|
|
|
|
|
|
case bool, []any:
|
|
|
|
|
|
// valid: false/true or list of names
|
|
|
|
|
|
default:
|
|
|
|
|
|
findings = append(findings, Finding{
|
|
|
|
|
|
Severity: Error,
|
|
|
|
|
|
Job: name,
|
|
|
|
|
|
Message: fmt.Sprintf("'inherit.%s' must be a boolean or a list of names", key),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return findings
|
|
|
|
|
|
}
|