124 lines
2.8 KiB
Go
124 lines
2.8 KiB
Go
package linter
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"git.k3nny.fr/gitlab-sim/internal/model"
|
|
)
|
|
|
|
func checkNeeds(p *model.Pipeline) []Finding {
|
|
var findings []Finding
|
|
|
|
// Build a stage-index map for ordering checks.
|
|
stageIndex := make(map[string]int, len(p.Stages))
|
|
for i, s := range p.Stages {
|
|
stageIndex[s] = i
|
|
}
|
|
|
|
// needsGraph maps each job to the list of jobs it depends on.
|
|
// Used for cycle detection after individual checks.
|
|
needsGraph := make(map[string][]string)
|
|
|
|
for name, job := range p.Jobs {
|
|
if len(job.Needs) == 0 {
|
|
continue
|
|
}
|
|
|
|
neededNames := parseNeedJobNames(job.Needs)
|
|
needsGraph[name] = neededNames
|
|
|
|
jobStageIdx, jobHasStage := stageIndex[job.Stage]
|
|
|
|
for _, needed := range neededNames {
|
|
neededJob, exists := p.Jobs[needed]
|
|
if !exists {
|
|
findings = append(findings, Finding{
|
|
Severity: Error,
|
|
Job: name,
|
|
Message: fmt.Sprintf("needs unknown job %q", needed),
|
|
})
|
|
continue
|
|
}
|
|
|
|
// A job cannot need a job in a later stage.
|
|
if len(p.Stages) > 0 && jobHasStage && neededJob.Stage != "" {
|
|
neededStageIdx, neededHasStage := stageIndex[neededJob.Stage]
|
|
if neededHasStage && neededStageIdx > jobStageIdx {
|
|
findings = append(findings, Finding{
|
|
Severity: Error,
|
|
Job: name,
|
|
Message: fmt.Sprintf(
|
|
"needs %q which is in a later stage (%q after %q)",
|
|
needed, neededJob.Stage, job.Stage,
|
|
),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
findings = append(findings, detectNeedsCycles(needsGraph)...)
|
|
return findings
|
|
}
|
|
|
|
// parseNeedJobNames extracts job names from a needs: list.
|
|
// Each element is either a plain string or a map with a "job" key.
|
|
// Cross-pipeline needs (maps with a "pipeline" key) are skipped.
|
|
func parseNeedJobNames(needs []any) []string {
|
|
var names []string
|
|
for _, n := range needs {
|
|
switch v := n.(type) {
|
|
case string:
|
|
names = append(names, v)
|
|
case map[string]any:
|
|
if _, crossPipeline := v["pipeline"]; crossPipeline {
|
|
continue
|
|
}
|
|
if job, ok := v["job"].(string); ok {
|
|
names = append(names, job)
|
|
}
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
func detectNeedsCycles(graph map[string][]string) []Finding {
|
|
const (
|
|
unvisited = 0
|
|
visiting = 1
|
|
visited = 2
|
|
)
|
|
|
|
state := make(map[string]int, len(graph))
|
|
var findings []Finding
|
|
reported := make(map[string]bool)
|
|
|
|
var visit func(name string, path []string)
|
|
visit = func(name string, path []string) {
|
|
switch state[name] {
|
|
case visited:
|
|
return
|
|
case visiting:
|
|
if !reported[name] {
|
|
reported[name] = true
|
|
findings = append(findings, Finding{
|
|
Severity: Error,
|
|
Job: name,
|
|
Message: fmt.Sprintf("circular dependency in needs: %v → %s", path, name),
|
|
})
|
|
}
|
|
return
|
|
}
|
|
state[name] = visiting
|
|
for _, dep := range graph[name] {
|
|
visit(dep, append(path, name))
|
|
}
|
|
state[name] = visited
|
|
}
|
|
|
|
for name := range graph {
|
|
visit(name, nil)
|
|
}
|
|
return findings
|
|
}
|