feat(linter): add GL046-GL049 rules, AND-group support, and GL032 fix
ci / vet, staticcheck, test, build (push) Successful in 8m44s
release / Build and publish release (push) Successful in 9m52s

- 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:
2026-07-30 22:44:22 +02:00
co-authored by Claude Sonnet 4.6
parent 3eea496b9f
commit 986162d270
16 changed files with 497 additions and 28 deletions
+95
View File
@@ -114,6 +114,8 @@ func checkJobKeywords(name string, job model.Job) []Finding {
findings = append(findings, checkSecrets(name, job)...)
findings = append(findings, checkPagesKeyword(name, job)...)
findings = append(findings, checkCacheKeyFiles(name, job)...)
findings = append(findings, checkPullPolicy(name, job)...)
findings = append(findings, checkRulesAllowFailure(name, job)...)
return findings
}
@@ -313,6 +315,20 @@ func checkTrigger(name string, job model.Job) []Finding {
Message: "'trigger' map must specify 'project' or 'include'",
})
}
// GL048: validate trigger.forward keys.
if fwd, ok := m["forward"].(map[string]any); ok {
validForwardKeys := map[string]bool{"pipeline_variables": true, "yaml_variables": true}
for k := range fwd {
if !validForwardKeys[k] {
findings = append(findings, Finding{
Severity: Error,
Rule: RuleInvalidTriggerForward,
Job: name,
Message: fmt.Sprintf("'trigger.forward' has unrecognised key %q; valid keys: pipeline_variables, yaml_variables", k),
})
}
}
}
}
return findings
}
@@ -793,6 +809,85 @@ func checkPagesKeyword(name string, job model.Job) []Finding {
}}
}
// GL046: image.pull_policy and services[n].pull_policy must use recognised values.
var validPullPolicy = map[string]bool{
"always": true, "if-not-present": true, "never": true,
}
func checkPullPolicy(name string, job model.Job) []Finding {
var findings []Finding
findings = append(findings, checkPullPolicyValue(name, "image", job.Image)...)
for i, svc := range job.Services {
findings = append(findings, checkPullPolicyValue(name, fmt.Sprintf("services[%d]", i), svc)...)
}
return findings
}
func checkPullPolicyValue(jobName, field string, v any) []Finding {
m, ok := v.(map[string]any)
if !ok {
return nil
}
pp, exists := m["pull_policy"]
if !exists || pp == nil {
return nil
}
var policies []string
switch x := pp.(type) {
case string:
policies = []string{x}
case []any:
for _, item := range x {
if s, ok := item.(string); ok {
policies = append(policies, s)
}
}
}
var findings []Finding
for _, p := range policies {
if !validPullPolicy[p] {
findings = append(findings, Finding{
Severity: Error,
Rule: RuleInvalidPullPolicy,
Job: jobName,
Message: fmt.Sprintf("%s.pull_policy has unrecognised value %q; valid: always, if-not-present, never", field, p),
})
}
}
return findings
}
// GL049: rules[n].allow_failure must be a boolean or a map with exit_codes:.
func checkRulesAllowFailure(name string, job model.Job) []Finding {
var findings []Finding
for i, rule := range job.Rules {
if rule.AllowFailure == nil {
continue
}
switch v := rule.AllowFailure.(type) {
case bool:
// valid
case map[string]any:
if _, ok := v["exit_codes"]; !ok {
findings = append(findings, Finding{
Severity: Error,
Rule: RuleInvalidRulesAllowFailure,
Job: name,
Message: fmt.Sprintf("rules[%d].allow_failure map form must contain 'exit_codes'", i),
})
}
default:
findings = append(findings, Finding{
Severity: Error,
Rule: RuleInvalidRulesAllowFailure,
Job: name,
Message: fmt.Sprintf("rules[%d].allow_failure must be a boolean or a map with 'exit_codes'", i),
})
}
}
return findings
}
// GL041: cache.key.files must be a list of exact file paths, not glob patterns.
func checkCacheKeyFiles(name string, job model.Job) []Finding {
if job.Cache == nil {