diff --git a/internal/actions/act.go b/internal/actions/act.go new file mode 100644 index 0000000..cd90fbb --- /dev/null +++ b/internal/actions/act.go @@ -0,0 +1,54 @@ +// Implementation for basic Action-related operations. +package actions + +import ( + logger "courgette/internal/logging" + "os" + "os/exec" + "strings" +) + +type CliClient interface { + Clone(url string, destination string) error + Exec(args ...string) error +} + +type GitClient struct{} + +func (g GitClient) Exec(args ...string) error { + cmd := exec.Command("git", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +func (g GitClient) Clone(url string, destination string) error { + return g.Exec("clone", url, destination, "--depth", "1") +} + +// Returns a key that can be used as a directory / cache-key string +// based on the provided Action repository url. +func GetActionKey(url string) string { + url = strings.ToLower(url) + + if strings.HasPrefix(url, "http://") { + url = strings.TrimPrefix(url, "http://") + } else if strings.HasPrefix(url, "https://") { + url = strings.TrimPrefix(url, "https://") + } + + parts := strings.Split(url, "/") + + return strings.Join(parts, "__") +} + +// Prefetches and caches the action defined by at . +// +// is expected to be the full url of the repository where +// the action lives. +func PrefetchAction(client CliClient, actionName string, destination string) error { + logger.Info("Prefetching action: %s to %s", actionName, destination) + + return client.Clone(actionName, destination) +} diff --git a/internal/cache/cache_manager.go b/internal/cache/cache_manager.go index d767231..379b9bc 100644 --- a/internal/cache/cache_manager.go +++ b/internal/cache/cache_manager.go @@ -30,14 +30,14 @@ func (c CacheManager) Path(elems ...string) string { // Checks whether a specific top-level items exists in the cache. func (c CacheManager) Exists(key string) bool { - if _, err := os.Stat(c.Path(key)); errors.Is(err, os.ErrNotExist) { + if _, err := os.Stat(c.Path(key)); errors.Is(err, os.ErrNotExist) { return false } - return true + return true } // Deletes an element from the cache, if it exists. func (c CacheManager) Evict(key string) { - os.RemoveAll(c.Path(key)) + os.RemoveAll(c.Path(key)) } diff --git a/internal/commands/execute_workflow.go b/internal/commands/execute_workflow.go index 0929bf1..3778ccc 100644 --- a/internal/commands/execute_workflow.go +++ b/internal/commands/execute_workflow.go @@ -19,7 +19,7 @@ func ExecuteWorkflow(configuration Configuration, workflowFile string) error { runnerInstance := runner.NewRunner( driver, configuration.Runner.Labels, - configuration.Cache.Dir, + configuration.GetCacheDir(), ) workflow, err := workflow.FromYamlFile(workflowFile) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index fb43a8e..50b195a 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -2,10 +2,11 @@ package runner import ( "context" + actions "courgette/internal/actions" cache "courgette/internal/cache" driver "courgette/internal/driver" logger "courgette/internal/logging" - workflow "courgette/internal/workflow" + workflows "courgette/internal/workflow" "errors" "fmt" "sync" @@ -36,7 +37,7 @@ func NewRunner(driver driver.ContainerDriver, labels map[string]string, cacheRoo // This is the high-level call that will set up the container // that the jobs will be executed in, run the jobs's steps and // tear down the container once no longer useful. -func (r *Runner) RunWorkflow(workflow workflow.Workflow) TaskTracker { +func (r *Runner) RunWorkflow(workflow workflows.Workflow) TaskTracker { logger.Info("Executing workflow: %s", workflow.SourcePath) rootTracker := NewTaskTracker(nil) @@ -44,6 +45,15 @@ func (r *Runner) RunWorkflow(workflow workflow.Workflow) TaskTracker { defer r.deferred.RunAllDeferredTasks() + workflow.Walk(func(n interface{}) { + switch n.(type) { + case workflows.Step: + if useAction := n.(workflows.Step).Use; useAction != "" { + actions.PrefetchAction(actions.GitClient{}, useAction, r.Cache.Path(actions.GetActionKey(useAction))) + } + } + }) + for _, group := range workflow.GetJobsAsGroups() { var groupWait sync.WaitGroup @@ -72,7 +82,7 @@ func (r *Runner) RunWorkflow(workflow workflow.Workflow) TaskTracker { // a WaitGroup to coordinate concurrent tasks) and updates the tracker with results. func (r Runner) runJob(jobContext context.Context, jobTracker *TaskTracker, jobWaitGroup *sync.WaitGroup) { - job := jobContext.Value("currentJob").(workflow.Job) + job := jobContext.Value("currentJob").(workflows.Job) containerName := fmt.Sprintf("runner-%s", jobTracker.TaskId) defer jobWaitGroup.Done() @@ -134,11 +144,11 @@ func (r *Runner) RunJobInContainer(imageUri string, containerId string, jobConte r.Driver.Stop(containerId) }) - job := jobContext.Value("currentJob").(workflow.Job) + job := jobContext.Value("currentJob").(workflows.Job) logger.Info("Started %s", containerId) for stepIndex, step := range job.Steps { - workflow := jobContext.Value("workflow").(workflow.Workflow) + workflow := jobContext.Value("workflow").(workflows.Workflow) stepCwd := workflow.GetWorkingDirectory(job.Name, stepIndex) stepEnv := workflow.GetEnv(job.Name, stepIndex) stepShell := workflow.GetShell(job.Name, stepIndex) diff --git a/internal/workflow/models.go b/internal/workflow/models.go index 9e62cc5..f474ee9 100644 --- a/internal/workflow/models.go +++ b/internal/workflow/models.go @@ -22,6 +22,13 @@ type Job struct { } `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{} @@ -46,13 +53,18 @@ type Step struct { 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 == "" { - validationErrors = append(validationErrors, errors.New("Missing \"run\" field on step.")) + if s.Run == "" && s.Use == "" { + validationErrors = append(validationErrors, errors.New("Must have a \"run\" or \"step\" clause.")) } return validationErrors @@ -64,6 +76,14 @@ type Workflow struct { 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. //