2024-08-01 22:43:18 +00:00
|
|
|
package workflow
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2024-08-21 03:35:49 +00:00
|
|
|
"maps"
|
2024-08-01 22:43:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Job struct {
|
2024-08-09 04:17:29 +00:00
|
|
|
RunsOn string `yaml:"runs-on"`
|
|
|
|
Steps []Step `yaml:"steps"`
|
|
|
|
Needs []string `yaml:"needs"`
|
2024-08-09 04:31:06 +00:00
|
|
|
// Job name; this isn't guaranteed to be unique.
|
2024-08-20 03:35:33 +00:00
|
|
|
Name string `yaml:"name"`
|
|
|
|
// If truthy, job-level failure does not bubble up to the workflow.
|
2024-08-21 03:35:49 +00:00
|
|
|
ContinueOnError bool `yaml:"continue-on-error"`
|
|
|
|
Env map[string]string `yaml:"env"`
|
2024-08-20 03:35:33 +00:00
|
|
|
Defaults struct {
|
2024-08-12 04:34:17 +00:00
|
|
|
Run struct {
|
|
|
|
WorkingDirectory string `yaml:"working-directory"`
|
2024-08-23 03:38:32 +00:00
|
|
|
Shell string `yaml:"shell"`
|
2024-08-12 04:34:17 +00:00
|
|
|
} `yaml:"run"`
|
|
|
|
} `yaml:"defaults"`
|
2024-08-01 22:43:18 +00:00
|
|
|
}
|
|
|
|
|
2024-08-31 03:09:31 +00:00
|
|
|
func (j Job) Walk(handler func(node interface{})) {
|
|
|
|
handler(j)
|
|
|
|
|
|
|
|
for _, step := range j.Steps {
|
|
|
|
step.Walk(handler)
|
|
|
|
}
|
|
|
|
}
|
2024-08-01 22:43:18 +00:00
|
|
|
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 {
|
2024-08-21 03:35:49 +00:00
|
|
|
Run string `yaml:"run"`
|
|
|
|
WorkingDirectory string `yaml:"working-directory"`
|
|
|
|
ContinueOnError bool `yaml:"continue-on-error"`
|
|
|
|
Env map[string]string `yaml:"env"`
|
2024-08-23 03:38:32 +00:00
|
|
|
Shell string `yaml:"shell"`
|
2024-09-03 00:10:25 +00:00
|
|
|
Uses string `yaml:"uses"`
|
2024-09-02 16:28:23 +00:00
|
|
|
With map[string]string `yaml:"with"`
|
2024-08-31 03:09:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s Step) Walk(handler func(node interface{})) {
|
|
|
|
handler(s)
|
2024-08-01 22:43:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s Step) Validate() []error {
|
|
|
|
validationErrors := []error{}
|
|
|
|
|
2024-09-03 00:10:25 +00:00
|
|
|
if s.Run == "" && s.Uses == "" {
|
|
|
|
validationErrors = append(validationErrors, errors.New("Must have a \"run\" or \"uses\" clause."))
|
2024-08-01 22:43:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return validationErrors
|
|
|
|
}
|
|
|
|
|
|
|
|
type Workflow struct {
|
|
|
|
SourcePath string
|
2024-08-21 03:35:49 +00:00
|
|
|
Jobs map[string]Job `yaml:"jobs"`
|
|
|
|
Env map[string]string `yaml:"env"`
|
2024-08-01 22:43:18 +00:00
|
|
|
}
|
|
|
|
|
2024-08-31 03:09:31 +00:00
|
|
|
func (w Workflow) Walk(handler func(node interface{})) {
|
|
|
|
handler(w)
|
|
|
|
|
|
|
|
for _, job := range w.Jobs {
|
|
|
|
job.Walk(handler)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-12 04:34:17 +00:00
|
|
|
// 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 "."
|
|
|
|
}
|
|
|
|
|
2024-08-21 03:35:49 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-08-23 03:38:32 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-08-01 22:43:18 +00:00
|
|
|
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
|
|
|
|
}
|
2024-08-09 04:17:29 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|