courgette/internal/workflow/models.go

188 lines
4.6 KiB
Go

package workflow
import (
"errors"
"maps"
)
type Job struct {
RunsOn string `yaml:"runs-on"`
Steps []Step `yaml:"steps"`
Needs []string `yaml:"needs"`
// Job name; this isn't guaranteed to be unique.
Name string `yaml:"name"`
// If truthy, job-level failure does not bubble up to the workflow.
ContinueOnError bool `yaml:"continue-on-error"`
Env map[string]string `yaml:"env"`
Defaults struct {
Run struct {
WorkingDirectory string `yaml:"working-directory"`
Shell string `yaml:"shell"`
} `yaml:"run"`
} `yaml:"defaults"`
}
func (j Job) Walk(handler func(node interface{})) {
handler(j)
for _, step := range j.Steps {
step.Walk(handler)
}
}
func (j Job) Validate() []error {
validationErrors := []error{}
if j.RunsOn == "" {
validationErrors = append(validationErrors, errors.New("Missing \"runs-on\" field on job."))
}
if len(j.Steps) == 0 {
validationErrors = append(validationErrors, errors.New("Missing \"steps\" field on job."))
}
for _, step := range j.Steps {
validationErrors = append(validationErrors, step.Validate()...)
}
return validationErrors
}
type Step struct {
Run string `yaml:"run"`
WorkingDirectory string `yaml:"working-directory"`
ContinueOnError bool `yaml:"continue-on-error"`
Env map[string]string `yaml:"env"`
Shell string `yaml:"shell"`
Use string `yaml:"use"`
}
func (s Step) Walk(handler func(node interface{})) {
handler(s)
}
func (s Step) Validate() []error {
validationErrors := []error{}
if s.Run == "" && s.Use == "" {
validationErrors = append(validationErrors, errors.New("Must have a \"run\" or \"step\" clause."))
}
return validationErrors
}
type Workflow struct {
SourcePath string
Jobs map[string]Job `yaml:"jobs"`
Env map[string]string `yaml:"env"`
}
func (w Workflow) Walk(handler func(node interface{})) {
handler(w)
for _, job := range w.Jobs {
job.Walk(handler)
}
}
// Returns the given workflow's job+step working directory inside the container
// that runs the job.
//
// Values considered, in order:
// - Step working-directory;
// - Job default run working-directory
func (w Workflow) GetWorkingDirectory(jobName string, stepIndex int) string {
jobDefinition := w.Jobs[jobName]
stepDefinition := jobDefinition.Steps[stepIndex]
if stepDefinition.WorkingDirectory != "" {
return stepDefinition.WorkingDirectory
}
if jobDefinition.Defaults.Run.WorkingDirectory != "" {
return jobDefinition.Defaults.Run.WorkingDirectory
}
return "."
}
// Returns the merged map of environment variants defined by:
// - Workflow-level "env"
// - Job-level "env"
// - Step-level "env"
// The environment is merged in order, and name collisions overwrite
// previous values such that jobs can overwrite workflows, and steps, jobs.
func (w Workflow) GetEnv(jobName string, stepIndex int) map[string]string {
finalEnv := map[string]string{}
maps.Copy(finalEnv, w.Env)
maps.Copy(finalEnv, w.Jobs[jobName].Env)
maps.Copy(finalEnv, w.Jobs[jobName].Steps[stepIndex].Env)
return finalEnv
}
// Returns the shell defined for the given job's step.
// Overriding values are considered in the order: step, job, global.
//
// If the shell is undefined in all cases, "bash" is used.
func (w Workflow) GetShell(jobName string, stepIndex int) string {
var shell string
if stepShell := w.Jobs[jobName].Steps[stepIndex].Shell; stepShell != "" {
shell = stepShell
} else if jobShell := w.Jobs[jobName].Defaults.Run.Shell; jobShell != "" {
shell = jobShell
}
if shell == "" {
return "bash"
}
return shell
}
func (w Workflow) Validate() []error {
validationErrors := []error{}
if len(w.Jobs) == 0 {
validationErrors = append(validationErrors, errors.New("Missing \"jobs\" field on workflow."))
}
for _, job := range w.Jobs {
validationErrors = append(validationErrors, job.Validate()...)
}
return validationErrors
}
// Creates a deterministic, ordered collection of jobs that respects
// the jobs's dependencies.
func (w Workflow) GetJobsAsGroups() [][]Job {
dependenciesMap := map[string][]string{}
for jobLabel, job := range w.Jobs {
if len(job.Needs) == 0 {
dependenciesMap[""] = append(dependenciesMap[""], jobLabel)
}
for _, need := range job.Needs {
dependenciesMap[need] = append(dependenciesMap[need], jobLabel)
}
}
levels := SplitFlatTreeIntoGroups(dependenciesMap)
groups := [][]Job{}
for _, jobLabels := range levels {
jobs := []Job{}
for _, jobLabel := range jobLabels {
jobs = append(jobs, w.Jobs[jobLabel])
}
groups = append(groups, jobs)
}
return groups
}