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"` Uses string `yaml:"uses"` With map[string]string `yaml:"with"` } func (s Step) Walk(handler func(node interface{})) { handler(s) } func (s Step) Validate() []error { validationErrors := []error{} if s.Run == "" && s.Uses == "" { validationErrors = append(validationErrors, errors.New("Must have a \"run\" or \"uses\" 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 }