From 532af98aefbce84ec0de375cdfa0df813a84737a Mon Sep 17 00:00:00 2001
From: Casey Lee <cplee@nektos.com>
Date: Thu, 6 Feb 2020 22:17:58 -0800
Subject: [PATCH] successfully able to run simple workflows

Signed-off-by: Casey Lee <cplee@nektos.com>
---
 .github/workflows/basic.yml            |  12 +-
 {pkg/runner => actions}/runner_exec.go |   9 +-
 cmd/graph.go                           |   3 +-
 cmd/input.go                           |   9 +-
 cmd/root.go                            |  43 +--
 go.mod                                 |   9 +-
 go.sum                                 |  25 +-
 pkg/common/dryrun.go                   |  25 ++
 pkg/common/executor.go                 | 169 ++++++++----
 pkg/common/executor_test.go            |  37 +--
 pkg/common/git.go                      |   3 +-
 pkg/common/logger.go                   |  27 ++
 pkg/container/docker_build.go          |  19 +-
 pkg/container/docker_common.go         | 115 --------
 pkg/container/docker_logger.go         | 117 ++++++++
 pkg/container/docker_pull.go           |  38 ++-
 pkg/container/docker_run.go            | 356 +++++++++++++------------
 pkg/container/docker_run_test.go       |  14 +-
 pkg/model/planner.go                   |  17 ++
 pkg/model/workflow.go                  |  42 ++-
 actions/log.go => pkg/runner/logger.go |  41 ++-
 pkg/runner/run_context.go              | 269 +++++++++++++++++++
 pkg/runner/runner.go                   |  54 ++--
 23 files changed, 958 insertions(+), 495 deletions(-)
 rename {pkg/runner => actions}/runner_exec.go (99%)
 create mode 100644 pkg/common/dryrun.go
 create mode 100644 pkg/common/logger.go
 delete mode 100644 pkg/container/docker_common.go
 create mode 100644 pkg/container/docker_logger.go
 rename actions/log.go => pkg/runner/logger.go (60%)
 create mode 100644 pkg/runner/run_context.go

diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml
index 7cdb3b4..ae962d0 100644
--- a/.github/workflows/basic.yml
+++ b/.github/workflows/basic.yml
@@ -4,5 +4,15 @@ on: push
 jobs:
   build:
     runs-on: ubuntu-latest
+    container: 
+      image: node:10.16-jessie
+      env:
+        NODE_ENV: development
     steps:
-    - run: echo hello world!
+      - run: env
+  test:
+    runs-on: ubuntu-latest
+    steps:
+    - run: cp $GITHUB_EVENT_PATH $HOME/foo.json
+    - run: ls $HOME
+    - run: cat $HOME/foo.json
diff --git a/pkg/runner/runner_exec.go b/actions/runner_exec.go
similarity index 99%
rename from pkg/runner/runner_exec.go
rename to actions/runner_exec.go
index 0a52fab..78b97f5 100644
--- a/pkg/runner/runner_exec.go
+++ b/actions/runner_exec.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/nektos/act/pkg/common"
 	"github.com/nektos/act/pkg/container"
-	"github.com/nektos/act/pkg/model"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -34,7 +33,6 @@ func (runner *runnerImpl) newRunExecutor(run *model.Run) common.Executor {
 	return common.NewPipelineExecutor(executors...)
 }
 
-/*
 func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.Executor) (string, error) {
 	var image string
 	logger := newActionLogger(action.Identifier, runner.config.Dryrun)
@@ -111,7 +109,6 @@ func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.E
 
 	return image, nil
 }
-*/
 
 func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors *[]common.Executor) error {
 	logger := newActionLogger(action.Identifier, runner.config.Dryrun)
@@ -141,7 +138,11 @@ func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors
 
 	var cmd, entrypoint []string
 	if action.Args != nil {
-		cmd = action.Args.Split()
+		cmd = []string{
+			"/bin/sh", 
+			"-c",
+			action.Args,
+		}
 	}
 	if action.Runs != nil {
 		entrypoint = action.Runs.Split()
diff --git a/cmd/graph.go b/cmd/graph.go
index e8d0204..4f8895e 100644
--- a/cmd/graph.go
+++ b/cmd/graph.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"fmt"
 	"os"
 
 	"github.com/nektos/act/pkg/common"
@@ -21,7 +20,7 @@ func drawGraph(plan *model.Plan) error {
 
 		ids := make([]string, 0)
 		for _, r := range stage.Runs {
-			ids = append(ids, fmt.Sprintf("%s/%s", r.Workflow.Name, r.JobID))
+			ids = append(ids, r.String())
 		}
 		drawings = append(drawings, jobPen.DrawBoxes(ids...))
 	}
diff --git a/cmd/input.go b/cmd/input.go
index 9f6ea5c..0366105 100644
--- a/cmd/input.go
+++ b/cmd/input.go
@@ -7,7 +7,7 @@ import (
 
 // Input contains the input for the root command
 type Input struct {
-	workingDir      string
+	workdir         string
 	workflowsPath   string
 	eventPath       string
 	reuseContainers bool
@@ -16,7 +16,7 @@ type Input struct {
 }
 
 func (i *Input) resolve(path string) string {
-	basedir, err := filepath.Abs(i.workingDir)
+	basedir, err := filepath.Abs(i.workdir)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -29,6 +29,11 @@ func (i *Input) resolve(path string) string {
 	return path
 }
 
+// Workdir returns path to workdir
+func (i *Input) Workdir() string {
+	return i.resolve(".")
+}
+
 // WorkflowsPath returns path to workflows
 func (i *Input) WorkflowsPath() string {
 	return i.resolve(i.workflowsPath)
diff --git a/cmd/root.go b/cmd/root.go
index afe9a73..3bde51b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -5,8 +5,11 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/nektos/act/pkg/common"
+
 	fswatch "github.com/andreaskoch/go-fswatch"
 	"github.com/nektos/act/pkg/model"
+	"github.com/nektos/act/pkg/runner"
 	gitignore "github.com/sabhiram/go-gitignore"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -31,7 +34,7 @@ func Execute(ctx context.Context, version string) {
 	rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) if already present")
 	rootCmd.Flags().StringVarP(&input.eventPath, "event", "e", "", "path to event JSON file")
 	rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow files")
-	rootCmd.PersistentFlags().StringVarP(&input.workingDir, "directory", "C", ".", "working directory")
+	rootCmd.PersistentFlags().StringVarP(&input.workdir, "directory", "C", ".", "working directory")
 	rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
 	rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
 	if err := rootCmd.Execute(); err != nil {
@@ -87,26 +90,30 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 		}
 
 		// run the plan
-		// runner, err := runner.New(config)
-		// if err != nil {
-		// 	return err
-		// }
-		// defer runner.Close()
+		config := &runner.Config{
+			EventName:       eventName,
+			EventPath:       input.EventPath(),
+			ForcePull:       input.forcePull,
+			ReuseContainers: input.reuseContainers,
+			Workdir:         input.Workdir(),
+		}
+		runner, err := runner.New(config)
+		if err != nil {
+			return err
+		}
 
-		// if watch, err := cmd.Flags().GetBool("watch"); err != nil {
-		// 	return err
-		// } else if watch {
-		// 	return watchAndRun(ctx, func() error {
-		// 		return runner.RunPlan(plan)
-		// 	})
-		// }
+		ctx = common.WithDryrun(ctx, input.dryrun)
+		if watch, err := cmd.Flags().GetBool("watch"); err != nil {
+			return err
+		} else if watch {
+			return watchAndRun(ctx, runner.NewPlanExecutor(plan))
+		}
 
-		// return runner.RunPlan(plan)
-		return nil
+		return runner.NewPlanExecutor(plan)(ctx)
 	}
 }
 
-func watchAndRun(ctx context.Context, fn func() error) error {
+func watchAndRun(ctx context.Context, fn common.Executor) error {
 	recurse := true
 	checkIntervalInSeconds := 2
 	dir, err := os.Getwd()
@@ -132,13 +139,13 @@ func watchAndRun(ctx context.Context, fn func() error) error {
 
 	go func() {
 		for folderWatcher.IsRunning() {
-			if err = fn(); err != nil {
+			if err = fn(ctx); err != nil {
 				break
 			}
 			log.Debugf("Watching %s for changes", dir)
 			for changes := range folderWatcher.ChangeDetails() {
 				log.Debugf("%s", changes.String())
-				if err = fn(); err != nil {
+				if err = fn(ctx); err != nil {
 					break
 				}
 				log.Debugf("Watching %s for changes", dir)
diff --git a/go.mod b/go.mod
index 5aa814e..d135010 100644
--- a/go.mod
+++ b/go.mod
@@ -13,10 +13,8 @@ require (
 	github.com/emirpasic/gods v1.12.0 // indirect
 	github.com/go-ini/ini v1.41.0
 	github.com/gogo/protobuf v1.2.0 // indirect
-	github.com/gophercloud/gophercloud v0.7.0
 	github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
 	github.com/gorilla/mux v1.7.0 // indirect
-	github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93
 	github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/jtolds/gls v4.2.1+incompatible // indirect
@@ -24,7 +22,7 @@ require (
 	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
 	github.com/opencontainers/image-spec v1.0.1 // indirect
 	github.com/opencontainers/runc v0.1.1 // indirect
-	github.com/pkg/errors v0.8.1 // indirect
+	github.com/pkg/errors v0.8.1
 	github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
 	github.com/sirupsen/logrus v1.3.0
 	github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
@@ -33,9 +31,14 @@ require (
 	github.com/spf13/pflag v1.0.3 // indirect
 	github.com/stretchr/testify v1.3.0
 	golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e
+	golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
+	golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
+	golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect
+	golang.org/x/text v0.3.2 // indirect
 	golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
 	google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca // indirect
 	google.golang.org/grpc v1.18.0 // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	gopkg.in/ini.v1 v1.41.0 // indirect
 	gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect
 	gopkg.in/src-d/go-git-fixtures.v3 v3.3.0 // indirect
diff --git a/go.sum b/go.sum
index 30170e1..3bc8655 100644
--- a/go.sum
+++ b/go.sum
@@ -45,14 +45,10 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/gophercloud/gophercloud v0.7.0 h1:vhmQQEM2SbnGCg2/3EzQnQZ3V7+UCGy9s8exQCprNYg=
-github.com/gophercloud/gophercloud v0.7.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
 github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 h1:T1Q6ag9tCwun16AW+XK3tAql24P4uTGUMIn1/92WsQQ=
-github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE=
 github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
 github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -77,10 +73,6 @@ github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnG
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
-github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
 github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
 github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
@@ -122,8 +114,6 @@ github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro
 github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
-golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU=
 golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -133,33 +123,24 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
-golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk=
 golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533 h1:bLfqnzrpeG4usq5OvMCrwTdmMJ6aTmlCuo1eKl0mhkI=
-golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU=
 golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
@@ -167,9 +148,6 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -197,7 +175,6 @@ gopkg.in/src-d/go-git.v4 v4.9.1 h1:0oKHJZY8tM7B71378cfTg2c5jmWyNlXvestTT6WfY+4=
 gopkg.in/src-d/go-git.v4 v4.9.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk=
 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
-gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
diff --git a/pkg/common/dryrun.go b/pkg/common/dryrun.go
new file mode 100644
index 0000000..2d5a14e
--- /dev/null
+++ b/pkg/common/dryrun.go
@@ -0,0 +1,25 @@
+package common
+
+import (
+	"context"
+)
+
+type dryrunContextKey string
+
+const dryrunContextKeyVal = dryrunContextKey("dryrun")
+
+// Dryrun returns true if the current context is dryrun
+func Dryrun(ctx context.Context) bool {
+	val := ctx.Value(dryrunContextKeyVal)
+	if val != nil {
+		if dryrun, ok := val.(bool); ok {
+			return dryrun
+		}
+	}
+	return false
+}
+
+// WithDryrun adds a value to the context for dryrun
+func WithDryrun(ctx context.Context, dryrun bool) context.Context {
+	return context.WithValue(ctx, dryrunContextKeyVal, dryrun)
+}
diff --git a/pkg/common/executor.go b/pkg/common/executor.go
index 18c6528..0002567 100644
--- a/pkg/common/executor.go
+++ b/pkg/common/executor.go
@@ -1,6 +1,7 @@
 package common
 
 import (
+	"context"
 	"fmt"
 
 	log "github.com/sirupsen/logrus"
@@ -25,26 +26,76 @@ func Warningf(format string, args ...interface{}) Warning {
 }
 
 // Executor define contract for the steps of a workflow
-type Executor func() error
+type Executor func(ctx context.Context) error
 
 // Conditional define contract for the conditional predicate
-type Conditional func() bool
+type Conditional func(ctx context.Context) bool
+
+// NewInfoExecutor is an executor that logs messages
+func NewInfoExecutor(format string, args ...interface{}) Executor {
+	return func(ctx context.Context) error {
+		logger := Logger(ctx)
+		logger.Infof(format, args...)
+		return nil
+	}
+}
 
 // NewPipelineExecutor creates a new executor from a series of other executors
 func NewPipelineExecutor(executors ...Executor) Executor {
-	return func() error {
-		for _, executor := range executors {
-			if executor == nil {
-				continue
+	if executors == nil {
+		return func(ctx context.Context) error {
+			return nil
+		}
+	}
+	var rtn Executor
+	for _, executor := range executors {
+		if rtn == nil {
+			rtn = executor
+		} else {
+			rtn = rtn.Then(executor)
+		}
+	}
+	return rtn
+}
+
+// NewConditionalExecutor creates a new executor based on conditions
+func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
+	return func(ctx context.Context) error {
+		if conditional(ctx) {
+			if trueExecutor != nil {
+				return trueExecutor(ctx)
 			}
-			err := executor()
-			if err != nil {
-				switch err.(type) {
-				case Warning:
-					log.Warning(err.Error())
-					return nil
-				default:
-					log.Debugf("%+v", err)
+		} else {
+			if falseExecutor != nil {
+				return falseExecutor(ctx)
+			}
+		}
+		return nil
+	}
+}
+
+// NewErrorExecutor creates a new executor that always errors out
+func NewErrorExecutor(err error) Executor {
+	return func(ctx context.Context) error {
+		return err
+	}
+}
+
+// NewParallelExecutor creates a new executor from a parallel of other executors
+func NewParallelExecutor(executors ...Executor) Executor {
+	return func(ctx context.Context) error {
+		errChan := make(chan error)
+
+		for _, executor := range executors {
+			go executor.ChannelError(errChan)(ctx)
+		}
+
+		for i := 0; i < len(executors); i++ {
+			select {
+			case <-ctx.Done():
+				return ctx.Err()
+			case err := <-errChan:
+				if err != nil {
 					return err
 				}
 			}
@@ -53,48 +104,76 @@ func NewPipelineExecutor(executors ...Executor) Executor {
 	}
 }
 
-// NewConditionalExecutor creates a new executor based on conditions
-func NewConditionalExecutor(conditional Conditional, trueExecutor Executor, falseExecutor Executor) Executor {
-	return func() error {
-		if conditional() {
-			if trueExecutor != nil {
-				return trueExecutor()
-			}
-		} else {
-			if falseExecutor != nil {
-				return falseExecutor()
+// ChannelError sends error to errChan rather than returning error
+func (e Executor) ChannelError(errChan chan error) Executor {
+	return func(ctx context.Context) error {
+		errChan <- e(ctx)
+		return nil
+	}
+}
+
+// Then runs another executor if this executor succeeds
+func (e Executor) Then(then Executor) Executor {
+	return func(ctx context.Context) error {
+		err := e(ctx)
+		if err != nil {
+			switch err.(type) {
+			case Warning:
+				log.Warning(err.Error())
+			default:
+				log.Debugf("%+v", err)
+				return err
 			}
 		}
+		if ctx.Err() != nil {
+			return ctx.Err()
+		}
+		return then(ctx)
+	}
+}
+
+// If only runs this executor if conditional is true
+func (e Executor) If(conditional Conditional) Executor {
+	return func(ctx context.Context) error {
+		if conditional(ctx) {
+			return e(ctx)
+		}
 		return nil
 	}
 }
 
-func executeWithChan(executor Executor, errChan chan error) {
-	errChan <- executor()
+// IfNot only runs this executor if conditional is true
+func (e Executor) IfNot(conditional Conditional) Executor {
+	return func(ctx context.Context) error {
+		if !conditional(ctx) {
+			return e(ctx)
+		}
+		return nil
+	}
 }
 
-// NewErrorExecutor creates a new executor that always errors out
-func NewErrorExecutor(err error) Executor {
-	return func() error {
+// IfBool only runs this executor if conditional is true
+func (e Executor) IfBool(conditional bool) Executor {
+	return e.If(func(ctx context.Context) bool {
+		return conditional
+	})
+}
+
+// Finally adds an executor to run after other executor
+func (e Executor) Finally(finally Executor) Executor {
+	return func(ctx context.Context) error {
+		err := e(ctx)
+		err2 := finally(ctx)
+		if err2 != nil {
+			return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err)
+		}
 		return err
 	}
 }
 
-// NewParallelExecutor creates a new executor from a parallel of other executors
-func NewParallelExecutor(executors ...Executor) Executor {
-	return func() error {
-		errChan := make(chan error)
-
-		for _, executor := range executors {
-			go executeWithChan(executor, errChan)
-		}
-
-		for i := 0; i < len(executors); i++ {
-			err := <-errChan
-			if err != nil {
-				return err
-			}
-		}
-		return nil
+// Not return an inverted conditional
+func (c Conditional) Not() Conditional {
+	return func(ctx context.Context) bool {
+		return !c(ctx)
 	}
 }
diff --git a/pkg/common/executor_test.go b/pkg/common/executor_test.go
index cf85089..215d4fc 100644
--- a/pkg/common/executor_test.go
+++ b/pkg/common/executor_test.go
@@ -1,6 +1,7 @@
 package common
 
 import (
+	"context"
 	"fmt"
 	"testing"
 
@@ -10,58 +11,62 @@ import (
 func TestNewWorkflow(t *testing.T) {
 	assert := assert.New(t)
 
+	ctx := context.Background()
+
 	// empty
 	emptyWorkflow := NewPipelineExecutor()
-	assert.Nil(emptyWorkflow())
+	assert.Nil(emptyWorkflow(ctx))
 
 	// error case
 	errorWorkflow := NewErrorExecutor(fmt.Errorf("test error"))
-	assert.NotNil(errorWorkflow())
+	assert.NotNil(errorWorkflow(ctx))
 
 	// multiple success case
 	runcount := 0
 	successWorkflow := NewPipelineExecutor(
-		func() error {
+		func(ctx context.Context) error {
 			runcount++
 			return nil
 		},
-		func() error {
+		func(ctx context.Context) error {
 			runcount++
 			return nil
 		})
-	assert.Nil(successWorkflow())
+	assert.Nil(successWorkflow(ctx))
 	assert.Equal(2, runcount)
 }
 
 func TestNewConditionalExecutor(t *testing.T) {
 	assert := assert.New(t)
 
+	ctx := context.Background()
+
 	trueCount := 0
 	falseCount := 0
 
-	err := NewConditionalExecutor(func() bool {
+	err := NewConditionalExecutor(func(ctx context.Context) bool {
 		return false
-	}, func() error {
+	}, func(ctx context.Context) error {
 		trueCount++
 		return nil
-	}, func() error {
+	}, func(ctx context.Context) error {
 		falseCount++
 		return nil
-	})()
+	})(ctx)
 
 	assert.Nil(err)
 	assert.Equal(0, trueCount)
 	assert.Equal(1, falseCount)
 
-	err = NewConditionalExecutor(func() bool {
+	err = NewConditionalExecutor(func(ctx context.Context) bool {
 		return true
-	}, func() error {
+	}, func(ctx context.Context) error {
 		trueCount++
 		return nil
-	}, func() error {
+	}, func(ctx context.Context) error {
 		falseCount++
 		return nil
-	})()
+	})(ctx)
 
 	assert.Nil(err)
 	assert.Equal(1, trueCount)
@@ -71,13 +76,15 @@ func TestNewConditionalExecutor(t *testing.T) {
 func TestNewParallelExecutor(t *testing.T) {
 	assert := assert.New(t)
 
+	ctx := context.Background()
+
 	count := 0
-	emptyWorkflow := NewPipelineExecutor(func() error {
+	emptyWorkflow := NewPipelineExecutor(func(ctx context.Context) error {
 		count++
 		return nil
 	})
 
-	err := NewParallelExecutor(emptyWorkflow, emptyWorkflow)()
+	err := NewParallelExecutor(emptyWorkflow, emptyWorkflow)(ctx)
 	assert.Equal(2, count)
 
 	assert.Nil(err)
diff --git a/pkg/common/git.go b/pkg/common/git.go
index 03afea2..a3fcb4a 100644
--- a/pkg/common/git.go
+++ b/pkg/common/git.go
@@ -1,6 +1,7 @@
 package common
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -190,7 +191,7 @@ type NewGitCloneExecutorInput struct {
 
 // NewGitCloneExecutor creates an executor to clone git repos
 func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
-	return func() error {
+	return func(ctx context.Context) error {
 		input.Logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
 		input.Logger.Debugf("  cloning %s to %s", input.URL, input.Dir)
 
diff --git a/pkg/common/logger.go b/pkg/common/logger.go
new file mode 100644
index 0000000..a9501ce
--- /dev/null
+++ b/pkg/common/logger.go
@@ -0,0 +1,27 @@
+package common
+
+import (
+	"context"
+
+	"github.com/sirupsen/logrus"
+)
+
+type loggerContextKey string
+
+const loggerContextKeyVal = loggerContextKey("logrus.FieldLogger")
+
+// Logger returns the appropriate logger for current context
+func Logger(ctx context.Context) logrus.FieldLogger {
+	val := ctx.Value(loggerContextKeyVal)
+	if val != nil {
+		if logger, ok := val.(logrus.FieldLogger); ok {
+			return logger
+		}
+	}
+	return logrus.StandardLogger()
+}
+
+// WithLogger adds a value to the context for the logger
+func WithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context {
+	return context.WithValue(ctx, loggerContextKeyVal, logger)
+}
diff --git a/pkg/container/docker_build.go b/pkg/container/docker_build.go
index 9d3c621..d3b5b34 100644
--- a/pkg/container/docker_build.go
+++ b/pkg/container/docker_build.go
@@ -1,6 +1,7 @@
 package container
 
 import (
+	"context"
 	"io"
 	"os"
 	"path/filepath"
@@ -16,16 +17,16 @@ import (
 
 // NewDockerBuildExecutorInput the input for the NewDockerBuildExecutor function
 type NewDockerBuildExecutorInput struct {
-	DockerExecutorInput
 	ContextDir string
 	ImageTag   string
 }
 
 // NewDockerBuildExecutor function to create a run executor for the container
 func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
-	return func() error {
-		input.Logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
-		if input.Dryrun {
+	return func(ctx context.Context) error {
+		logger := common.Logger(ctx)
+		logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
+		if common.Dryrun(ctx) {
 			return nil
 		}
 
@@ -33,9 +34,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
 		if err != nil {
 			return err
 		}
-		cli.NegotiateAPIVersion(input.Ctx)
+		cli.NegotiateAPIVersion(ctx)
 
-		input.Logger.Debugf("Building image from '%v'", input.ContextDir)
+		logger.Debugf("Building image from '%v'", input.ContextDir)
 
 		tags := []string{input.ImageTag}
 		options := types.ImageBuildOptions{
@@ -49,10 +50,10 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
 
 		defer buildContext.Close()
 
-		input.Logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag)
-		resp, err := cli.ImageBuild(input.Ctx, buildContext, options)
+		logger.Debugf("Creating image from context dir '%s' with tag '%s'", input.ContextDir, input.ImageTag)
+		resp, err := cli.ImageBuild(ctx, buildContext, options)
 
-		err = input.logDockerResponse(resp.Body, err != nil)
+		err = logDockerResponse(logger, resp.Body, err != nil)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/container/docker_common.go b/pkg/container/docker_common.go
deleted file mode 100644
index 5d9352e..0000000
--- a/pkg/container/docker_common.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package container
-
-import (
-	"bufio"
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"github.com/docker/docker/pkg/stdcopy"
-	"io"
-	"os"
-
-	"github.com/sirupsen/logrus"
-)
-
-// DockerExecutorInput common input params
-type DockerExecutorInput struct {
-	Ctx    context.Context
-	Logger *logrus.Entry
-	Dryrun bool
-}
-
-type dockerMessage struct {
-	ID          string `json:"id"`
-	Stream      string `json:"stream"`
-	Error       string `json:"error"`
-	ErrorDetail struct {
-		Message string
-	}
-	Status   string `json:"status"`
-	Progress string `json:"progress"`
-}
-
-func (i *DockerExecutorInput) logDockerOutput(dockerResponse io.Reader) {
-	w := i.Logger.Writer()
-	_, err := stdcopy.StdCopy(w, w, dockerResponse)
-	if err != nil {
-		i.Logger.Error(err)
-	}
-}
-
-func (i *DockerExecutorInput) streamDockerOutput(dockerResponse io.Reader) {
-	out := os.Stdout
-	go func() {
-		<-i.Ctx.Done()
-		fmt.Println()
-	}()
-
-	_, err := io.Copy(out, dockerResponse)
-	if err != nil {
-		i.Logger.Error(err)
-	}
-}
-
-func (i *DockerExecutorInput) writeLog(isError bool, format string, args ...interface{}) {
-	if i.Logger == nil {
-		return
-	}
-	if isError {
-		i.Logger.Errorf(format, args...)
-	} else {
-		i.Logger.Debugf(format, args...)
-	}
-
-}
-
-func (i *DockerExecutorInput) logDockerResponse(dockerResponse io.ReadCloser, isError bool) error {
-	if dockerResponse == nil {
-		return nil
-	}
-	defer dockerResponse.Close()
-
-	scanner := bufio.NewScanner(dockerResponse)
-	msg := dockerMessage{}
-
-	for scanner.Scan() {
-		line := scanner.Bytes()
-
-		msg.ID = ""
-		msg.Stream = ""
-		msg.Error = ""
-		msg.ErrorDetail.Message = ""
-		msg.Status = ""
-		msg.Progress = ""
-
-		if err := json.Unmarshal(line, &msg); err != nil {
-			i.writeLog(false, "Unable to unmarshal line [%s] ==> %v", string(line), err)
-			continue
-		}
-
-		if msg.Error != "" {
-			i.writeLog(isError, "%s", msg.Error)
-			return errors.New(msg.Error)
-		}
-
-		if msg.ErrorDetail.Message != "" {
-			i.writeLog(isError, "%s", msg.ErrorDetail.Message)
-			return errors.New(msg.Error)
-		}
-
-		if msg.Status != "" {
-			if msg.Progress != "" {
-				i.writeLog(isError, "%s :: %s :: %s\n", msg.Status, msg.ID, msg.Progress)
-			} else {
-				i.writeLog(isError, "%s :: %s\n", msg.Status, msg.ID)
-			}
-		} else if msg.Stream != "" {
-			i.writeLog(isError, msg.Stream)
-		} else {
-			i.writeLog(false, "Unable to handle line: %s", string(line))
-		}
-	}
-
-	return nil
-}
diff --git a/pkg/container/docker_logger.go b/pkg/container/docker_logger.go
new file mode 100644
index 0000000..8a27d41
--- /dev/null
+++ b/pkg/container/docker_logger.go
@@ -0,0 +1,117 @@
+package container
+
+import (
+	"bufio"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/nektos/act/pkg/common"
+	"github.com/sirupsen/logrus"
+
+	"github.com/docker/docker/pkg/stdcopy"
+)
+
+type dockerMessage struct {
+	ID          string `json:"id"`
+	Stream      string `json:"stream"`
+	Error       string `json:"error"`
+	ErrorDetail struct {
+		Message string
+	}
+	Status   string `json:"status"`
+	Progress string `json:"progress"`
+}
+
+func logDockerOutput(ctx context.Context, dockerResponse io.Reader) {
+	logger := common.Logger(ctx)
+	if entry, ok := logger.(*logrus.Entry); ok {
+		w := entry.Writer()
+		_, err := stdcopy.StdCopy(w, w, dockerResponse)
+		if err != nil {
+			logrus.Error(err)
+		}
+	} else if lgr, ok := logger.(*logrus.Logger); ok {
+		w := lgr.Writer()
+		_, err := stdcopy.StdCopy(w, w, dockerResponse)
+		if err != nil {
+			logrus.Error(err)
+		}
+	} else {
+		logrus.Errorf("Unable to get writer from logger (type=%T)", logger)
+	}
+}
+
+func streamDockerOutput(ctx context.Context, dockerResponse io.Reader) {
+	out := os.Stdout
+	go func() {
+		<-ctx.Done()
+		fmt.Println()
+	}()
+
+	_, err := io.Copy(out, dockerResponse)
+	if err != nil {
+		logrus.Error(err)
+	}
+}
+
+func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
+	if dockerResponse == nil {
+		return nil
+	}
+	defer dockerResponse.Close()
+
+	scanner := bufio.NewScanner(dockerResponse)
+	msg := dockerMessage{}
+
+	for scanner.Scan() {
+		line := scanner.Bytes()
+
+		msg.ID = ""
+		msg.Stream = ""
+		msg.Error = ""
+		msg.ErrorDetail.Message = ""
+		msg.Status = ""
+		msg.Progress = ""
+
+		if err := json.Unmarshal(line, &msg); err != nil {
+			writeLog(logger, false, "Unable to unmarshal line [%s] ==> %v", string(line), err)
+			continue
+		}
+
+		if msg.Error != "" {
+			writeLog(logger, isError, "%s", msg.Error)
+			return errors.New(msg.Error)
+		}
+
+		if msg.ErrorDetail.Message != "" {
+			writeLog(logger, isError, "%s", msg.ErrorDetail.Message)
+			return errors.New(msg.Error)
+		}
+
+		if msg.Status != "" {
+			if msg.Progress != "" {
+				writeLog(logger, isError, "%s :: %s :: %s\n", msg.Status, msg.ID, msg.Progress)
+			} else {
+				writeLog(logger, isError, "%s :: %s\n", msg.Status, msg.ID)
+			}
+		} else if msg.Stream != "" {
+			writeLog(logger, isError, msg.Stream)
+		} else {
+			writeLog(logger, false, "Unable to handle line: %s", string(line))
+		}
+	}
+
+	return nil
+}
+
+func writeLog(logger logrus.FieldLogger, isError bool, format string, args ...interface{}) {
+	if isError {
+		logger.Errorf(format, args...)
+	} else {
+		logger.Debugf(format, args...)
+	}
+}
diff --git a/pkg/container/docker_pull.go b/pkg/container/docker_pull.go
index c324598..4e1e5fe 100644
--- a/pkg/container/docker_pull.go
+++ b/pkg/container/docker_pull.go
@@ -1,40 +1,60 @@
 package container
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/client"
 	"github.com/nektos/act/pkg/common"
+	log "github.com/sirupsen/logrus"
 )
 
 // NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
 type NewDockerPullExecutorInput struct {
-	DockerExecutorInput
-	Image string
+	Image     string
+	ForcePull bool
 }
 
 // NewDockerPullExecutor function to create a run executor for the container
 func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
-	return func() error {
-		input.Logger.Infof("docker pull %v", input.Image)
+	return func(ctx context.Context) error {
+		logger := common.Logger(ctx)
+		logger.Infof("docker pull %v", input.Image)
 
-		if input.Dryrun {
+		if common.Dryrun(ctx) {
+			return nil
+		}
+
+		pull := input.ForcePull
+		if !pull {
+			imageExists, err := ImageExistsLocally(ctx, input.Image)
+			log.Debugf("Image exists? %v", imageExists)
+			if err != nil {
+				return fmt.Errorf("unable to determine if image already exists for image %q", input.Image)
+			}
+
+			if !imageExists {
+				pull = true
+			}
+		}
+
+		if !pull {
 			return nil
 		}
 
 		imageRef := cleanImage(input.Image)
-		input.Logger.Debugf("pulling image '%v'", imageRef)
+		logger.Debugf("pulling image '%v'", imageRef)
 
 		cli, err := client.NewClientWithOpts(client.FromEnv)
 		if err != nil {
 			return err
 		}
-		cli.NegotiateAPIVersion(input.Ctx)
+		cli.NegotiateAPIVersion(ctx)
 
-		reader, err := cli.ImagePull(input.Ctx, imageRef, types.ImagePullOptions{})
-		_ = input.logDockerResponse(reader, err != nil)
+		reader, err := cli.ImagePull(ctx, imageRef, types.ImagePullOptions{})
+		_ = logDockerResponse(logger, reader, err != nil)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go
index 0397ec0..acf7180 100644
--- a/pkg/container/docker_run.go
+++ b/pkg/container/docker_run.go
@@ -10,12 +10,12 @@ import (
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/client"
 	"github.com/nektos/act/pkg/common"
+	"github.com/pkg/errors"
 	"golang.org/x/crypto/ssh/terminal"
 )
 
 // NewDockerRunExecutorInput the input for the NewDockerRunExecutor function
 type NewDockerRunExecutorInput struct {
-	DockerExecutorInput
 	Image           string
 	Entrypoint      []string
 	Cmd             []string
@@ -30,182 +30,198 @@ type NewDockerRunExecutorInput struct {
 
 // NewDockerRunExecutor function to create a run executor for the container
 func NewDockerRunExecutor(input NewDockerRunExecutorInput) common.Executor {
-	return func() error {
+	cr := new(containerReference)
+	cr.input = input
 
-		input.Logger.Infof("docker run image=%s entrypoint=%+q cmd=%+q", input.Image, input.Entrypoint, input.Cmd)
-		if input.Dryrun {
+	return common.
+		NewInfoExecutor("docker run image=%s entrypoint=%+q cmd=%+q", input.Image, input.Entrypoint, input.Cmd).
+		Then(
+			common.NewPipelineExecutor(
+				cr.connect(),
+				cr.find(),
+				cr.remove().IfBool(!input.ReuseContainers),
+				cr.create(),
+				cr.copyContent(),
+				cr.attach(),
+				cr.start(),
+				cr.wait(),
+			).Finally(
+				cr.remove().IfBool(!input.ReuseContainers),
+			).IfNot(common.Dryrun),
+		)
+}
+
+type containerReference struct {
+	input NewDockerRunExecutorInput
+	cli   *client.Client
+	id    string
+}
+
+func (cr *containerReference) connect() common.Executor {
+	return func(ctx context.Context) error {
+		cli, err := client.NewClientWithOpts(client.FromEnv)
+		if err != nil {
+			return errors.WithStack(err)
+		}
+		cli.NegotiateAPIVersion(ctx)
+		cr.cli = cli
+		return nil
+	}
+}
+
+func (cr *containerReference) find() common.Executor {
+	return func(ctx context.Context) error {
+		containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{
+			All: true,
+		})
+		if err != nil {
+			return errors.WithStack(err)
+		}
+
+		for _, container := range containers {
+			for _, name := range container.Names {
+				if name[1:] == cr.input.Name {
+					cr.id = container.ID
+					return nil
+				}
+			}
+		}
+
+		cr.id = ""
+		return nil
+	}
+}
+
+func (cr *containerReference) remove() common.Executor {
+	return func(ctx context.Context) error {
+		if cr.id == "" {
 			return nil
 		}
 
-		cli, err := client.NewClientWithOpts(client.FromEnv)
+		logger := common.Logger(ctx)
+		err := cr.cli.ContainerRemove(context.Background(), cr.id, types.ContainerRemoveOptions{
+			RemoveVolumes: true,
+			Force:         true,
+		})
 		if err != nil {
-			return err
+			return errors.WithStack(err)
 		}
-		cli.NegotiateAPIVersion(input.Ctx)
+		cr.id = ""
 
-		// check if container exists
-		containerID, err := findContainer(input, cli, input.Name)
-		if err != nil {
-			return err
-		}
-
-		// if we have an old container and we aren't reusing, remove it!
-		if !input.ReuseContainers && containerID != "" {
-			input.Logger.Debugf("Found existing container for %s...removing", input.Name)
-			removeContainer(input, cli, containerID)
-			containerID = ""
-		}
-
-		// create a new container if we don't have one to reuse
-		if containerID == "" {
-			containerID, err = createContainer(input, cli)
-			if err != nil {
-				return err
-			}
-		}
-
-		// be sure to cleanup container if we aren't reusing
-		if !input.ReuseContainers {
-			defer removeContainer(input, cli, containerID)
-		}
-
-		executor := common.NewPipelineExecutor(
-			func() error {
-				return copyContentToContainer(input, cli, containerID)
-			}, func() error {
-				return attachContainer(input, cli, containerID)
-			}, func() error {
-				return startContainer(input, cli, containerID)
-			}, func() error {
-				return waitContainer(input, cli, containerID)
-			},
-		)
-		return executor()
-	}
-
-}
-
-func createContainer(input NewDockerRunExecutorInput, cli *client.Client) (string, error) {
-	isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
-
-	config := &container.Config{
-		Image:      input.Image,
-		Cmd:        input.Cmd,
-		Entrypoint: input.Entrypoint,
-		WorkingDir: input.WorkingDir,
-		Env:        input.Env,
-		Tty:        isTerminal,
-	}
-
-	if len(input.Volumes) > 0 {
-		config.Volumes = make(map[string]struct{})
-		for _, vol := range input.Volumes {
-			config.Volumes[vol] = struct{}{}
-		}
-	}
-
-	resp, err := cli.ContainerCreate(input.Ctx, config, &container.HostConfig{
-		Binds: input.Binds,
-	}, nil, input.Name)
-	if err != nil {
-		return "", err
-	}
-	input.Logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image)
-	input.Logger.Debugf("ENV ==> %v", input.Env)
-
-	return resp.ID, nil
-}
-
-func findContainer(input NewDockerRunExecutorInput, cli *client.Client, containerName string) (string, error) {
-	containers, err := cli.ContainerList(input.Ctx, types.ContainerListOptions{
-		All: true,
-	})
-	if err != nil {
-		return "", err
-	}
-
-	for _, container := range containers {
-		for _, name := range container.Names {
-			if name[1:] == containerName {
-				return container.ID, nil
-			}
-		}
-	}
-
-	return "", nil
-}
-
-func removeContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) {
-	err := cli.ContainerRemove(context.Background(), containerID, types.ContainerRemoveOptions{
-		RemoveVolumes: true,
-		Force:         true,
-	})
-	if err != nil {
-		input.Logger.Errorf("%v", err)
-	}
-
-	input.Logger.Debugf("Removed container: %v", containerID)
-}
-
-func copyContentToContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
-	for dstPath, srcReader := range input.Content {
-		input.Logger.Debugf("Extracting content to '%s'", dstPath)
-		err := cli.CopyToContainer(input.Ctx, containerID, dstPath, srcReader, types.CopyToContainerOptions{})
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func attachContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
-	out, err := cli.ContainerAttach(input.Ctx, containerID, types.ContainerAttachOptions{
-		Stream: true,
-		Stdout: true,
-		Stderr: true,
-	})
-	if err != nil {
-		return err
-	}
-	isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
-	if !isTerminal || os.Getenv("NORAW") != "" {
-		go input.logDockerOutput(out.Reader)
-	} else {
-		go input.streamDockerOutput(out.Reader)
-	}
-	return nil
-}
-
-func startContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
-	input.Logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", input.Image, input.Entrypoint, input.Cmd)
-
-	if err := cli.ContainerStart(input.Ctx, containerID, types.ContainerStartOptions{}); err != nil {
-		return err
-	}
-
-	input.Logger.Debugf("Started container: %v", containerID)
-	return nil
-}
-
-func waitContainer(input NewDockerRunExecutorInput, cli *client.Client, containerID string) error {
-	statusCh, errCh := cli.ContainerWait(input.Ctx, containerID, container.WaitConditionNotRunning)
-	var statusCode int64
-	select {
-	case err := <-errCh:
-		if err != nil {
-			return err
-		}
-	case status := <-statusCh:
-		statusCode = status.StatusCode
-	}
-
-	input.Logger.Debugf("Return status: %v", statusCode)
-
-	if statusCode == 0 {
+		logger.Debugf("Removed container: %v", cr.id)
 		return nil
-	} else if statusCode == 78 {
-		return fmt.Errorf("exit with `NEUTRAL`: 78")
 	}
-
-	return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
+}
+
+func (cr *containerReference) create() common.Executor {
+	return func(ctx context.Context) error {
+		if cr.id != "" {
+			return nil
+		}
+		logger := common.Logger(ctx)
+		isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
+
+		input := cr.input
+		config := &container.Config{
+			Image:      input.Image,
+			Cmd:        input.Cmd,
+			Entrypoint: input.Entrypoint,
+			WorkingDir: input.WorkingDir,
+			Env:        input.Env,
+			Tty:        isTerminal,
+		}
+
+		if len(input.Volumes) > 0 {
+			config.Volumes = make(map[string]struct{})
+			for _, vol := range input.Volumes {
+				config.Volumes[vol] = struct{}{}
+			}
+		}
+
+		resp, err := cr.cli.ContainerCreate(ctx, config, &container.HostConfig{
+			Binds: input.Binds,
+		}, nil, input.Name)
+		if err != nil {
+			return errors.WithStack(err)
+		}
+		logger.Debugf("Created container name=%s id=%v from image %v", input.Name, resp.ID, input.Image)
+		logger.Debugf("ENV ==> %v", input.Env)
+
+		cr.id = resp.ID
+		return nil
+	}
+}
+
+func (cr *containerReference) copyContent() common.Executor {
+	return func(ctx context.Context) error {
+		logger := common.Logger(ctx)
+		for dstPath, srcReader := range cr.input.Content {
+			logger.Debugf("Extracting content to '%s'", dstPath)
+			err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, srcReader, types.CopyToContainerOptions{})
+			if err != nil {
+				return errors.WithStack(err)
+			}
+		}
+		return nil
+	}
+}
+
+func (cr *containerReference) attach() common.Executor {
+	return func(ctx context.Context) error {
+		out, err := cr.cli.ContainerAttach(ctx, cr.id, types.ContainerAttachOptions{
+			Stream: true,
+			Stdout: true,
+			Stderr: true,
+		})
+		if err != nil {
+			return errors.WithStack(err)
+		}
+		isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
+		if !isTerminal || os.Getenv("NORAW") != "" {
+			go logDockerOutput(ctx, out.Reader)
+		} else {
+			go streamDockerOutput(ctx, out.Reader)
+		}
+		return nil
+	}
+}
+
+func (cr *containerReference) start() common.Executor {
+	return func(ctx context.Context) error {
+		logger := common.Logger(ctx)
+		logger.Debugf("STARTING image=%s entrypoint=%s cmd=%v", cr.input.Image, cr.input.Entrypoint, cr.input.Cmd)
+
+		if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil {
+			return errors.WithStack(err)
+		}
+
+		logger.Debugf("Started container: %v", cr.id)
+		return nil
+	}
+}
+
+func (cr *containerReference) wait() common.Executor {
+	return func(ctx context.Context) error {
+		logger := common.Logger(ctx)
+		statusCh, errCh := cr.cli.ContainerWait(ctx, cr.id, container.WaitConditionNotRunning)
+		var statusCode int64
+		select {
+		case err := <-errCh:
+			if err != nil {
+				return errors.WithStack(err)
+			}
+		case status := <-statusCh:
+			statusCode = status.StatusCode
+		}
+
+		logger.Debugf("Return status: %v", statusCode)
+
+		if statusCode == 0 {
+			return nil
+		} else if statusCode == 78 {
+			return fmt.Errorf("exit with `NEUTRAL`: 78")
+		}
+
+		return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
+	}
 }
diff --git a/pkg/container/docker_run_test.go b/pkg/container/docker_run_test.go
index 9d92b22..54575c6 100644
--- a/pkg/container/docker_run_test.go
+++ b/pkg/container/docker_run_test.go
@@ -30,27 +30,21 @@ func TestNewDockerRunExecutor(t *testing.T) {
 	logger.SetOutput(buf)
 	logger.SetFormatter(&rawFormatter{})
 
+	ctx := common.WithLogger(context.Background(), logger)
+
 	runner := NewDockerRunExecutor(NewDockerRunExecutorInput{
-		DockerExecutorInput: DockerExecutorInput{
-			Ctx:    context.TODO(),
-			Logger: logrus.NewEntry(logger),
-		},
 		Image: "hello-world",
 	})
 
 	puller := NewDockerPullExecutor(NewDockerPullExecutorInput{
-		DockerExecutorInput: DockerExecutorInput{
-			Ctx:    context.TODO(),
-			Logger: logrus.NewEntry(noopLogger),
-		},
 		Image: "hello-world",
 	})
 
 	pipeline := common.NewPipelineExecutor(puller, runner)
-	err := pipeline()
+	err := pipeline(ctx)
 	assert.NoError(t, err)
 
-	expected := `docker run image=hello-world entrypoint=[] cmd=[]Hello from Docker!`
+	expected := `docker pull hello-worlddocker run image=hello-world entrypoint=[] cmd=[]Hello from Docker!`
 	actual := buf.String()
 	assert.Equal(t, expected, actual[:len(expected)])
 }
diff --git a/pkg/model/planner.go b/pkg/model/planner.go
index 854bf81..47d1ee4 100644
--- a/pkg/model/planner.go
+++ b/pkg/model/planner.go
@@ -1,6 +1,7 @@
 package model
 
 import (
+	"fmt"
 	"io/ioutil"
 	"math"
 	"os"
@@ -33,6 +34,19 @@ type Run struct {
 	JobID    string
 }
 
+func (r *Run) String() string {
+	jobName := r.Job().Name
+	if jobName == "" {
+		jobName = r.JobID
+	}
+	return fmt.Sprintf("%s/%s", r.Workflow.Name, jobName)
+}
+
+// Job returns the job for this Run
+func (r *Run) Job() *Job {
+	return r.Workflow.GetJob(r.JobID)
+}
+
 // NewWorkflowPlanner will load all workflows from a directory
 func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) {
 	log.Debugf("Loading workflows from '%s'", dirname)
@@ -55,6 +69,9 @@ func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) {
 				f.Close()
 				return nil, err
 			}
+			if workflow.Name == "" {
+				workflow.Name = file.Name()
+			}
 			wp.workflows = append(wp.workflows, workflow)
 			f.Close()
 		}
diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go
index 9be0dfa..616867b 100644
--- a/pkg/model/workflow.go
+++ b/pkg/model/workflow.go
@@ -1,7 +1,9 @@
 package model
 
 import (
+	"fmt"
 	"io"
+	"strings"
 
 	"gopkg.in/yaml.v2"
 )
@@ -16,13 +18,26 @@ type Workflow struct {
 
 // Job is the structure of one job in a workflow
 type Job struct {
-	Name           string            `yaml:"name"`
-	Needs          []string          `yaml:"needs"`
-	RunsOn         string            `yaml:"runs-on"`
-	Env            map[string]string `yaml:"env"`
-	If             string            `yaml:"if"`
-	Steps          []*Step           `yaml:"steps"`
-	TimeoutMinutes int64             `yaml:"timeout-minutes"`
+	Name           string                    `yaml:"name"`
+	Needs          []string                  `yaml:"needs"`
+	RunsOn         string                    `yaml:"runs-on"`
+	Env            map[string]string         `yaml:"env"`
+	If             string                    `yaml:"if"`
+	Steps          []*Step                   `yaml:"steps"`
+	TimeoutMinutes int64                     `yaml:"timeout-minutes"`
+	Container      *ContainerSpec            `yaml:"container"`
+	Services       map[string]*ContainerSpec `yaml:"services"`
+}
+
+// ContainerSpec is the specification of the container to use for the job
+type ContainerSpec struct {
+	Image      string            `yaml:"image"`
+	Env        map[string]string `yaml:"env"`
+	Ports      []int             `yaml:"ports"`
+	Volumes    []string          `yaml:"volumes"`
+	Options    string            `yaml:"options"`
+	Entrypoint string
+	Args       string
 }
 
 // Step is the structure of one step in a job
@@ -40,6 +55,19 @@ type Step struct {
 	TimeoutMinutes   int64             `yaml:"timeout-minutes"`
 }
 
+// GetEnv gets the env for a step
+func (s *Step) GetEnv() map[string]string {
+	rtnEnv := make(map[string]string)
+	for k, v := range s.Env {
+		rtnEnv[k] = v
+	}
+	for k, v := range s.With {
+		envKey := fmt.Sprintf("INPUT_%s", strings.ToUpper(k))
+		rtnEnv[envKey] = v
+	}
+	return rtnEnv
+}
+
 // ReadWorkflow returns a list of jobs for a given workflow file reader
 func ReadWorkflow(in io.Reader) (*Workflow, error) {
 	w := new(Workflow)
diff --git a/actions/log.go b/pkg/runner/logger.go
similarity index 60%
rename from actions/log.go
rename to pkg/runner/logger.go
index 7a8fef2..b36b9b9 100644
--- a/actions/log.go
+++ b/pkg/runner/logger.go
@@ -1,4 +1,4 @@
-package actions
+package runner
 
 import (
 	"bytes"
@@ -11,15 +11,6 @@ import (
 	"golang.org/x/crypto/ssh/terminal"
 )
 
-type actionLogFormatter struct {
-}
-
-var formatter *actionLogFormatter
-
-func init() {
-	formatter = new(actionLogFormatter)
-}
-
 const (
 	//nocolor = 0
 	red    = 31
@@ -29,16 +20,20 @@ const (
 	gray   = 37
 )
 
-func newActionLogger(actionName string, dryrun bool) *logrus.Entry {
+// NewJobLogger gets the logger for the Job
+func NewJobLogger(jobName string, dryrun bool) logrus.FieldLogger {
 	logger := logrus.New()
-	logger.SetFormatter(formatter)
+	logger.SetFormatter(new(jobLogFormatter))
 	logger.SetOutput(os.Stdout)
 	logger.SetLevel(logrus.GetLevel())
-	rtn := logger.WithFields(logrus.Fields{"action_name": actionName, "dryrun": dryrun})
+	rtn := logger.WithFields(logrus.Fields{"job_name": jobName, "dryrun": dryrun})
 	return rtn
 }
 
-func (f *actionLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
+type jobLogFormatter struct {
+}
+
+func (f *jobLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
 	b := &bytes.Buffer{}
 
 	if f.isColored(entry) {
@@ -51,7 +46,7 @@ func (f *actionLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
 	return b.Bytes(), nil
 }
 
-func (f *actionLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
+func (f *jobLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry) {
 	var levelColor int
 	switch entry.Level {
 	case logrus.DebugLevel, logrus.TraceLevel:
@@ -65,27 +60,27 @@ func (f *actionLogFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry)
 	}
 
 	entry.Message = strings.TrimSuffix(entry.Message, "\n")
-	actionName := entry.Data["action_name"]
+	jobName := entry.Data["job_name"]
 
 	if entry.Data["dryrun"] == true {
-		fmt.Fprintf(b, "\x1b[%dm*DRYRUN* \x1b[%dm[%s] \x1b[0m%s", green, levelColor, actionName, entry.Message)
+		fmt.Fprintf(b, "\x1b[%dm*DRYRUN* \x1b[%dm[%s] \x1b[0m%s", green, levelColor, jobName, entry.Message)
 	} else {
-		fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s", levelColor, actionName, entry.Message)
+		fmt.Fprintf(b, "\x1b[%dm[%s] \x1b[0m%s", levelColor, jobName, entry.Message)
 	}
 }
 
-func (f *actionLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
+func (f *jobLogFormatter) print(b *bytes.Buffer, entry *logrus.Entry) {
 	entry.Message = strings.TrimSuffix(entry.Message, "\n")
-	actionName := entry.Data["action_name"]
+	jobName := entry.Data["job_name"]
 
 	if entry.Data["dryrun"] == true {
-		fmt.Fprintf(b, "*DRYRUN* [%s] %s", actionName, entry.Message)
+		fmt.Fprintf(b, "*DRYRUN* [%s] %s", jobName, entry.Message)
 	} else {
-		fmt.Fprintf(b, "[%s] %s", actionName, entry.Message)
+		fmt.Fprintf(b, "[%s] %s", jobName, entry.Message)
 	}
 }
 
-func (f *actionLogFormatter) isColored(entry *logrus.Entry) bool {
+func (f *jobLogFormatter) isColored(entry *logrus.Entry) bool {
 
 	isColored := checkIfTerminal(entry.Logger.Out)
 
diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go
new file mode 100644
index 0000000..e00c9c4
--- /dev/null
+++ b/pkg/runner/run_context.go
@@ -0,0 +1,269 @@
+package runner
+
+import (
+	"archive/tar"
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+
+	"github.com/nektos/act/pkg/container"
+
+	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/model"
+	log "github.com/sirupsen/logrus"
+)
+
+// RunContext contains info about current job
+type RunContext struct {
+	Config    *Config
+	Run       *model.Run
+	EventJSON string
+	Env       map[string]string
+	Outputs   map[string]string
+	Tempdir   string
+}
+
+// GetEnv returns the env for the context
+func (rc *RunContext) GetEnv() map[string]string {
+	if rc.Env == nil {
+		rc.Env = mergeMaps(rc.Run.Workflow.Env, rc.Run.Job().Env)
+	}
+	return rc.Env
+}
+
+// StepEnv returns the env for a step
+func (rc *RunContext) StepEnv(step *model.Step) map[string]string {
+	env := make(map[string]string)
+	env["HOME"] = "/github/home"
+	env["GITHUB_WORKFLOW"] = rc.Run.Workflow.Name
+	env["GITHUB_RUN_ID"] = "1"
+	env["GITHUB_RUN_NUMBER"] = "1"
+	env["GITHUB_ACTION"] = step.ID
+	env["GITHUB_ACTOR"] = "nektos/act"
+
+	repoPath := rc.Config.Workdir
+	repo, err := common.FindGithubRepo(repoPath)
+	if err != nil {
+		log.Warningf("unable to get git repo: %v", err)
+	} else {
+		env["GITHUB_REPOSITORY"] = repo
+	}
+	env["GITHUB_EVENT_NAME"] = rc.Config.EventName
+	env["GITHUB_EVENT_PATH"] = "/github/workflow/event.json"
+	env["GITHUB_WORKSPACE"] = "/github/workspace"
+
+	_, rev, err := common.FindGitRevision(repoPath)
+	if err != nil {
+		log.Warningf("unable to get git revision: %v", err)
+	} else {
+		env["GITHUB_SHA"] = rev
+	}
+
+	ref, err := common.FindGitRef(repoPath)
+	if err != nil {
+		log.Warningf("unable to get git ref: %v", err)
+	} else {
+		log.Infof("using github ref: %s", ref)
+		env["GITHUB_REF"] = ref
+	}
+	job := rc.Run.Job()
+	if job.Container != nil {
+		return mergeMaps(rc.GetEnv(), job.Container.Env, step.GetEnv(), env)
+	}
+	return mergeMaps(rc.GetEnv(), step.GetEnv(), env)
+}
+
+// Close cleans up temp dir
+func (rc *RunContext) Close(ctx context.Context) error {
+	return os.RemoveAll(rc.Tempdir)
+}
+
+// Executor returns a pipeline executor for all the steps in the job
+func (rc *RunContext) Executor() common.Executor {
+	steps := make([]common.Executor, 0)
+	steps = append(steps, rc.setupTempDir())
+
+	for _, step := range rc.Run.Job().Steps {
+		containerSpec := new(model.ContainerSpec)
+
+		var stepExecutor common.Executor
+		if step.Run != "" {
+			stepExecutor = common.NewPipelineExecutor(
+				rc.setupContainerSpec(step, containerSpec),
+				rc.pullImage(containerSpec),
+				rc.runContainer(containerSpec),
+			)
+		} else if step.Uses != "" {
+			stepExecutor = common.NewErrorExecutor(fmt.Errorf("Not yet implemented - job:%s step:%+v", rc.Run, step))
+			// clone action repo
+			// read action.yaml
+			// if runs.using == node12, start node12 container and run `main`
+			// if runs.using == docker, pull `image` and run
+			// set inputs as env
+			// caputre output
+		} else {
+			stepExecutor = common.NewErrorExecutor(fmt.Errorf("Unable to determine how to run job:%s step:%+v", rc.Run, step))
+		}
+		steps = append(steps, stepExecutor)
+	}
+	return common.NewPipelineExecutor(steps...).Finally(rc.Close)
+}
+
+func (rc *RunContext) setupContainerSpec(step *model.Step, containerSpec *model.ContainerSpec) common.Executor {
+	return func(ctx context.Context) error {
+		job := rc.Run.Job()
+
+		containerSpec.Env = rc.StepEnv(step)
+
+		if step.Uses != "" {
+			containerSpec.Image = step.Uses
+		} else if job.Container != nil {
+			containerSpec.Image = job.Container.Image
+			containerSpec.Args = step.Run
+			containerSpec.Ports = job.Container.Ports
+			containerSpec.Volumes = job.Container.Volumes
+			containerSpec.Options = job.Container.Options
+		} else if step.Run != "" {
+			containerSpec.Image = platformImage(job.RunsOn)
+			containerSpec.Args = step.Run
+		} else {
+			return fmt.Errorf("Unable to setup container for %s", step)
+		}
+		return nil
+	}
+}
+
+func platformImage(platform string) string {
+	switch platform {
+	case "ubuntu-latest", "ubuntu-18.04":
+		return "ubuntu:18.04"
+	case "ubuntu-16.04":
+		return "ubuntu:16.04"
+	case "windows-latest", "windows-2019", "macos-latest", "macos-10.15":
+		return ""
+	default:
+		return ""
+	}
+}
+
+func mergeMaps(maps ...map[string]string) map[string]string {
+	rtnMap := make(map[string]string)
+	for _, m := range maps {
+		for k, v := range m {
+			rtnMap[k] = v
+		}
+	}
+	return rtnMap
+}
+
+func (rc *RunContext) setupTempDir() common.Executor {
+	return func(ctx context.Context) error {
+		var err error
+		rc.Tempdir, err = ioutil.TempDir("", "act-")
+		return err
+	}
+}
+
+func (rc *RunContext) pullImage(containerSpec *model.ContainerSpec) common.Executor {
+	return func(ctx context.Context) error {
+		return container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{
+			Image:     containerSpec.Image,
+			ForcePull: rc.Config.ForcePull,
+		})(ctx)
+	}
+}
+
+func (rc *RunContext) runContainer(containerSpec *model.ContainerSpec) common.Executor {
+	return func(ctx context.Context) error {
+		ghReader, err := rc.createGithubTarball()
+		if err != nil {
+			return err
+		}
+
+		envList := make([]string, 0)
+		for k, v := range containerSpec.Env {
+			envList = append(envList, fmt.Sprintf("%s=%s", k, v))
+		}
+		var cmd, entrypoint []string
+		if containerSpec.Args != "" {
+			cmd = []string{
+				"/bin/sh",
+				"-c",
+				containerSpec.Args,
+			}
+		}
+		if containerSpec.Entrypoint != "" {
+			entrypoint = strings.Fields(containerSpec.Entrypoint)
+		}
+
+		return container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
+			Cmd:        cmd,
+			Entrypoint: entrypoint,
+			Image:      containerSpec.Image,
+			WorkingDir: "/github/workspace",
+			Env:        envList,
+			Name:       rc.createContainerName(),
+			Binds: []string{
+				fmt.Sprintf("%s:%s", rc.Config.Workdir, "/github/workspace"),
+				fmt.Sprintf("%s:%s", rc.Tempdir, "/github/home"),
+				fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
+			},
+			Content:         map[string]io.Reader{"/github": ghReader},
+			ReuseContainers: rc.Config.ReuseContainers,
+		})(ctx)
+	}
+}
+
+func (rc *RunContext) createGithubTarball() (io.Reader, error) {
+	var buf bytes.Buffer
+	tw := tar.NewWriter(&buf)
+	var files = []struct {
+		Name string
+		Mode int64
+		Body string
+	}{
+		{"workflow/event.json", 0644, rc.EventJSON},
+	}
+	for _, file := range files {
+		log.Debugf("Writing entry to tarball %s len:%d", file.Name, len(rc.EventJSON))
+		hdr := &tar.Header{
+			Name: file.Name,
+			Mode: file.Mode,
+			Size: int64(len(rc.EventJSON)),
+		}
+		if err := tw.WriteHeader(hdr); err != nil {
+			return nil, err
+		}
+		if _, err := tw.Write([]byte(rc.EventJSON)); err != nil {
+			return nil, err
+		}
+	}
+	if err := tw.Close(); err != nil {
+		return nil, err
+	}
+
+	return &buf, nil
+
+}
+
+func (rc *RunContext) createContainerName() string {
+	containerName := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(rc.Run.String(), "-")
+
+	prefix := fmt.Sprintf("%s-", trimToLen(filepath.Base(rc.Config.Workdir), 10))
+	suffix := ""
+	containerName = trimToLen(containerName, 30-(len(prefix)+len(suffix)))
+	return fmt.Sprintf("%s%s%s", prefix, containerName, suffix)
+}
+
+func trimToLen(s string, l int) string {
+	if len(s) > l {
+		return s[:l]
+	}
+	return s
+}
diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go
index c242e67..45f60be 100644
--- a/pkg/runner/runner.go
+++ b/pkg/runner/runner.go
@@ -1,9 +1,7 @@
 package runner
 
 import (
-	"io"
 	"io/ioutil"
-	"os"
 
 	"github.com/nektos/act/pkg/common"
 	"github.com/nektos/act/pkg/model"
@@ -12,18 +10,13 @@ import (
 
 // Runner provides capabilities to run GitHub actions
 type Runner interface {
-	PlanRunner
-	io.Closer
-}
-
-// PlanRunner to run a specific actions
-type PlanRunner interface {
-	RunPlan(plan *model.Plan) error
+	NewPlanExecutor(plan *model.Plan) common.Executor
+	NewRunExecutor(run *model.Run) common.Executor
 }
 
 // Config contains the config for a new runner
 type Config struct {
-	Dryrun          bool   // don't start any of the containers
+	Workdir         string // path to working directory
 	EventName       string // name of event to run
 	EventPath       string // path to JSON file to use for event.json in containers
 	ReuseContainers bool   // reuse containers to maintain state
@@ -32,57 +25,44 @@ type Config struct {
 
 type runnerImpl struct {
 	config    *Config
-	tempDir   string
 	eventJSON string
 }
 
-// NewRunner Creates a new Runner
-func NewRunner(runnerConfig *Config) (Runner, error) {
+// New Creates a new Runner
+func New(runnerConfig *Config) (Runner, error) {
 	runner := &runnerImpl{
 		config: runnerConfig,
 	}
 
-	init := common.NewPipelineExecutor(
-		runner.setupTempDir,
-		runner.setupEvent,
-	)
-
-	return runner, init()
-}
-
-func (runner *runnerImpl) setupTempDir() error {
-	var err error
-	runner.tempDir, err = ioutil.TempDir("", "act-")
-	return err
-}
-
-func (runner *runnerImpl) setupEvent() error {
 	runner.eventJSON = "{}"
-	if runner.config.EventPath != "" {
+	if runnerConfig.EventPath != "" {
 		log.Debugf("Reading event.json from %s", runner.config.EventPath)
 		eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath)
 		if err != nil {
-			return err
+			return nil, err
 		}
 		runner.eventJSON = string(eventJSONBytes)
 	}
-	return nil
+	return runner, nil
 }
 
-func (runner *runnerImpl) RunPlan(plan *model.Plan) error {
+func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
 	pipeline := make([]common.Executor, 0)
 	for _, stage := range plan.Stages {
 		stageExecutor := make([]common.Executor, 0)
 		for _, run := range stage.Runs {
-			stageExecutor = append(stageExecutor, runner.newRunExecutor(run))
+			stageExecutor = append(stageExecutor, runner.NewRunExecutor(run))
 		}
 		pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...))
 	}
 
-	executor := common.NewPipelineExecutor(pipeline...)
-	return executor()
+	return common.NewPipelineExecutor(pipeline...)
 }
 
-func (runner *runnerImpl) Close() error {
-	return os.RemoveAll(runner.tempDir)
+func (runner *runnerImpl) NewRunExecutor(run *model.Run) common.Executor {
+	rc := new(RunContext)
+	rc.Config = runner.config
+	rc.Run = run
+	rc.EventJSON = runner.eventJSON
+	return rc.Executor()
 }