From 8c49ba0cec0bd62f13f3dadd5ae556c7dd571436 Mon Sep 17 00:00:00 2001
From: Casey Lee <cplee@nektos.com>
Date: Tue, 4 Feb 2020 16:38:41 -0800
Subject: [PATCH] initial load of yaml working

---
 .github/workflows/basic.yml                   |   8 +
 actions/action.go                             |   7 +-
 actions/api.go                                |  51 -----
 actions/graph.go                              |  64 ------
 actions/runner.go                             | 160 --------------
 cmd/graph.go                                  |  40 ++++
 cmd/input.go                                  |  40 ++++
 cmd/root.go                                   | 157 +++++---------
 go.mod                                        |  15 +-
 go.sum                                        |  45 ++--
 {common => pkg/common}/draw.go                |   0
 {common => pkg/common}/executor.go            |   0
 {common => pkg/common}/executor_test.go       |   0
 {common => pkg/common}/file.go                |   0
 {common => pkg/common}/git.go                 |   0
 {common => pkg/common}/git_test.go            |   0
 {container => pkg/container}/docker_build.go  |   2 +-
 {container => pkg/container}/docker_common.go |   0
 {container => pkg/container}/docker_images.go |   0
 .../container}/docker_images_test.go          |   0
 {container => pkg/container}/docker_pull.go   |   2 +-
 .../container}/docker_pull_test.go            |   0
 {container => pkg/container}/docker_run.go    |   2 +-
 .../container}/docker_run_test.go             |   7 +-
 pkg/model/planner.go                          | 196 ++++++++++++++++++
 pkg/model/workflow.go                         |  67 ++++++
 pkg/runner/api.go                             |   5 +
 pkg/runner/runner.go                          |  88 ++++++++
 {actions => pkg/runner}/runner_exec.go        |  17 +-
 {actions => pkg/runner}/runner_test.go        |   2 +-
 30 files changed, 560 insertions(+), 415 deletions(-)
 create mode 100644 .github/workflows/basic.yml
 delete mode 100644 actions/api.go
 delete mode 100644 actions/graph.go
 delete mode 100644 actions/runner.go
 create mode 100644 cmd/graph.go
 create mode 100644 cmd/input.go
 rename {common => pkg/common}/draw.go (100%)
 rename {common => pkg/common}/executor.go (100%)
 rename {common => pkg/common}/executor_test.go (100%)
 rename {common => pkg/common}/file.go (100%)
 rename {common => pkg/common}/git.go (100%)
 rename {common => pkg/common}/git_test.go (100%)
 rename {container => pkg/container}/docker_build.go (98%)
 rename {container => pkg/container}/docker_common.go (100%)
 rename {container => pkg/container}/docker_images.go (100%)
 rename {container => pkg/container}/docker_images_test.go (100%)
 rename {container => pkg/container}/docker_pull.go (97%)
 rename {container => pkg/container}/docker_pull_test.go (100%)
 rename {container => pkg/container}/docker_run.go (99%)
 rename {container => pkg/container}/docker_run_test.go (97%)
 create mode 100644 pkg/model/planner.go
 create mode 100644 pkg/model/workflow.go
 create mode 100644 pkg/runner/api.go
 create mode 100644 pkg/runner/runner.go
 rename {actions => pkg/runner}/runner_exec.go (93%)
 rename {actions => pkg/runner}/runner_test.go (99%)

diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml
new file mode 100644
index 0000000..7cdb3b4
--- /dev/null
+++ b/.github/workflows/basic.yml
@@ -0,0 +1,8 @@
+name: basic
+on: push
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - run: echo hello world!
diff --git a/actions/action.go b/actions/action.go
index 2a3136a..4b32916 100644
--- a/actions/action.go
+++ b/actions/action.go
@@ -5,17 +5,18 @@ import (
 	"log"
 	"os"
 
-	"github.com/actions/workflow-parser/model"
 	"github.com/howeyc/gopass"
 )
 
 var secretCache map[string]string
 
 type actionEnvironmentApplier struct {
-	*model.Action
+	*Action
 }
 
-func newActionEnvironmentApplier(action *model.Action) environmentApplier {
+type Action struct{}
+
+func newActionEnvironmentApplier(action *Action) environmentApplier {
 	return &actionEnvironmentApplier{action}
 }
 
diff --git a/actions/api.go b/actions/api.go
deleted file mode 100644
index 0ebdc2d..0000000
--- a/actions/api.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package actions
-
-import (
-	"context"
-	"io"
-)
-
-// Runner provides capabilities to run GitHub actions
-type Runner interface {
-	EventGrapher
-	EventLister
-	EventRunner
-	ActionRunner
-	io.Closer
-}
-
-// EventGrapher to list the actions
-type EventGrapher interface {
-	GraphEvent(eventName string) ([][]string, error)
-}
-
-// EventLister to list the events
-type EventLister interface {
-	ListEvents() []string
-}
-
-// EventRunner to run the actions for a given event
-type EventRunner interface {
-	RunEvent() error
-}
-
-// ActionRunner to run a specific actions
-type ActionRunner interface {
-	RunActions(actionNames ...string) error
-}
-
-// RunnerConfig contains the config for a new runner
-type RunnerConfig struct {
-	Ctx             context.Context // context to use for the run
-	Dryrun          bool            // don't start any of the containers
-	WorkingDir      string          // base directory to use
-	WorkflowPath    string          // path to load main.workflow file, relative to WorkingDir
-	EventName       string          // name of event to run
-	EventPath       string          // path to JSON file to use for event.json in containers, relative to WorkingDir
-	ReuseContainers bool            // reuse containers to maintain state
-	ForcePull       bool            // force pulling of the image, if already present
-}
-
-type environmentApplier interface {
-	applyEnvironment(map[string]string)
-}
diff --git a/actions/graph.go b/actions/graph.go
deleted file mode 100644
index 1e1bbc9..0000000
--- a/actions/graph.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package actions
-
-import (
-	"log"
-
-	"github.com/actions/workflow-parser/model"
-)
-
-// return a pipeline that is run in series.  pipeline is a list of steps to run in parallel
-func newExecutionGraph(workflowConfig *model.Configuration, actionNames ...string) [][]string {
-	// first, build a list of all the necessary actions to run, and their dependencies
-	actionDependencies := make(map[string][]string)
-	for len(actionNames) > 0 {
-		newActionNames := make([]string, 0)
-		for _, aName := range actionNames {
-			// make sure we haven't visited this action yet
-			if _, ok := actionDependencies[aName]; !ok {
-				action := workflowConfig.GetAction(aName)
-				if action != nil {
-					actionDependencies[aName] = action.Needs
-					newActionNames = append(newActionNames, action.Needs...)
-				}
-			}
-		}
-		actionNames = newActionNames
-	}
-
-	// next, build an execution graph
-	graph := make([][]string, 0)
-	for len(actionDependencies) > 0 {
-		stage := make([]string, 0)
-		for aName, aDeps := range actionDependencies {
-			// make sure all deps are in the graph already
-			if listInLists(aDeps, graph...) {
-				stage = append(stage, aName)
-				delete(actionDependencies, aName)
-			}
-		}
-		if len(stage) == 0 {
-			log.Fatalf("Unable to build dependency graph!")
-		}
-		graph = append(graph, stage)
-	}
-
-	return graph
-}
-
-// return true iff all strings in srcList exist in at least one of the searchLists
-func listInLists(srcList []string, searchLists ...[]string) bool {
-	for _, src := range srcList {
-		found := false
-		for _, searchList := range searchLists {
-			for _, search := range searchList {
-				if src == search {
-					found = true
-				}
-			}
-		}
-		if !found {
-			return false
-		}
-	}
-	return true
-}
diff --git a/actions/runner.go b/actions/runner.go
deleted file mode 100644
index 0509747..0000000
--- a/actions/runner.go
+++ /dev/null
@@ -1,160 +0,0 @@
-package actions
-
-import (
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"sort"
-
-	"github.com/actions/workflow-parser/model"
-	"github.com/actions/workflow-parser/parser"
-	"github.com/nektos/act/common"
-	log "github.com/sirupsen/logrus"
-)
-
-type runnerImpl struct {
-	config         *RunnerConfig
-	workflowConfig *model.Configuration
-	tempDir        string
-	eventJSON      string
-}
-
-// NewRunner Creates a new Runner
-func NewRunner(runnerConfig *RunnerConfig) (Runner, error) {
-	runner := &runnerImpl{
-		config: runnerConfig,
-	}
-
-	init := common.NewPipelineExecutor(
-		runner.setupTempDir,
-		runner.setupWorkingDir,
-		runner.setupWorkflows,
-		runner.setupEvent,
-	)
-
-	return runner, init()
-}
-
-func (runner *runnerImpl) setupTempDir() error {
-	var err error
-	runner.tempDir, err = ioutil.TempDir("", "act-")
-	return err
-}
-
-func (runner *runnerImpl) setupWorkingDir() error {
-	var err error
-	runner.config.WorkingDir, err = filepath.Abs(runner.config.WorkingDir)
-	log.Debugf("Setting working dir to %s", runner.config.WorkingDir)
-	return err
-}
-
-func (runner *runnerImpl) setupWorkflows() error {
-	runner.config.WorkflowPath = runner.resolvePath(runner.config.WorkflowPath)
-	log.Debugf("Loading workflow config from %s", runner.config.WorkflowPath)
-	workflowReader, err := os.Open(runner.config.WorkflowPath)
-	if err != nil {
-		return err
-	}
-	defer workflowReader.Close()
-
-	runner.workflowConfig, err = parser.Parse(workflowReader)
-	return err
-}
-
-func (runner *runnerImpl) setupEvent() error {
-	runner.eventJSON = "{}"
-	if runner.config.EventPath != "" {
-		runner.config.EventPath = runner.resolvePath(runner.config.EventPath)
-		log.Debugf("Reading event.json from %s", runner.config.EventPath)
-		eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath)
-		if err != nil {
-			return err
-		}
-		runner.eventJSON = string(eventJSONBytes)
-	}
-	return nil
-}
-
-func (runner *runnerImpl) resolvePath(path string) string {
-	if path == "" {
-		return path
-	}
-	if !filepath.IsAbs(path) {
-		path = filepath.Join(runner.config.WorkingDir, path)
-	}
-	return path
-}
-
-// ListEvents gets all the events in the workflows file
-func (runner *runnerImpl) ListEvents() []string {
-	log.Debugf("Listing all events")
-	events := make([]string, 0)
-	for _, w := range runner.workflowConfig.Workflows {
-		events = append(events, w.On)
-	}
-
-	// sort the list based on depth of dependencies
-	sort.Slice(events, func(i, j int) bool {
-		return events[i] < events[j]
-	})
-
-	return events
-}
-
-// GraphEvent builds an execution path
-func (runner *runnerImpl) GraphEvent(eventName string) ([][]string, error) {
-	log.Debugf("Listing actions for event '%s'", eventName)
-	resolves := runner.resolveEvent(eventName)
-	return newExecutionGraph(runner.workflowConfig, resolves...), nil
-}
-
-// RunAction runs a set of actions in parallel, and their dependencies
-func (runner *runnerImpl) RunActions(actionNames ...string) error {
-	log.Debugf("Running actions %+q", actionNames)
-	graph := newExecutionGraph(runner.workflowConfig, actionNames...)
-
-	pipeline := make([]common.Executor, 0)
-	for _, actions := range graph {
-		stage := make([]common.Executor, 0)
-		for _, actionName := range actions {
-			stage = append(stage, runner.newActionExecutor(actionName))
-		}
-		pipeline = append(pipeline, common.NewParallelExecutor(stage...))
-	}
-
-	executor := common.NewPipelineExecutor(pipeline...)
-	return executor()
-}
-
-// RunEvent runs the actions for a single event
-func (runner *runnerImpl) RunEvent() error {
-	log.Debugf("Running event '%s'", runner.config.EventName)
-	resolves := runner.resolveEvent(runner.config.EventName)
-	log.Debugf("Running actions %s -> %s", runner.config.EventName, resolves)
-	return runner.RunActions(resolves...)
-}
-
-func (runner *runnerImpl) Close() error {
-	return os.RemoveAll(runner.tempDir)
-}
-
-// get list of resolves for an event
-func (runner *runnerImpl) resolveEvent(eventName string) []string {
-	workflows := runner.workflowConfig.GetWorkflows(eventName)
-	resolves := make([]string, 0)
-	for _, workflow := range workflows {
-		for _, resolve := range workflow.Resolves {
-			found := false
-			for _, r := range resolves {
-				if r == resolve {
-					found = true
-					break
-				}
-			}
-			if !found {
-				resolves = append(resolves, resolve)
-			}
-		}
-	}
-	return resolves
-}
diff --git a/cmd/graph.go b/cmd/graph.go
new file mode 100644
index 0000000..e8d0204
--- /dev/null
+++ b/cmd/graph.go
@@ -0,0 +1,40 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/model"
+)
+
+func drawGraph(plan *model.Plan) error {
+
+	drawings := make([]*common.Drawing, 0)
+
+	jobPen := common.NewPen(common.StyleSingleLine, 96)
+	arrowPen := common.NewPen(common.StyleNoLine, 97)
+	for i, stage := range plan.Stages {
+		if i > 0 {
+			drawings = append(drawings, arrowPen.DrawArrow())
+		}
+
+		ids := make([]string, 0)
+		for _, r := range stage.Runs {
+			ids = append(ids, fmt.Sprintf("%s/%s", r.Workflow.Name, r.JobID))
+		}
+		drawings = append(drawings, jobPen.DrawBoxes(ids...))
+	}
+
+	maxWidth := 0
+	for _, d := range drawings {
+		if d.GetWidth() > maxWidth {
+			maxWidth = d.GetWidth()
+		}
+	}
+
+	for _, d := range drawings {
+		d.Draw(os.Stdout, maxWidth)
+	}
+	return nil
+}
diff --git a/cmd/input.go b/cmd/input.go
new file mode 100644
index 0000000..9f6ea5c
--- /dev/null
+++ b/cmd/input.go
@@ -0,0 +1,40 @@
+package cmd
+
+import (
+	"log"
+	"path/filepath"
+)
+
+// Input contains the input for the root command
+type Input struct {
+	workingDir      string
+	workflowsPath   string
+	eventPath       string
+	reuseContainers bool
+	dryrun          bool
+	forcePull       bool
+}
+
+func (i *Input) resolve(path string) string {
+	basedir, err := filepath.Abs(i.workingDir)
+	if err != nil {
+		log.Fatal(err)
+	}
+	if path == "" {
+		return path
+	}
+	if !filepath.IsAbs(path) {
+		path = filepath.Join(basedir, path)
+	}
+	return path
+}
+
+// WorkflowsPath returns path to workflows
+func (i *Input) WorkflowsPath() string {
+	return i.resolve(i.workflowsPath)
+}
+
+// EventPath returns the path to events file
+func (i *Input) EventPath() string {
+	return i.resolve(i.eventPath)
+}
diff --git a/cmd/root.go b/cmd/root.go
index dc6ea16..afe9a73 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -2,13 +2,11 @@ package cmd
 
 import (
 	"context"
-	"fmt"
 	"os"
 	"path/filepath"
 
 	fswatch "github.com/andreaskoch/go-fswatch"
-	"github.com/nektos/act/actions"
-	"github.com/nektos/act/common"
+	"github.com/nektos/act/pkg/model"
 	gitignore "github.com/sabhiram/go-gitignore"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -16,26 +14,26 @@ import (
 
 // Execute is the entry point to running the CLI
 func Execute(ctx context.Context, version string) {
-	runnerConfig := &actions.RunnerConfig{Ctx: ctx}
+	input := new(Input)
 	var rootCmd = &cobra.Command{
 		Use:              "act [event name to run]",
 		Short:            "Run Github actions locally by specifying the event name (e.g. `push`) or an action name directly.",
 		Args:             cobra.MaximumNArgs(1),
-		RunE:             newRunCommand(runnerConfig),
+		RunE:             newRunCommand(ctx, input),
 		PersistentPreRun: setupLogging,
 		Version:          version,
 		SilenceUsage:     true,
 	}
 	rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
-	rootCmd.Flags().BoolP("list", "l", false, "list actions")
-	rootCmd.Flags().StringP("action", "a", "", "run action")
-	rootCmd.Flags().BoolVarP(&runnerConfig.ReuseContainers, "reuse", "r", false, "reuse action containers to maintain state")
-	rootCmd.Flags().StringVarP(&runnerConfig.EventPath, "event", "e", "", "path to event JSON file")
-	rootCmd.Flags().BoolVarP(&runnerConfig.ForcePull, "pull", "p", false, "pull docker image(s) if already present")
+	rootCmd.Flags().BoolP("list", "l", false, "list workflows")
+	rootCmd.Flags().StringP("job", "j", "", "run job")
+	rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "reuse action containers to maintain state")
+	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().BoolP("verbose", "v", false, "verbose output")
-	rootCmd.PersistentFlags().BoolVarP(&runnerConfig.Dryrun, "dryrun", "n", false, "dryrun mode")
-	rootCmd.PersistentFlags().StringVarP(&runnerConfig.WorkflowPath, "file", "f", "./.github/main.workflow", "path to workflow file")
-	rootCmd.PersistentFlags().StringVarP(&runnerConfig.WorkingDir, "directory", "C", ".", "working directory")
+	rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
 	if err := rootCmd.Execute(); err != nil {
 		os.Exit(1)
 	}
@@ -49,67 +47,63 @@ func setupLogging(cmd *cobra.Command, args []string) {
 	}
 }
 
-func newRunCommand(runnerConfig *actions.RunnerConfig) func(*cobra.Command, []string) error {
+func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
 	return func(cmd *cobra.Command, args []string) error {
-		if len(args) > 0 {
-			runnerConfig.EventName = args[0]
-		}
-
-		watch, err := cmd.Flags().GetBool("watch")
+		planner, err := model.NewWorkflowPlanner(input.WorkflowsPath())
 		if err != nil {
 			return err
 		}
-		if watch {
-			return watchAndRun(runnerConfig.Ctx, func() error {
-				return parseAndRun(cmd, runnerConfig)
-			})
+
+		// Determine the event name
+		var eventName string
+		if len(args) > 0 {
+			eventName = args[0]
+		} else {
+			// set default event type if we only have a single workflow in the file.
+			// this way user dont have to specify the event.
+			if events := planner.GetEvents(); len(events) == 1 {
+				log.Debugf("Using detected workflow event: %s", events[0])
+				eventName = events[0]
+			}
 		}
-		return parseAndRun(cmd, runnerConfig)
-	}
-}
 
-func parseAndRun(cmd *cobra.Command, runnerConfig *actions.RunnerConfig) error {
-	// create the runner
-	runner, err := actions.NewRunner(runnerConfig)
-	if err != nil {
-		return err
-	}
-	defer runner.Close()
-
-	// set default event type if we only have a single workflow in the file.
-	// this way user dont have to specify the event.
-	if runnerConfig.EventName == "" {
-		if events := runner.ListEvents(); len(events) == 1 {
-			log.Debugf("Using detected workflow event: %s", events[0])
-			runnerConfig.EventName = events[0]
+		// build the plan for this run
+		var plan *model.Plan
+		if jobID, err := cmd.Flags().GetString("job"); err != nil {
+			return err
+		} else if jobID != "" {
+			log.Debugf("Planning job: %s", jobID)
+			plan = planner.PlanJob(jobID)
+		} else {
+			log.Debugf("Planning event: %s", eventName)
+			plan = planner.PlanEvent(eventName)
 		}
-	}
 
-	// fall back to default event name if we could not detect one.
-	if runnerConfig.EventName == "" {
-		runnerConfig.EventName = "push"
-	}
+		// check if we should just print the graph
+		if list, err := cmd.Flags().GetBool("list"); err != nil {
+			return err
+		} else if list {
+			return drawGraph(plan)
+		}
 
-	// check if we should just print the graph
-	list, err := cmd.Flags().GetBool("list")
-	if err != nil {
-		return err
-	}
-	if list {
-		return drawGraph(runner)
-	}
+		// run the plan
+		// runner, err := runner.New(config)
+		// if err != nil {
+		// 	return err
+		// }
+		// defer runner.Close()
 
-	// check if we are running just a single action
-	actionName, err := cmd.Flags().GetString("action")
-	if err != nil {
-		return err
-	}
-	if actionName != "" {
-		return runner.RunActions(actionName)
-	}
+		// if watch, err := cmd.Flags().GetBool("watch"); err != nil {
+		// 	return err
+		// } else if watch {
+		// 	return watchAndRun(ctx, func() error {
+		// 		return runner.RunPlan(plan)
+		// 	})
+		// }
 
-	// run the event in the RunnerRonfig
-	return runner.RunEvent()
+		// return runner.RunPlan(plan)
+		return nil
+	}
 }
 
 func watchAndRun(ctx context.Context, fn func() error) error {
@@ -155,40 +149,3 @@ func watchAndRun(ctx context.Context, fn func() error) error {
 	folderWatcher.Stop()
 	return err
 }
-
-func drawGraph(runner actions.Runner) error {
-	eventNames := runner.ListEvents()
-	for _, eventName := range eventNames {
-		graph, err := runner.GraphEvent(eventName)
-		if err != nil {
-			return err
-		}
-
-		drawings := make([]*common.Drawing, 0)
-		eventPen := common.NewPen(common.StyleDoubleLine, 91 /*34*/)
-
-		drawings = append(drawings, eventPen.DrawBoxes(fmt.Sprintf("EVENT: %s", eventName)))
-
-		actionPen := common.NewPen(common.StyleSingleLine, 96)
-		arrowPen := common.NewPen(common.StyleNoLine, 97)
-		drawings = append(drawings, arrowPen.DrawArrow())
-		for i, stage := range graph {
-			if i > 0 {
-				drawings = append(drawings, arrowPen.DrawArrow())
-			}
-			drawings = append(drawings, actionPen.DrawBoxes(stage...))
-		}
-
-		maxWidth := 0
-		for _, d := range drawings {
-			if d.GetWidth() > maxWidth {
-				maxWidth = d.GetWidth()
-			}
-		}
-
-		for _, d := range drawings {
-			d.Draw(os.Stdout, maxWidth)
-		}
-	}
-	return nil
-}
diff --git a/go.mod b/go.mod
index 75d3143..5aa814e 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,6 @@ require (
 	github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
 	github.com/Microsoft/go-winio v0.4.11 // indirect
 	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
-	github.com/actions/workflow-parser v1.0.0
 	github.com/andreaskoch/go-fswatch v1.0.0
 	github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
 	github.com/docker/distribution v2.7.1+incompatible // indirect
@@ -14,9 +13,10 @@ 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/hcl v1.0.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
@@ -29,15 +29,10 @@ require (
 	github.com/sirupsen/logrus v1.3.0
 	github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
 	github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
-	github.com/soniakeys/graph v0.0.0 // indirect
 	github.com/spf13/cobra v0.0.3
 	github.com/spf13/pflag v1.0.3 // indirect
 	github.com/stretchr/testify v1.3.0
-	golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613
-	golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect
-	golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
-	golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533 // indirect
-	golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect
+	golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e
 	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
@@ -45,8 +40,10 @@ require (
 	gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect
 	gopkg.in/src-d/go-git-fixtures.v3 v3.3.0 // indirect
 	gopkg.in/src-d/go-git.v4 v4.9.1
-	gopkg.in/yaml.v2 v2.2.2 // indirect
+	gopkg.in/yaml.v2 v2.2.8
 	gotest.tools v2.2.0+incompatible
 )
 
 replace github.com/docker/docker => github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb
+
+go 1.13
diff --git a/go.sum b/go.sum
index 8ff9c36..30170e1 100644
--- a/go.sum
+++ b/go.sum
@@ -5,8 +5,6 @@ github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6
 github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
 github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
-github.com/actions/workflow-parser v1.0.0 h1:Zz2Ke31f3OMYCSzU2pqZSsk/Oz+lWXfEiXMisjxgGcc=
-github.com/actions/workflow-parser v1.0.0/go.mod h1:jz9ZVl8zUIcjMfDQearQjvUHIBhx9l1ys4keDd6be34=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
 github.com/andreaskoch/go-fswatch v1.0.0 h1:la8nP/HiaFCxP2IM6NZNUCoxgLWuyNFgH0RligBbnJU=
@@ -21,8 +19,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
-github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb h1:PyjxRdW1mqCmSoxy/6uP01P7CGbsD+woX+oOWbaUPwQ=
 github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
 github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@@ -49,12 +45,14 @@ 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/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+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=
@@ -79,6 +77,10 @@ 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=
@@ -103,10 +105,6 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PX
 github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
 github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
-github.com/soniakeys/bits v1.0.0 h1:Rune9VFefdJvLE0Q5iRCVGiKdSu2iDihs2I6SCm7evw=
-github.com/soniakeys/bits v1.0.0/go.mod h1:7yJHB//UizrUr64VFneewK6SX5oeCf0SMbDYe2ey1JA=
-github.com/soniakeys/graph v0.0.0 h1:C/Rr8rv9wbhZIsYHcWJFoI84pkipJocMYdRteE+/PQA=
-github.com/soniakeys/graph v0.0.0/go.mod h1:lxpIbor/bIzWUAqvt1Dx92Hr63uWeyuEAbPnsjYbVwM=
 github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
@@ -126,6 +124,9 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1
 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=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -134,25 +135,41 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r
 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/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=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 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=
@@ -161,10 +178,11 @@ google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca/go.mod h1:L3J43x8/
 google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
 google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
 google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE=
 gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo=
@@ -179,8 +197,9 @@ 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.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+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=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/common/draw.go b/pkg/common/draw.go
similarity index 100%
rename from common/draw.go
rename to pkg/common/draw.go
diff --git a/common/executor.go b/pkg/common/executor.go
similarity index 100%
rename from common/executor.go
rename to pkg/common/executor.go
diff --git a/common/executor_test.go b/pkg/common/executor_test.go
similarity index 100%
rename from common/executor_test.go
rename to pkg/common/executor_test.go
diff --git a/common/file.go b/pkg/common/file.go
similarity index 100%
rename from common/file.go
rename to pkg/common/file.go
diff --git a/common/git.go b/pkg/common/git.go
similarity index 100%
rename from common/git.go
rename to pkg/common/git.go
diff --git a/common/git_test.go b/pkg/common/git_test.go
similarity index 100%
rename from common/git_test.go
rename to pkg/common/git_test.go
diff --git a/container/docker_build.go b/pkg/container/docker_build.go
similarity index 98%
rename from container/docker_build.go
rename to pkg/container/docker_build.go
index 8fac5f4..9d3c621 100644
--- a/container/docker_build.go
+++ b/pkg/container/docker_build.go
@@ -10,7 +10,7 @@ import (
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/pkg/archive"
 	"github.com/docker/docker/pkg/fileutils"
-	"github.com/nektos/act/common"
+	"github.com/nektos/act/pkg/common"
 	log "github.com/sirupsen/logrus"
 )
 
diff --git a/container/docker_common.go b/pkg/container/docker_common.go
similarity index 100%
rename from container/docker_common.go
rename to pkg/container/docker_common.go
diff --git a/container/docker_images.go b/pkg/container/docker_images.go
similarity index 100%
rename from container/docker_images.go
rename to pkg/container/docker_images.go
diff --git a/container/docker_images_test.go b/pkg/container/docker_images_test.go
similarity index 100%
rename from container/docker_images_test.go
rename to pkg/container/docker_images_test.go
diff --git a/container/docker_pull.go b/pkg/container/docker_pull.go
similarity index 97%
rename from container/docker_pull.go
rename to pkg/container/docker_pull.go
index 99fcd94..c324598 100644
--- a/container/docker_pull.go
+++ b/pkg/container/docker_pull.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/client"
-	"github.com/nektos/act/common"
+	"github.com/nektos/act/pkg/common"
 )
 
 // NewDockerPullExecutorInput the input for the NewDockerPullExecutor function
diff --git a/container/docker_pull_test.go b/pkg/container/docker_pull_test.go
similarity index 100%
rename from container/docker_pull_test.go
rename to pkg/container/docker_pull_test.go
diff --git a/container/docker_run.go b/pkg/container/docker_run.go
similarity index 99%
rename from container/docker_run.go
rename to pkg/container/docker_run.go
index 246fca2..0397ec0 100644
--- a/container/docker_run.go
+++ b/pkg/container/docker_run.go
@@ -9,7 +9,7 @@ import (
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/client"
-	"github.com/nektos/act/common"
+	"github.com/nektos/act/pkg/common"
 	"golang.org/x/crypto/ssh/terminal"
 )
 
diff --git a/container/docker_run_test.go b/pkg/container/docker_run_test.go
similarity index 97%
rename from container/docker_run_test.go
rename to pkg/container/docker_run_test.go
index a1dfd9c..9d92b22 100644
--- a/container/docker_run_test.go
+++ b/pkg/container/docker_run_test.go
@@ -3,11 +3,12 @@ package container
 import (
 	"bytes"
 	"context"
-	"github.com/nektos/act/common"
-	"github.com/sirupsen/logrus"
-	"github.com/stretchr/testify/assert"
 	"io/ioutil"
 	"testing"
+
+	"github.com/nektos/act/pkg/common"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
 )
 
 type rawFormatter struct{}
diff --git a/pkg/model/planner.go b/pkg/model/planner.go
new file mode 100644
index 0000000..854bf81
--- /dev/null
+++ b/pkg/model/planner.go
@@ -0,0 +1,196 @@
+package model
+
+import (
+	"io/ioutil"
+	"math"
+	"os"
+	"path/filepath"
+	"sort"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// WorkflowPlanner contains methods for creating plans
+type WorkflowPlanner interface {
+	PlanEvent(eventName string) *Plan
+	PlanJob(jobName string) *Plan
+	GetEvents() []string
+}
+
+// Plan contains a list of stages to run in series
+type Plan struct {
+	Stages []*Stage
+}
+
+// Stage contains a list of runs to execute in parallel
+type Stage struct {
+	Runs []*Run
+}
+
+// Run represents a job from a workflow that needs to be run
+type Run struct {
+	Workflow *Workflow
+	JobID    string
+}
+
+// NewWorkflowPlanner will load all workflows from a directory
+func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) {
+	log.Debugf("Loading workflows from '%s'", dirname)
+	files, err := ioutil.ReadDir(dirname)
+	if err != nil {
+		return nil, err
+	}
+
+	wp := new(workflowPlanner)
+	for _, file := range files {
+		ext := filepath.Ext(file.Name())
+		if ext == ".yml" || ext == ".yaml" {
+			f, err := os.Open(filepath.Join(dirname, file.Name()))
+			if err != nil {
+				return nil, err
+			}
+
+			workflow, err := ReadWorkflow(f)
+			if err != nil {
+				f.Close()
+				return nil, err
+			}
+			wp.workflows = append(wp.workflows, workflow)
+			f.Close()
+		}
+	}
+
+	return wp, nil
+}
+
+type workflowPlanner struct {
+	workflows []*Workflow
+}
+
+// PlanEvent builds a new list of runs to execute in parallel for an event name
+func (wp *workflowPlanner) PlanEvent(eventName string) *Plan {
+	plan := new(Plan)
+	for _, w := range wp.workflows {
+		if w.On == eventName {
+			plan.mergeStages(createStages(w, w.GetJobIDs()...))
+		}
+	}
+	return plan
+}
+
+// PlanJob builds a new run to execute in parallel for a job name
+func (wp *workflowPlanner) PlanJob(jobName string) *Plan {
+	plan := new(Plan)
+	for _, w := range wp.workflows {
+		plan.mergeStages(createStages(w, jobName))
+	}
+	return plan
+}
+
+// GetEvents gets all the events in the workflows file
+func (wp *workflowPlanner) GetEvents() []string {
+	events := make([]string, 0)
+	for _, w := range wp.workflows {
+		found := false
+		for _, e := range events {
+			if e == w.On {
+				found = true
+				break
+			}
+		}
+
+		if !found {
+			events = append(events, w.On)
+		}
+	}
+
+	// sort the list based on depth of dependencies
+	sort.Slice(events, func(i, j int) bool {
+		return events[i] < events[j]
+	})
+
+	return events
+}
+
+// GetJobIDs will get all the job names in the stage
+func (s *Stage) GetJobIDs() []string {
+	names := make([]string, 0)
+	for _, r := range s.Runs {
+		names = append(names, r.JobID)
+	}
+	return names
+}
+
+// Merge stages with existing stages in plan
+func (p *Plan) mergeStages(stages []*Stage) {
+	newStages := make([]*Stage, int(math.Max(float64(len(p.Stages)), float64(len(stages)))))
+	for i := 0; i < len(newStages); i++ {
+		newStages[i] = new(Stage)
+		if i >= len(p.Stages) {
+			newStages[i].Runs = append(stages[i].Runs)
+		} else if i >= len(stages) {
+			newStages[i].Runs = append(p.Stages[i].Runs)
+		} else {
+			newStages[i].Runs = append(p.Stages[i].Runs, stages[i].Runs...)
+		}
+	}
+	p.Stages = newStages
+}
+
+func createStages(w *Workflow, jobIDs ...string) []*Stage {
+	// first, build a list of all the necessary jobs to run, and their dependencies
+	jobDependencies := make(map[string][]string)
+	for len(jobIDs) > 0 {
+		newJobIDs := make([]string, 0)
+		for _, jID := range jobIDs {
+			// make sure we haven't visited this job yet
+			if _, ok := jobDependencies[jID]; !ok {
+				if job := w.GetJob(jID); job != nil {
+					jobDependencies[jID] = job.Needs
+					newJobIDs = append(newJobIDs, job.Needs...)
+				}
+			}
+		}
+		jobIDs = newJobIDs
+	}
+
+	// next, build an execution graph
+	stages := make([]*Stage, 0)
+	for len(jobDependencies) > 0 {
+		stage := new(Stage)
+		for jID, jDeps := range jobDependencies {
+			// make sure all deps are in the graph already
+			if listInStages(jDeps, stages...) {
+				stage.Runs = append(stage.Runs, &Run{
+					Workflow: w,
+					JobID:    jID,
+				})
+				delete(jobDependencies, jID)
+			}
+		}
+		if len(stage.Runs) == 0 {
+			log.Fatalf("Unable to build dependency graph!")
+		}
+		stages = append(stages, stage)
+	}
+
+	return stages
+}
+
+// return true iff all strings in srcList exist in at least one of the stages
+func listInStages(srcList []string, stages ...*Stage) bool {
+	for _, src := range srcList {
+		found := false
+		for _, stage := range stages {
+			for _, search := range stage.GetJobIDs() {
+				if src == search {
+					found = true
+				}
+			}
+		}
+		if !found {
+			return false
+		}
+	}
+	return true
+}
diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go
new file mode 100644
index 0000000..9be0dfa
--- /dev/null
+++ b/pkg/model/workflow.go
@@ -0,0 +1,67 @@
+package model
+
+import (
+	"io"
+
+	"gopkg.in/yaml.v2"
+)
+
+// Workflow is the structure of the files in .github/workflows
+type Workflow struct {
+	Name string            `yaml:"name"`
+	On   string            `yaml:"on"`
+	Env  map[string]string `yaml:"env"`
+	Jobs map[string]*Job   `yaml:"jobs"`
+}
+
+// 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"`
+}
+
+// Step is the structure of one step in a job
+type Step struct {
+	ID               string            `yaml:"id"`
+	If               string            `yaml:"if"`
+	Name             string            `yaml:"name"`
+	Uses             string            `yaml:"uses"`
+	Run              string            `yaml:"run"`
+	WorkingDirectory string            `yaml:"working-directory"`
+	Shell            string            `yaml:"shell"`
+	Env              map[string]string `yaml:"env"`
+	With             map[string]string `yaml:"with"`
+	ContinueOnError  bool              `yaml:"continue-on-error"`
+	TimeoutMinutes   int64             `yaml:"timeout-minutes"`
+}
+
+// ReadWorkflow returns a list of jobs for a given workflow file reader
+func ReadWorkflow(in io.Reader) (*Workflow, error) {
+	w := new(Workflow)
+	err := yaml.NewDecoder(in).Decode(w)
+	return w, err
+}
+
+// GetJob will get a job by name in the workflow
+func (w *Workflow) GetJob(jobID string) *Job {
+	for id, j := range w.Jobs {
+		if jobID == id {
+			return j
+		}
+	}
+	return nil
+}
+
+// GetJobIDs will get all the job names in the workflow
+func (w *Workflow) GetJobIDs() []string {
+	ids := make([]string, 0)
+	for id := range w.Jobs {
+		ids = append(ids, id)
+	}
+	return ids
+}
diff --git a/pkg/runner/api.go b/pkg/runner/api.go
new file mode 100644
index 0000000..b9f342b
--- /dev/null
+++ b/pkg/runner/api.go
@@ -0,0 +1,5 @@
+package runner
+
+type environmentApplier interface {
+	applyEnvironment(map[string]string)
+}
diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go
new file mode 100644
index 0000000..c242e67
--- /dev/null
+++ b/pkg/runner/runner.go
@@ -0,0 +1,88 @@
+package runner
+
+import (
+	"io"
+	"io/ioutil"
+	"os"
+
+	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/model"
+	log "github.com/sirupsen/logrus"
+)
+
+// 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
+}
+
+// Config contains the config for a new runner
+type Config struct {
+	Dryrun          bool   // don't start any of the containers
+	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
+	ForcePull       bool   // force pulling of the image, if already present
+}
+
+type runnerImpl struct {
+	config    *Config
+	tempDir   string
+	eventJSON string
+}
+
+// NewRunner Creates a new Runner
+func NewRunner(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 != "" {
+		log.Debugf("Reading event.json from %s", runner.config.EventPath)
+		eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath)
+		if err != nil {
+			return err
+		}
+		runner.eventJSON = string(eventJSONBytes)
+	}
+	return nil
+}
+
+func (runner *runnerImpl) RunPlan(plan *model.Plan) error {
+	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))
+		}
+		pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...))
+	}
+
+	executor := common.NewPipelineExecutor(pipeline...)
+	return executor()
+}
+
+func (runner *runnerImpl) Close() error {
+	return os.RemoveAll(runner.tempDir)
+}
diff --git a/actions/runner_exec.go b/pkg/runner/runner_exec.go
similarity index 93%
rename from actions/runner_exec.go
rename to pkg/runner/runner_exec.go
index aec5151..0a52fab 100644
--- a/actions/runner_exec.go
+++ b/pkg/runner/runner_exec.go
@@ -1,21 +1,20 @@
-package actions
+package runner
 
 import (
 	"archive/tar"
 	"bytes"
 	"fmt"
 	"io"
-	"os"
 	"path/filepath"
 	"regexp"
 
-	"github.com/actions/workflow-parser/model"
-	"github.com/nektos/act/common"
-	"github.com/nektos/act/container"
+	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/container"
+	"github.com/nektos/act/pkg/model"
 	log "github.com/sirupsen/logrus"
 )
 
-func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
+func (runner *runnerImpl) newRunExecutor(run *model.Run) common.Executor {
 	action := runner.workflowConfig.GetAction(actionName)
 	if action == nil {
 		return common.NewErrorExecutor(fmt.Errorf("Unable to find action named '%s'", actionName))
@@ -35,7 +34,8 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
 	return common.NewPipelineExecutor(executors...)
 }
 
-func (runner *runnerImpl) addImageExecutor(action *model.Action, executors *[]common.Executor) (string, error) {
+/*
+func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.Executor) (string, error) {
 	var image string
 	logger := newActionLogger(action.Identifier, runner.config.Dryrun)
 	log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier)
@@ -111,8 +111,9 @@ func (runner *runnerImpl) addImageExecutor(action *model.Action, executors *[]co
 
 	return image, nil
 }
+*/
 
-func (runner *runnerImpl) addRunExecutor(action *model.Action, image string, executors *[]common.Executor) error {
+func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors *[]common.Executor) error {
 	logger := newActionLogger(action.Identifier, runner.config.Dryrun)
 	log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier)
 
diff --git a/actions/runner_test.go b/pkg/runner/runner_test.go
similarity index 99%
rename from actions/runner_test.go
rename to pkg/runner/runner_test.go
index cd96aae..5ca1d0f 100644
--- a/actions/runner_test.go
+++ b/pkg/runner/runner_test.go
@@ -1,4 +1,4 @@
-package actions
+package runner
 
 import (
 	"context"