Repository: diggerhq/digger
Pull request #91 (2023-03-31 14:01:53)

feat: support workflows and extra args to terraform executions


Patch:
diff --git a/cmd/digger/main.go b/cmd/digger/main.go
index 20b9f3ce..fbaad7d4 100644
--- a/cmd/digger/main.go
+++ b/cmd/digger/main.go
@@ -67,7 +67,7 @@ func main() {
 		githubPrService.PublishComment(prNumber, reply)
 	}
 
-	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, diggerConfig.Workflows)
 	if err != nil {
 		reportErrorAndExit(repoOwner, fmt.Sprintf("Failed to convert GitHub event to commands. %s", err), 7)
 	}
diff --git a/pkg/digger/digger.go b/pkg/digger/digger.go
index ae8fd5bc..33308ac0 100644
--- a/pkg/digger/digger.go
+++ b/pkg/digger/digger.go
@@ -66,6 +66,8 @@ func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string
 				projectCommands.ProjectDir,
 				repoName,
 				projectCommands.Terragrunt,
+				projectCommands.ApplyStage,
+				projectCommands.PlanStage,
 				prManager,
 				projectLock,
 				diggerConfig,
@@ -108,22 +110,30 @@ type ProjectCommand struct {
 	ProjectWorkspace string
 	Terragrunt       bool
 	Commands         []string
+	ApplyStage       Stage
+	PlanStage        Stage
 }
 
-func ConvertGithubEventToCommands(event models.Event, impactedProjects []Project) ([]ProjectCommand, error) {
+func ConvertGithubEventToCommands(event models.Event, impactedProjects []Project, workflows map[string]Workflow) ([]ProjectCommand, error) {
 	commandsPerProject := make([]ProjectCommand, 0)
 
 	switch event.(type) {
 	case models.PullRequestEvent:
 		event := event.(models.PullRequestEvent)
 		for _, project := range impactedProjects {
+			workflow, ok := workflows[project.Workflow]
+			if !ok {
+				workflow = *defaultWorkflow()
+			}
 			if event.Action == "closed" && event.PullRequest.Merged && event.PullRequest.Base.Ref == event.Repository.DefaultBranch {
 				commandsPerProject = append(commandsPerProject, ProjectCommand{
 					ProjectName:      project.Name,
 					ProjectDir:       project.Dir,
 					ProjectWorkspace: project.Workspace,
 					Terragrunt:       project.Terragrunt,
-					Commands:         project.WorkflowConfiguration.OnCommitToDefault,
+					Commands:         workflow.Configuration.OnCommitToDefault,
+					ApplyStage:       *workflow.Apply,
+					PlanStage:        *workflow.Plan,
 				})
 			} else if event.Action == "opened" || event.Action == "reopened" || event.Action == "synchronize" {
 				commandsPerProject = append(commandsPerProject, ProjectCommand{
@@ -131,7 +141,9 @@ func ConvertGithubEventToCommands(event models.Event, impactedProjects []Project
 					ProjectDir:       project.Dir,
 					ProjectWorkspace: project.Workspace,
 					Terragrunt:       project.Terragrunt,
-					Commands:         project.WorkflowConfiguration.OnPullRequestPushed,
+					Commands:         workflow.Configuration.OnPullRequestPushed,
+					ApplyStage:       *workflow.Apply,
+					PlanStage:        *workflow.Plan,
 				})
 			} else if event.Action == "closed" {
 				commandsPerProject = append(commandsPerProject, ProjectCommand{
@@ -139,7 +151,9 @@ func ConvertGithubEventToCommands(event models.Event, impactedProjects []Project
 					ProjectDir:       project.Dir,
 					ProjectWorkspace: project.Workspace,
 					Terragrunt:       project.Terragrunt,
-					Commands:         project.WorkflowConfiguration.OnPullRequestClosed,
+					Commands:         workflow.Configuration.OnPullRequestPushed,
+					ApplyStage:       *workflow.Apply,
+					PlanStage:        *workflow.Plan,
 				})
 			}
 		}
@@ -151,6 +165,10 @@ func ConvertGithubEventToCommands(event models.Event, impactedProjects []Project
 		for _, command := range supportedCommands {
 			if strings.Contains(event.Comment.Body, command) {
 				for _, project := range impactedProjects {
+					workflow, ok := workflows[project.Workflow]
+					if !ok {
+						workflow = *defaultWorkflow()
+					}
 					workspace := project.Workspace
 					workspaceOverride, err := parseWorkspace(event.Comment.Body)
 					if err != nil {
@@ -165,6 +183,8 @@ func ConvertGithubEventToCommands(event models.Event, impactedProjects []Project
 						ProjectWorkspace: workspace,
 						Terragrunt:       project.Terragrunt,
 						Commands:         []string{command},
+						ApplyStage:       *workflow.Apply,
+						PlanStage:        *workflow.Plan,
 					})
 				}
 			}
@@ -211,6 +231,8 @@ type DiggerExecutor struct {
 	projectDir   string
 	repoName     string
 	terragrunt   bool
+	applyStage   Stage
+	planStage    Stage
 	prManager    github.PullRequestManager
 	lock         utils.ProjectLock
 	configDigger *DiggerConfig
@@ -235,7 +257,18 @@ func (d DiggerExecutor) Plan(prNumber int) {
 		log.Fatalf("Error locking project: %v", err)
 	}
 	if res {
-		isNonEmptyPlan, stdout, stderr, err := terraformExecutor.Plan()
+		var initArgs []string
+		var planArgs []string
+
+		for _, step := range d.planStage.Steps {
+			if step.Action == "init" {
+				initArgs = append(initArgs, step.ExtraArgs...)
+			}
+			if step.Action == "plan" {
+				planArgs = append(planArgs, step.ExtraArgs...)
+			}
+		}
+		isNonEmptyPlan, stdout, stderr, err := terraformExecutor.Plan(initArgs, planArgs)
 
 		if err != nil {
 			log.Fatalf("Error executing plan: %v", err)
@@ -257,7 +290,18 @@ func (d DiggerExecutor) Apply(prNumber int) {
 	}
 
 	if res, _ := d.lock.Lock(d.LockId(), prNumber); res {
-		stdout, stderr, err := terraformExecutor.Apply()
+		var initArgs []string
+		var applyArgs []string
+
+		for _, step := range d.applyStage.Steps {
+			if step.Action == "init" {
+				initArgs = append(initArgs, step.ExtraArgs...)
+			}
+			if step.Action == "apply" {
+				applyArgs = append(applyArgs, step.ExtraArgs...)
+			}
+		}
+		stdout, stderr, err := terraformExecutor.Apply(initArgs, applyArgs)
 		applyOutput := cleanupTerraformApply(true, err, stdout, stderr)
 		comment := "Apply for **" + d.LockId() + "**\n" + applyOutput
 		d.prManager.PublishComment(prNumber, comment)
@@ -267,7 +311,7 @@ func (d DiggerExecutor) Apply(prNumber int) {
 				fmt.Errorf("error unlocking project: %v", err)
 			}
 		} else {
-			
+
 			d.prManager.PublishComment(prNumber, "Error during applying. Project lock will persist")
 		}
 	}
@@ -348,3 +392,33 @@ func CheckIfHelpComment(event models.Event) bool {
 	}
 	return false
 }
+
+func defaultWorkflow() *Workflow {
+	return &Workflow{
+		Configuration: &WorkflowConfiguration{
+			OnCommitToDefault:   []string{"digger unlock"},
+			OnPullRequestPushed: []string{"digger plan"},
+			OnPullRequestClosed: []string{"digger unlock"},
+		},
+		Plan: &Stage{
+			Steps: []Step{
+				{
+					Action: "init", ExtraArgs: []string{},
+				},
+				{
+					Action: "plan", ExtraArgs: []string{},
+				},
+			},
+		},
+		Apply: &Stage{
+			Steps: []Step{
+				{
+					Action: "init", ExtraArgs: []string{},
+				},
+				{
+					Action: "apply", ExtraArgs: []string{},
+				},
+			},
+		},
+	}
+}
diff --git a/pkg/digger/digger_config.go b/pkg/digger/digger_config.go
index 91fae669..5c347df1 100644
--- a/pkg/digger/digger_config.go
+++ b/pkg/digger/digger_config.go
@@ -3,12 +3,11 @@ package digger
 import (
 	"errors"
 	"fmt"
+	"gopkg.in/yaml.v3"
 	"os"
 	"path"
 	"path/filepath"
 	"strings"
-
-	"gopkg.in/yaml.v2"
 )
 
 type WorkflowConfiguration struct {
@@ -18,15 +17,26 @@ type WorkflowConfiguration struct {
 }
 
 type DiggerConfig struct {
-	Projects []Project `yaml:"projects"`
+	Projects  []Project           `yaml:"projects"`
+	Workflows map[string]Workflow `yaml:"workflows"`
 }
 
 type Project struct {
-	Name                  string                `yaml:"name"`
-	Dir                   string                `yaml:"dir"`
-	Workspace             string                `yaml:"workspace"`
-	Terragrunt            bool                  `yaml:"terragrunt"`
-	WorkflowConfiguration WorkflowConfiguration `yaml:"workflow_configuration"`
+	Name       string `yaml:"name"`
+	Dir        string `yaml:"dir"`
+	Workspace  string `yaml:"workspace"`
+	Terragrunt bool   `yaml:"terragrunt"`
+	Workflow   string `yaml:"workflow"`
+}
+
+type Stage struct {
+	Steps []Step `yaml:"steps"`
+}
+
+type Workflow struct {
+	Plan          *Stage                 `yaml:"plan,omitempty"`
+	Apply         *Stage                 `yaml:"apply,omitempty"`
+	Configuration *WorkflowConfiguration `yaml:"workflow_configuration"`
 }
 
 var ErrDiggerConfigConflict = errors.New("more than one digger config file detected, please keep either 'digger.yml' or 'digger.yaml'")
@@ -36,11 +46,7 @@ func (p *Project) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	raw := rawProject{
 		Workspace:  "default",
 		Terragrunt: false,
-		WorkflowConfiguration: WorkflowConfiguration{
-			OnPullRequestPushed: []string{"digger plan"},
-			OnPullRequestClosed: []string{"digger unlock"},
-			OnCommitToDefault:   []string{"digger apply"},
-		},
+		Workflow:   "default",
 	}
 	if err := unmarshal(&raw); err != nil {
 		return err
@@ -50,6 +56,28 @@ func (p *Project) UnmarshalYAML(unmarshal func(interface{}) error) error {
 
 }
 
+type Step struct {
+	Action    string
+	ExtraArgs []string `yaml:"extra_args,omitempty"`
+}
+
+func (s *Step) UnmarshalYAML(value *yaml.Node) error {
+	if value.Kind == yaml.ScalarNode {
+		return value.Decode(&s.Action)
+	}
+
+	var stepMap map[string]map[string][]string
+	if err := value.Decode(&stepMap); err != nil {
+		return err
+	}
+
+	for k, v := range stepMap {
+		s.Action = k
+		s.ExtraArgs = v["extra_args"]
+		break
+	}
+	return nil
+}
 func NewDiggerConfig(workingDir string) (*DiggerConfig, error) {
 	config := &DiggerConfig{}
 	fileName, err := retrieveConfigFile(workingDir)
@@ -63,6 +91,18 @@ func NewDiggerConfig(workingDir string) (*DiggerConfig, error) {
 	if err != nil {
 		config.Projects = make([]Project, 1)
 		config.Projects[0] = defaultProject()
+		config.Workflows = make(map[string]Workflow)
+		config.Workflows["default"] = Workflow{
+			Plan: &Stage{
+				Steps: []Step{{
+					Action:    "init",
+					ExtraArgs: []string{},
+				}, {
+					"plan",
+					[]string{},
+				}},
+			},
+		}
 		return config, nil
 	}
 
@@ -79,11 +119,8 @@ func defaultProject() Project {
 		Dir:        ".",
 		Workspace:  "default",
 		Terragrunt: false,
-		WorkflowConfiguration: WorkflowConfiguration{
-			OnPullRequestPushed: []string{"digger plan"},
-			OnPullRequestClosed: []string{"digger unlock"},
-			OnCommitToDefault:   []string{"digger apply"},
-		}}
+		Workflow:   "default",
+	}
 }
 
 func (c *DiggerConfig) GetProject(projectName string) *Project {
@@ -129,12 +166,29 @@ func (c *DiggerConfig) GetDirectory(projectName string) string {
 	return project.Dir
 }
 
+func (c *DiggerConfig) GetWorkflow(workflowName string) *Workflow {
+	workflows := c.Workflows
+
+	workflow, ok := workflows[workflowName]
+	if !ok {
+		return nil
+	}
+	return &workflow
+
+}
+
 func (c *DiggerConfig) GetWorkflowConfiguration(projectName string) WorkflowConfiguration {
 	project := c.GetProject(projectName)
+	workflows := c.Workflows
 	if project == nil {
 		return WorkflowConfiguration{}
 	}
-	return project.WorkflowConfiguration
+	workflow, ok := workflows[project.Workflow]
+
+	if !ok {
+		return WorkflowConfiguration{}
+	}
+	return *workflow.Configuration
 }
 
 type File struct {
diff --git a/pkg/terraform/tf.go b/pkg/terraform/tf.go
index dea39374..d71b30ad 100644
--- a/pkg/terraform/tf.go
+++ b/pkg/terraform/tf.go
@@ -2,18 +2,16 @@ package terraform
 
 import (
 	"bytes"
-	"context"
 	"fmt"
-	"github.com/hashicorp/terraform-exec/tfexec"
 	"io"
-	"log"
 	"os"
 	"os/exec"
+	"strings"
 )
 
 type TerraformExecutor interface {
-	Apply() (string, string, error)
-	Plan() (bool, string, string, error)
+	Apply([]string, []string) (string, string, error)
+	Plan([]string, []string) (bool, string, string, error)
 }
 
 type Terragrunt struct {
@@ -25,17 +23,33 @@ type Terraform struct {
 	Workspace  string
 }
 
-func (terragrunt Terragrunt) Apply() (string, string, error) {
-	return terragrunt.runTerragruntCommand("apply")
+func (terragrunt Terragrunt) Apply(initParams []string, applyParams []string) (string, string, error) {
+	stdout, stderr, err := terragrunt.runTerragruntCommand("init", initParams...)
+	if err != nil {
+		return stdout, stderr, err
+	}
+	applyParams = append(applyParams, "--auto-approve")
+	applyParams = append(applyParams, "--terragrunt-non-interactive")
+	stdout, stderr, err = terragrunt.runTerragruntCommand("apply", applyParams...)
+	return stdout, stderr, err
 }
 
-func (terragrunt Terragrunt) Plan() (bool, string, string, error) {
-	stdout, stderr, err := terragrunt.runTerragruntCommand("plan")
+func (terragrunt Terragrunt) Plan(initParams []string, planParams []string) (bool, string, string, error) {
+	stdout, stderr, err := terragrunt.runTerragruntCommand("init", initParams...)
+	if err != nil {
+		return false, stdout, stderr, err
+	}
+
+	stdout, stderr, err = terragrunt.runTerragruntCommand("plan", planParams...)
 	return true, stdout, stderr, err
 }
 
-func (terragrunt Terragrunt) runTerragruntCommand(command string) (string, string, error) {
-	cmd := exec.Command("terragrunt", command, "--terragrunt-working-dir", terragrunt.WorkingDir, "--terragrunt-non-interactive", "--auto-approve")
+func (terragrunt Terragrunt) runTerragruntCommand(command string, arg ...string) (string, string, error) {
+	args := []string{command}
+	args = append(args, arg...)
+	args = append(args, "--terragrunt-working-dir", terragrunt.WorkingDir)
+	cmd := exec.Command("terragrunt", args...)
+
 	env := os.Environ()
 	env = append(env, "TF_CLI_ARGS=-no-color")
 	env = append(env, "TF_IN_AUTOMATION=true")
@@ -55,48 +69,54 @@ func (terragrunt Terragrunt) runTerragruntCommand(command string) (string, strin
 	return stdout.String(), stderr.String(), err
 }
 
-func (terraform Terraform) Apply() (string, string, error) {
-	println("digger apply")
-	execDir := "terraform"
-	tf, err := tfexec.NewTerraform(terraform.WorkingDir, execDir)
+func (tf Terraform) Apply(initParams []string, applyParams []string) (string, string, error) {
+	initParams = append(append(append(initParams, "-upgrade=true"), "-input=false"), "-no-color")
+	_, _, _, err := tf.runTerraformCommand("init", initParams...)
 	if err != nil {
-		return "", "", fmt.Errorf("error while initializing terraform: %s", err)
+		return "", "", err
 	}
+	workspace, _, _, err := tf.runTerraformCommand("workspace", "show")
 
-	stdout := &StdWriter{[]byte{}, true}
-	//stderr := &StdWriter{[]byte{}, true}
-	tf.SetStdout(stdout)
-	//tf.SetStderr(stderr)
-	tf.SetStderr(os.Stderr)
-
-	err = tf.Init(context.Background(), tfexec.Upgrade(false))
 	if err != nil {
-		println("terraform init failed.")
-		return stdout.GetString(), "", fmt.Errorf("terraform init failed. %s", err)
+		return "", "", err
 	}
-	currentWorkspace, err := tf.WorkspaceShow(context.Background())
 
+	if strings.TrimSpace(workspace) != tf.Workspace {
+		_, _, _, err = tf.runTerraformCommand("workspace", "new", tf.Workspace)
+		if err != nil {
+			return "", "", err
+		}
+	}
+	applyParams = append(append(append(applyParams, "-input=false"), "-no-color"), "-auto-approve")
+	stdout, stderr, _, err := tf.runTerraformCommand("apply", applyParams...)
 	if err != nil {
-		log.Printf("terraform workspace show failed. workspace: %v . dir: %v", terraform.Workspace, terraform.WorkingDir)
-		return stdout.GetString(), "", fmt.Errorf("terraform show failed. %s", err)
+		return "", "", err
 	}
+	return stdout, stderr, nil
+}
 
-	if currentWorkspace != terraform.Workspace {
-		err = tf.WorkspaceNew(context.Background(), terraform.Workspace)
+func (tf Terraform) runTerraformCommand(command string, arg ...string) (string, string, int, error) {
+	args := []string{"-chdir=" + tf.WorkingDir}
+	args = append(args, command)
+	args = append(args, arg...)
+	fmt.Printf("Running terraform %v", args)
 
-		if err != nil {
-			log.Printf("terraform workspace new failed. workspace: %v . dir: %v", terraform.Workspace, terraform.WorkingDir)
-			return stdout.GetString(), "", fmt.Errorf("terraform select failed. %s", err)
-		}
-	}
+	var stdout, stderr bytes.Buffer
+	mwout := io.MultiWriter(os.Stdout, &stdout)
+	mwerr := io.MultiWriter(os.Stderr, &stderr)
+
+	cmd := exec.Command("terraform", args...)
+
+	cmd.Stdout = mwout
+	cmd.Stderr = mwerr
+
+	err := cmd.Run()
 
-	err = tf.Apply(context.Background())
 	if err != nil {
-		println("terraform plan failed.")
-		return stdout.GetString(), "", fmt.Errorf("terraform plan failed. %s", err)
+		fmt.Println("Error:", err)
 	}
 
-	return stdout.GetString(), "", nil
+	return stdout.String(), stderr.String(), cmd.ProcessState.ExitCode(), err
 }
 
 type StdWriter struct {
@@ -119,45 +139,27 @@ func (sw *StdWriter) GetString() string {
 	return s
 }
 
-func (terraform Terraform) Plan() (bool, string, string, error) {
-	execDir := "terraform"
-	tf, err := tfexec.NewTerraform(terraform.WorkingDir, execDir)
-
+func (tf Terraform) Plan(initParams []string, planParams []string) (bool, string, string, error) {
+	initParams = append(append(append(initParams, "-upgrade=true"), "-input=false"), "-no-color")
+	_, _, _, err := tf.runTerraformCommand("init", initParams...)
 	if err != nil {
-		println("Error while initializing terraform: " + err.Error())
-		os.Exit(1)
+		return false, "", "", err
 	}
-	stdout := &StdWriter{[]byte{}, true}
-	stderr := &StdWriter{[]byte{}, true}
-	tf.SetStdout(stdout)
-	tf.SetStderr(stderr)
+	workspace, _, _, err := tf.runTerraformCommand("workspace", "show")
 
-	err = tf.Init(context.Background(), tfexec.Upgrade(true))
 	if err != nil {
-		println("terraform init failed.")
-		return false, stdout.GetString(), stderr.GetString(), fmt.Errorf("terraform init failed. %s", err)
+		return false, "", "", err
 	}
-	currentWorkspace, err := tf.WorkspaceShow(context.Background())
-
-	if err != nil {
-		log.Printf("terraform workspace show failed. workspace: %v . dir: %v", terraform.Workspace, terraform.WorkingDir)
-		return false, stdout.GetString(), "", fmt.Errorf("terraform show failed. %s", err)
-	}
-
-	if currentWorkspace != terraform.Workspace {
-		err = tf.WorkspaceNew(context.Background(), terraform.Workspace)
-
+	if strings.TrimSpace(workspace) != tf.Workspace {
+		_, _, _, err = tf.runTerraformCommand("workspace", "new", tf.Workspace)
 		if err != nil {
-			log.Printf("terraform workspace new failed. workspace: %v . dir: %v", terraform.Workspace, terraform.WorkingDir)
-			return false, stdout.GetString(), "", fmt.Errorf("terraform select failed. %s", err)
+			return false, "", "", err
 		}
 	}
-
-	isNonEmptyPlan, err := tf.Plan(context.Background())
+	planParams = append(append(append(planParams, "-input=false"), "-no-color"), "-out=tfplan")
+	stdout, stderr, statusCode, err := tf.runTerraformCommand("plan", planParams...)
 	if err != nil {
-		println("terraform plan failed. dir: " + terraform.WorkingDir)
-		return isNonEmptyPlan, stdout.GetString(), stderr.GetString(), fmt.Errorf("terraform plan failed. %s", err)
+		return false, "", "", err
 	}
-
-	return isNonEmptyPlan, stdout.GetString(), stderr.GetString(), nil
+	return statusCode != 2, stdout, stderr, nil
 }


Test patch:
diff --git a/cmd/digger/main_test.go b/cmd/digger/main_test.go
index bfdfabac..48f82d30 100644
--- a/cmd/digger/main_test.go
+++ b/cmd/digger/main_test.go
@@ -873,7 +873,7 @@ func TestGitHubNewPullRequestContext(t *testing.T) {
 	lock := &utils.MockLock{}
 	prManager := &utils.MockPullRequestManager{ChangedFiles: []string{"dev/test.tf"}}
 	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
-	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, &diggerConfig, prManager, lock, "")
 	assert.NoError(t, err)
 	if err != nil {
@@ -893,7 +893,7 @@ func TestGitHubNewCommentContext(t *testing.T) {
 	lock := &utils.MockLock{}
 	prManager := &utils.MockPullRequestManager{ChangedFiles: []string{"dev/test.tf"}}
 	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
-	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, &diggerConfig, prManager, lock, "")
 	assert.NoError(t, err)
 	if err != nil {
@@ -914,17 +914,40 @@ func TestGitHubNewPullRequestInMultiEnvProjectContext(t *testing.T) {
 	assert.NoError(t, err)
 	ghEvent := context.Event
 	pullRequestNumber := 11
-	// digger config
-	dev := digger.Project{Name: "dev", Dir: "dev", WorkflowConfiguration: digger.WorkflowConfiguration{
-		OnPullRequestPushed: []string{"digger plan"},
-		OnPullRequestClosed: []string{"digger unlock"},
-		OnCommitToDefault:   []string{"digger apply"},
-	}}
-	prod := digger.Project{Name: "prod", Dir: "prod", WorkflowConfiguration: digger.WorkflowConfiguration{
-		OnPullRequestPushed: []string{"digger plan"},
-		OnPullRequestClosed: []string{"digger unlock"},
-		OnCommitToDefault:   []string{"digger apply"},
-	}}
+	dev := digger.Project{Name: "dev", Dir: "dev", Workflow: "dev"}
+	prod := digger.Project{Name: "prod", Dir: "prod", Workflow: "prod"}
+	workflows := map[string]digger.Workflow{
+		"dev": {
+			Plan: &digger.Stage{Steps: []digger.Step{
+				{Action: "init", ExtraArgs: []string{}},
+				{Action: "plan", ExtraArgs: []string{"-var-file=dev.tfvars"}},
+			}},
+			Apply: &digger.Stage{Steps: []digger.Step{
+				{Action: "init", ExtraArgs: []string{}},
+				{Action: "apply", ExtraArgs: []string{"-var-file=dev.tfvars"}},
+			}},
+			Configuration: &digger.WorkflowConfiguration{
+				OnPullRequestPushed: []string{"digger plan"},
+				OnPullRequestClosed: []string{"digger unlock"},
+				OnCommitToDefault:   []string{"digger apply"},
+			},
+		},
+		"prod": {
+			Plan: &digger.Stage{Steps: []digger.Step{
+				{Action: "init", ExtraArgs: []string{}},
+				{Action: "plan", ExtraArgs: []string{"-var-file=dev.tfvars"}},
+			}},
+			Apply: &digger.Stage{Steps: []digger.Step{
+				{Action: "init", ExtraArgs: []string{}},
+				{Action: "apply", ExtraArgs: []string{"-var-file=dev.tfvars"}},
+			}},
+			Configuration: &digger.WorkflowConfiguration{
+				OnPullRequestPushed: []string{"digger plan"},
+				OnPullRequestClosed: []string{"digger unlock"},
+				OnCommitToDefault:   []string{"digger apply"},
+			},
+		},
+	}
 	projects := []digger.Project{dev, prod}
 	diggerConfig := digger.DiggerConfig{Projects: projects}
 
@@ -933,7 +956,7 @@ func TestGitHubNewPullRequestInMultiEnvProjectContext(t *testing.T) {
 	lock := &utils.MockLock{}
 	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, workflows)
 	spew.Dump(lock.MapLock)
 	assert.Equal(t, pullRequestNumber, prNumber)
 	assert.Equal(t, 1, len(commandsToRunPerProject))
diff --git a/pkg/digger/digger_config_test.go b/pkg/digger/digger_config_test.go
index b5e36ccf..771745b9 100644
--- a/pkg/digger/digger_config_test.go
+++ b/pkg/digger/digger_config_test.go
@@ -88,20 +88,21 @@ func TestDefaultValuesForWorkflowConfiguration(t *testing.T) {
 	defer teardown()
 
 	diggerCfg := `
-projects:
-- name: dev
-  branch: /main/
-  dir: .
-  workspace: default
+workflows:
+  default:
+    plan:
+      steps:
+      - init
+      - plan:
+          extra_args: ["-var-file=terraform.tfvars"]
 `
 	deleteFile := createFile(path.Join(tempDir, "digger.yaml"), diggerCfg)
 	defer deleteFile()
 
 	dg, err := NewDiggerConfig(tempDir)
 	assert.NoError(t, err, "expected error to be nil")
-	assert.Equal(t, dg.Projects[0].WorkflowConfiguration.OnPullRequestPushed[0], "digger plan")
-	assert.Equal(t, dg.Projects[0].WorkflowConfiguration.OnPullRequestClosed[0], "digger unlock")
-	assert.Equal(t, dg.Projects[0].WorkflowConfiguration.OnCommitToDefault[0], "digger apply")
+	assert.Equal(t, Step{"init", nil}, dg.Workflows["default"].Plan.Steps[0], "expected step name to be 'init'")
+	assert.Equal(t, Step{"plan", []string{"-var-file=terraform.tfvars"}}, dg.Workflows["default"].Plan.Steps[1], "expected step name to be 'init'")
 }
 
 func createTempDir() string {
diff --git a/pkg/integration/integration_test.go b/pkg/integration/integration_test.go
index ad1ba4c0..4a0836a4 100644
--- a/pkg/integration/integration_test.go
+++ b/pkg/integration/integration_test.go
@@ -379,7 +379,7 @@ func TestHappyPath(t *testing.T) {
 	//  new pr should lock the project
 	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
@@ -406,7 +406,7 @@ func TestHappyPath(t *testing.T) {
 	// 'digger plan' comment should trigger terraform execution
 	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
@@ -420,7 +420,7 @@ func TestHappyPath(t *testing.T) {
 	// 'digger apply' comment should trigger terraform execution and unlock the project
 	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
@@ -444,7 +444,7 @@ func TestHappyPath(t *testing.T) {
 
 	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
@@ -522,7 +522,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	// no files changed, no locks
 	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
@@ -548,7 +548,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	// 'digger plan' comment should trigger terraform execution
 	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
@@ -562,7 +562,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	// 'digger apply' comment should trigger terraform execution and unlock the project
 	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
@@ -586,7 +586,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 
 	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
 	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
diff --git a/pkg/terraform/tf_test.go b/pkg/terraform/tf_test.go
index 0c1c0bfc..a2dc6a94 100644
--- a/pkg/terraform/tf_test.go
+++ b/pkg/terraform/tf_test.go
@@ -19,7 +19,7 @@ func TestExecuteTerraformPlan(t *testing.T) {
 	CreateValidTerraformTestFile(dir)
 
 	tf := Terraform{WorkingDir: dir, Workspace: "dev"}
-	_, _, _, err := tf.Plan()
+	_, _, _, err := tf.Plan([]string{}, []string{})
 	assert.NoError(t, err)
 }
 
@@ -35,7 +35,7 @@ func TestExecuteTerraformApply(t *testing.T) {
 	CreateValidTerraformTestFile(dir)
 
 	tf := Terraform{WorkingDir: dir, Workspace: "dev"}
-	_, _, err := tf.Apply()
+	_, _, _, err := tf.Plan([]string{}, []string{})
 	assert.NoError(t, err)
 }
 
@@ -51,6 +51,6 @@ func TestExecuteTerraformApplyDefaultWorkspace(t *testing.T) {
 	CreateValidTerraformTestFile(dir)
 
 	tf := Terraform{WorkingDir: dir, Workspace: "default"}
-	_, _, err := tf.Apply()
+	_, _, err := tf.Apply([]string{}, []string{})
 	assert.NoError(t, err)
 }