8dc30d9207
Add GL044 to validate jobs listed in rules:needs: overrides (GitLab CI 16.4+). rules:needs: lets a specific rule override the job's top-level needs: list; any referenced job must exist in the pipeline. Unknown jobs produce an error; optional: true entries produce a warning, matching the GL027 behaviour for top-level needs:. Cross-pipeline needs and skipped jobs (when a context is active) are excluded from checking. Implementation: - model.Rule gains a Needs []any field (yaml:"needs") - checkRulesNeeds(p, skipped) added to needs.go; wired into Lint - GL044 / RuleRulesNeedsUnknown added to rules.go and explain.go - 6 unit tests + 2 testdata fixtures; task validate updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
187 lines
5.1 KiB
Go
187 lines
5.1 KiB
Go
package linter
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.k3nny.fr/glint/internal/model"
|
|
)
|
|
|
|
// TestCheckNeeds_SkippedJob verifies that a skipped job's needs: violations are suppressed.
|
|
func TestCheckNeeds_SkippedJob(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Stages: []string{"build", "deploy"},
|
|
Jobs: map[string]model.Job{
|
|
"skipped-job": {
|
|
Name: "skipped-job",
|
|
Stage: "build",
|
|
Script: []any{"echo"},
|
|
Needs: []any{"nonexistent-job"},
|
|
},
|
|
},
|
|
}
|
|
// Without skipping: should report unknown needs.
|
|
withoutSkip := checkNeeds(p, nil)
|
|
if len(withoutSkip) == 0 {
|
|
t.Fatal("expected GL027 without skipped set, got none")
|
|
}
|
|
// With job skipped: no findings.
|
|
withSkip := checkNeeds(p, map[string]bool{"skipped-job": true})
|
|
if len(withSkip) != 0 {
|
|
t.Errorf("expected no findings for skipped job, got %v", withSkip)
|
|
}
|
|
}
|
|
|
|
// TestCheckRulesNeeds_UnknownJob verifies that an unknown job in rules:needs: produces GL044.
|
|
func TestCheckRulesNeeds_UnknownJob(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Stages: []string{"build", "test"},
|
|
Jobs: map[string]model.Job{
|
|
"build-job": {
|
|
Name: "build-job",
|
|
Stage: "build",
|
|
Script: []any{"make"},
|
|
},
|
|
"test-job": {
|
|
Name: "test-job",
|
|
Stage: "test",
|
|
Script: []any{"make test"},
|
|
Rules: []model.Rule{
|
|
{
|
|
If: `$CI_COMMIT_BRANCH == "main"`,
|
|
Needs: []any{"build-job", "nonexistent"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
findings := checkRulesNeeds(p, nil)
|
|
var got bool
|
|
for _, f := range findings {
|
|
if f.Rule == RuleRulesNeedsUnknown && f.Severity == Error {
|
|
got = true
|
|
}
|
|
}
|
|
if !got {
|
|
t.Errorf("expected GL044 error for unknown rules:needs: job; got %v", findings)
|
|
}
|
|
}
|
|
|
|
// TestCheckRulesNeeds_KnownJob verifies no finding when all rules:needs: jobs exist.
|
|
func TestCheckRulesNeeds_KnownJob(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Stages: []string{"build", "test"},
|
|
Jobs: map[string]model.Job{
|
|
"build-job": {Name: "build-job", Stage: "build", Script: []any{"make"}},
|
|
"test-job": {
|
|
Name: "test-job",
|
|
Stage: "test",
|
|
Script: []any{"make test"},
|
|
Rules: []model.Rule{{Needs: []any{"build-job"}}},
|
|
},
|
|
},
|
|
}
|
|
if findings := checkRulesNeeds(p, nil); len(findings) != 0 {
|
|
t.Errorf("expected no findings for valid rules:needs:; got %v", findings)
|
|
}
|
|
}
|
|
|
|
// TestCheckRulesNeeds_Optional verifies that optional: true downgrades to Warning.
|
|
func TestCheckRulesNeeds_Optional(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Jobs: map[string]model.Job{
|
|
"test-job": {
|
|
Name: "test-job",
|
|
Script: []any{"make test"},
|
|
Rules: []model.Rule{
|
|
{Needs: []any{map[string]any{"job": "ghost", "optional": true}}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
findings := checkRulesNeeds(p, nil)
|
|
if len(findings) != 1 || findings[0].Severity != Warning {
|
|
t.Errorf("optional unknown rules:needs: should produce Warning; got %v", findings)
|
|
}
|
|
}
|
|
|
|
// TestCheckRulesNeeds_CrossPipeline verifies that cross-pipeline needs are ignored.
|
|
func TestCheckRulesNeeds_CrossPipeline(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Jobs: map[string]model.Job{
|
|
"test-job": {
|
|
Name: "test-job",
|
|
Script: []any{"make test"},
|
|
Rules: []model.Rule{
|
|
{Needs: []any{map[string]any{"pipeline": "other", "job": "j"}}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if findings := checkRulesNeeds(p, nil); len(findings) != 0 {
|
|
t.Errorf("cross-pipeline rules:needs: should be ignored; got %v", findings)
|
|
}
|
|
}
|
|
|
|
// TestCheckRulesNeeds_SkippedJob verifies that a skipped job's rules:needs: violations are suppressed.
|
|
func TestCheckRulesNeeds_SkippedJob(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Jobs: map[string]model.Job{
|
|
"test-job": {
|
|
Name: "test-job",
|
|
Script: []any{"make test"},
|
|
Rules: []model.Rule{{Needs: []any{"nonexistent"}}},
|
|
},
|
|
},
|
|
}
|
|
if findings := checkRulesNeeds(p, map[string]bool{"test-job": true}); len(findings) != 0 {
|
|
t.Errorf("skipped job: expected no findings; got %v", findings)
|
|
}
|
|
}
|
|
|
|
// TestCheckRulesNeeds_NoNeeds verifies no findings when rules have no needs: override.
|
|
func TestCheckRulesNeeds_NoNeeds(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Jobs: map[string]model.Job{
|
|
"test-job": {
|
|
Name: "test-job",
|
|
Script: []any{"make test"},
|
|
Rules: []model.Rule{{When: "on_success"}},
|
|
},
|
|
},
|
|
}
|
|
if findings := checkRulesNeeds(p, nil); len(findings) != 0 {
|
|
t.Errorf("rules without needs: should produce no findings; got %v", findings)
|
|
}
|
|
}
|
|
|
|
// TestCheckNeeds_StageOrder verifies that a job needing a job in a later stage
|
|
// produces RuleNeedsStageOrder (line 64-76 in needs.go).
|
|
func TestCheckNeeds_StageOrder(t *testing.T) {
|
|
p := &model.Pipeline{
|
|
Stages: []string{"build", "test", "deploy"},
|
|
Jobs: map[string]model.Job{
|
|
"build-job": {
|
|
Name: "build-job",
|
|
Stage: "build",
|
|
Script: []any{"make"},
|
|
Needs: []any{"deploy-job"},
|
|
},
|
|
"deploy-job": {
|
|
Name: "deploy-job",
|
|
Stage: "deploy",
|
|
Script: []any{"make deploy"},
|
|
},
|
|
},
|
|
}
|
|
findings := checkNeeds(p, nil)
|
|
var gotStageOrder bool
|
|
for _, f := range findings {
|
|
if f.Rule == RuleNeedsStageOrder {
|
|
gotStageOrder = true
|
|
}
|
|
}
|
|
if !gotStageOrder {
|
|
t.Errorf("build-job needing deploy-job (later stage): expected GL025 finding; got: %v", findings)
|
|
}
|
|
}
|