diff --git a/cmd/root.go b/cmd/root.go index d37b33e0bf9..5f9283d6f27 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,6 +11,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/andreaskoch/go-fswatch" + "github.com/google/uuid" "github.com/joho/godotenv" "github.com/mitchellh/go-homedir" gitignore "github.com/sabhiram/go-gitignore" @@ -250,6 +251,9 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str } } + // Generate a new UUID + runid := strings.ReplaceAll(uuid.New().String(), "-", "") + // run the plan config := &runner.Config{ Actor: input.actor, @@ -274,6 +278,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str ContainerCapAdd: input.containerCapAdd, ContainerCapDrop: input.containerCapDrop, AutoRemove: input.autoRemove, + RunID: runid, } r, err := runner.New(config) if err != nil { diff --git a/go.mod b/go.mod index 6f6662ae279..7fe200a2845 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-git/go-git/v5 v5.2.0 github.com/go-ini/ini v1.62.0 github.com/golang/protobuf v1.4.3 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/joho/godotenv v1.3.0 diff --git a/go.sum b/go.sum index d8ffbdd7b0d..7715e08ab59 100644 --- a/go.sum +++ b/go.sum @@ -466,6 +466,8 @@ github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= diff --git a/pkg/common/executor.go b/pkg/common/executor.go index cd92a6c54d1..5e9fe501572 100644 --- a/pkg/common/executor.go +++ b/pkg/common/executor.go @@ -90,20 +90,30 @@ func NewErrorExecutor(err error) Executor { } } +func parallelExecutorWorker(executorChan <-chan Executor, errChan chan error, ctx *context.Context) { + for executor := range executorChan { + err := executor.ChannelError(errChan)(*ctx) + if err != nil { + log.Fatal(err) + } + } +} + // NewParallelExecutor creates a new executor from a parallel of other executors func NewParallelExecutor(executors ...Executor) Executor { return func(ctx context.Context) error { + maxJobs := 4 errChan := make(chan error) + executorChan := make(chan Executor, len(executors)) + for i := 0; i < maxJobs; i++ { + go parallelExecutorWorker(executorChan, errChan, &ctx) + } for _, executor := range executors { e := executor - go func() { - err := e.ChannelError(errChan)(ctx) - if err != nil { - log.Fatal(err) - } - }() + executorChan <- e } + close(executorChan) // Executor waits all executors to cleanup these resources. var firstErr error diff --git a/pkg/common/git.go b/pkg/common/git.go index c87906aeaaa..6b4793f47aa 100644 --- a/pkg/common/git.go +++ b/pkg/common/git.go @@ -285,6 +285,11 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor { defer cloneLock.Unlock() refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", input.Ref)) + + if _, err := git.PlainOpen(input.Dir); err == nil { + return nil + } + r, err := CloneIfRequired(ctx, refName, input, logger) if err != nil { return err diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 7e21c5f35fb..d91c09105d4 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -226,21 +226,11 @@ func (j *Job) GetMatrixes() []map[string]interface{} { case []interface{}: for _, i := range t { i := i.(map[string]interface{}) - for k := range i { - if _, ok := m[k]; ok { - includes = append(includes, i) - break - } - } + includes = append(includes, i) } case interface{}: v := v.(map[string]interface{}) - for k := range v { - if _, ok := m[k]; ok { - includes = append(includes, v) - break - } - } + includes = append(includes, v) } } delete(m, "include") @@ -271,8 +261,20 @@ func (j *Job) GetMatrixes() []map[string]interface{} { matrixes = append(matrixes, matrix) } for _, include := range includes { - log.Debugf("Adding include '%v'", include) - matrixes = append(matrixes, include) + notAdded := true + for _, matrix := range matrixProduct { + if hasKeysInCommon(matrix, include) && commonKeysMatch(matrix, include) { + log.Debugf("Applying include '%v' to matrix entry '%v'", include, matrix) + for k, v := range include { + matrix[k] = v + } + notAdded = false + } + } + if notAdded { + log.Debugf("Appending include '%v' to matrix", include) + matrixes = append(matrixes, include) + } } } else { matrixes = append(matrixes, make(map[string]interface{})) @@ -292,6 +294,17 @@ func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool { return true } +func hasKeysInCommon(a map[string]interface{}, b map[string]interface{}) bool { + for aKey := range a { + for bKey := range b { + if aKey == bKey { + return true + } + } + } + return false +} + // ContainerSpec is the specification of the container to use for the job type ContainerSpec struct { Image string `yaml:"image"` diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index c736ed176aa..4b15ed9248c 100755 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -62,7 +62,8 @@ func (rc *RunContext) GetEnv() map[string]string { } func (rc *RunContext) jobContainerName() string { - return createContainerName("act", rc.String()) + name := createContainerName("act", rc.Config.RunID, rc.String()) + return name } // Returns the binds and mounts for the container, resolving paths as appopriate @@ -74,11 +75,15 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) { } binds := []string{ - fmt.Sprintf("%s:%s", rc.Config.ContainerDaemonSocket, "/var/run/docker.sock"), + // ***DO NOT BIND '/var/run/docker.sock'!*** If you do, then you're giving the runner image, running code + // scraped from the internet, access to docker ON THE HOST MACHINE. + // (I've already come across a workflow that runs 'docker image prune -af'. That was a nasty shock. --Robert) + // fmt.Sprintf("%s:%s", rc.Config.ContainerDaemonSocket, "/var/run/docker.sock"), } mounts := map[string]string{ "act-toolcache": "/toolcache", + "act-artifacts-" + rc.Config.RunID: "/artifacts", } if rc.Config.BindWorkdir { @@ -224,7 +229,9 @@ func (rc *RunContext) Executor() common.Executor { } steps = append(steps, rc.stopJobContainer()) - return common.NewPipelineExecutor(steps...).If(rc.isEnabled) + return common.NewPipelineExecutor(steps...). + Finally(rc.stopJobContainer().IfBool(rc.Config.AutoRemove)). + If(rc.isEnabled) } func (rc *RunContext) newStepExecutor(step *model.Step) common.Executor { @@ -403,22 +410,8 @@ func mergeMaps(maps ...map[string]string) map[string]string { func createContainerName(parts ...string) string { name := make([]string, 0) pattern := regexp.MustCompile("[^a-zA-Z0-9]") - partLen := (30 / len(parts)) - 1 - for i, part := range parts { - if i == len(parts)-1 { - name = append(name, pattern.ReplaceAllString(part, "-")) - } else { - // If any part has a '-' on the end it is likely part of a matrix job. - // Let's preserve the number to prevent clashes in container names. - re := regexp.MustCompile("-[0-9]+$") - num := re.FindStringSubmatch(part) - if len(num) > 0 { - name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen-len(num[0]))) - name = append(name, num[0]) - } else { - name = append(name, trimToLen(pattern.ReplaceAllString(part, "-"), partLen)) - } - } + for _, part := range parts { + name = append(name, pattern.ReplaceAllString(part, "-")) } return strings.ReplaceAll(strings.Trim(strings.Join(name, "-"), "-"), "--", "-") } @@ -585,7 +578,7 @@ func (rc *RunContext) getGithubContext() *githubContext { return ghc } -func (ghc *githubContext) isLocalCheckout(step *model.Step) bool { +func (ghc *githubContext) isLocalCheckout(step *model.Step, ee ExpressionEvaluator) bool { if step.Type() == model.StepTypeInvalid { // This will be errored out by the executor later, we need this here to avoid a null panic though return false @@ -605,8 +598,11 @@ func (ghc *githubContext) isLocalCheckout(step *model.Step) bool { if repository, ok := step.With["repository"]; ok && repository != ghc.Repository { return false } - if repository, ok := step.With["ref"]; ok && repository != ghc.Ref { - return false + if ref, ok := step.With["ref"]; ok { + interp, ok2 := ee.InterpolateWithStringCheck(ref) + if ok2 && interp != ghc.Ref { + return false + } } return true } @@ -721,7 +717,7 @@ func (rc *RunContext) withGithubEnv(env map[string]string) map[string]string { func (rc *RunContext) localCheckoutPath() (string, bool) { ghContext := rc.getGithubContext() for _, step := range rc.Run.Job().Steps { - if ghContext.isLocalCheckout(step) { + if ghContext.isLocalCheckout(step, rc.ExprEval) { return step.With["path"], true } } diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index f34f9e80a33..75ffa7293bd 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/container" "github.com/nektos/act/pkg/model" log "github.com/sirupsen/logrus" ) @@ -43,6 +44,7 @@ type Config struct { ContainerCapAdd []string // list of kernel capabilities to add to the containers ContainerCapDrop []string // list of kernel capabilities to remove from the containers AutoRemove bool // controls if the container is automatically removed upon workflow completion + RunID string // A unique ID for the current workflow run } // Resolves the equivalent host path inside the container @@ -149,7 +151,19 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor { pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...)) } - return common.NewPipelineExecutor(pipeline...) + return common.NewPipelineExecutor(pipeline...). + Finally(func(ctx context.Context) error { + if !runner.config.AutoRemove { + return nil + } + artifactVolume := "act-artifacts-" + runner.config.RunID + log.Infof("Cleaning up artifacts volume \"%s\"", artifactVolume) + err := container.NewDockerVolumeRemoveExecutor(artifactVolume, false)(ctx) + if err != nil { + log.Errorf("Error cleaning up volume \"%s\": %v", artifactVolume, err) + } + return err + }) } func (runner *runnerImpl) newRunContext(run *model.Run, matrix map[string]interface{}) *RunContext { diff --git a/pkg/runner/step_context.go b/pkg/runner/step_context.go index baa88a56e6a..eb2447a9410 100644 --- a/pkg/runner/step_context.go +++ b/pkg/runner/step_context.go @@ -92,7 +92,7 @@ func (sc *StepContext) Executor() common.Executor { remoteAction.URL = rc.Config.GitHubInstance github := rc.getGithubContext() - if remoteAction.IsCheckout() && github.isLocalCheckout(step) { + if remoteAction.IsCheckout() && github.isLocalCheckout(step, rc.ExprEval) { return func(ctx context.Context) error { common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied") return nil @@ -227,7 +227,7 @@ func (sc *StepContext) setupShellCommand() common.Executor { run = runPrepend + "\n" + run + "\n" + runAppend log.Debugf("Wrote command '%s' to '%s'", run, scriptName) - scriptPath := fmt.Sprintf("%s/%s", rc.Config.ContainerWorkdir(), scriptName) + scriptPath := fmt.Sprintf("/tmp/%s", scriptName) if step.Shell == "" { step.Shell = rc.Run.Job().Defaults.Run.Shell @@ -252,7 +252,7 @@ func (sc *StepContext) setupShellCommand() common.Executor { sc.Cmd = finalCMD - return rc.JobContainer.Copy(rc.Config.ContainerWorkdir(), &container.FileEntry{ + return rc.JobContainer.Copy("/tmp/", &container.FileEntry{ Name: scriptName, Mode: 0755, Body: script.String(),