feat(linter): add GL046-GL049 rules, AND-group support, and GL032 fix
- GL046: validate image/service pull_policy values (always, if-not-present, never)
- GL047: error when a variables.options default value is not in the options list
- GL048: error on unrecognised trigger.forward keys
- GL049: validate rules[n].allow_failure (bool or {exit_codes:} map)
- Parse and evaluate workflow.rules/job.rules nested-array AND-groups; crash
on !!seq nodes is fixed; all members of a group must match for it to fire
- Add workflow.name and workflow.auto_cancel fields to Workflow struct
- Fix GL032 false positive: variables declared in any workflow rule's variables:
block no longer trigger an undeclared-variable warning in sibling workflow
rule if: expressions
- Add Windows ARM64 release build target (task build-windows-arm64)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ func Lint(p *model.Pipeline, skipped map[string]bool) []Finding {
|
||||
findings = append(findings, checkDuplicateStages(p)...)
|
||||
findings = append(findings, checkDefault(p)...)
|
||||
findings = append(findings, checkWorkflow(p)...)
|
||||
findings = append(findings, checkPipelineVariableOptions(p)...)
|
||||
findings = append(findings, checkJobs(p)...)
|
||||
findings = append(findings, checkNeeds(p, skipped)...)
|
||||
findings = append(findings, checkRulesNeeds(p, skipped)...)
|
||||
@@ -223,6 +224,7 @@ func checkJob(name string, job model.Job, stageSet map[string]bool) []Finding {
|
||||
}
|
||||
|
||||
findings = append(findings, checkJobKeywords(name, job)...)
|
||||
findings = append(findings, checkVariableOptionsForJob(name, job)...)
|
||||
|
||||
// Attach source location to every job-scoped finding collected above.
|
||||
for i := range findings {
|
||||
@@ -235,6 +237,55 @@ func checkJob(name string, job model.Job, stageSet map[string]bool) []Finding {
|
||||
return findings
|
||||
}
|
||||
|
||||
// GL047: variable declared with options: must have its default value in the options list.
|
||||
|
||||
func checkPipelineVariableOptions(p *model.Pipeline) []Finding {
|
||||
return checkVariableOptionsMap(p.Variables, "", p.SourceFile, 0, 0)
|
||||
}
|
||||
|
||||
func checkVariableOptionsForJob(name string, job model.Job) []Finding {
|
||||
return checkVariableOptionsMap(job.Variables, name, job.File, job.Line, job.Column)
|
||||
}
|
||||
|
||||
func checkVariableOptionsMap(vars map[string]any, jobName, file string, line, col int) []Finding {
|
||||
var findings []Finding
|
||||
for varName, v := range vars {
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
rawOpts, hasOpts := m["options"]
|
||||
rawVal, hasVal := m["value"]
|
||||
if !hasOpts || !hasVal || rawVal == nil {
|
||||
continue
|
||||
}
|
||||
opts, ok := rawOpts.([]any)
|
||||
if !ok || len(opts) == 0 {
|
||||
continue
|
||||
}
|
||||
val := fmt.Sprint(rawVal)
|
||||
inOptions := false
|
||||
for _, opt := range opts {
|
||||
if fmt.Sprint(opt) == val {
|
||||
inOptions = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inOptions {
|
||||
findings = append(findings, Finding{
|
||||
Severity: Error,
|
||||
Rule: RuleVariableValueNotInOptions,
|
||||
Job: jobName,
|
||||
File: file,
|
||||
Line: line,
|
||||
Column: col,
|
||||
Message: fmt.Sprintf("variable %q: default value %q is not listed in 'options'", varName, val),
|
||||
})
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
// scriptNonEmpty reports whether a script/before_script/after_script field
|
||||
// (which may be a []any list or a plain string) is non-empty.
|
||||
func scriptNonEmpty(v any) bool {
|
||||
|
||||
Reference in New Issue
Block a user