Repository: diggerhq/digger
Pull request #318 (2023-06-06 09:45:22)

merge develop into pre 23


Patch:
diff --git a/cmd/digger/main.go b/cmd/digger/main.go
index 307157d7..8e452d2a 100644
--- a/cmd/digger/main.go
+++ b/cmd/digger/main.go
@@ -2,12 +2,16 @@ package main
 
 import (
 	"context"
+	"digger/pkg/azure"
 	"digger/pkg/configuration"
 	"digger/pkg/digger"
 	"digger/pkg/gcp"
 	dg_github "digger/pkg/github"
+	github_models "digger/pkg/github/models"
 	"digger/pkg/gitlab"
+	"digger/pkg/locking"
 	"digger/pkg/models"
+	"digger/pkg/storage"
 	"digger/pkg/usage"
 	"digger/pkg/utils"
 	"fmt"
@@ -17,7 +21,7 @@ import (
 	"strings"
 )
 
-func gitHubCI(lock utils.Lock) {
+func gitHubCI(lock locking.Lock) {
 	println("Using GitHub.")
 	githubRepositoryOwner := os.Getenv("GITHUB_REPOSITORY_OWNER")
 	if githubRepositoryOwner != "" {
@@ -36,7 +40,7 @@ func gitHubCI(lock utils.Lock) {
 		reportErrorAndExit(githubRepositoryOwner, "GITHUB_CONTEXT is not defined", 2)
 	}
 
-	parsedGhContext, err := models.GetGitHubContext(ghContext)
+	parsedGhContext, err := github_models.GetGitHubContext(ghContext)
 	if err != nil {
 		reportErrorAndExit(githubRepositoryOwner, fmt.Sprintf("Failed to parse GitHub context. %s", err), 3)
 	}
@@ -44,13 +48,13 @@ func gitHubCI(lock utils.Lock) {
 
 	walker := configuration.FileSystemDirWalker{}
 
-	diggerConfig, err := configuration.NewDiggerConfig("./", &walker)
+	diggerConfig, err := configuration.LoadDiggerConfig("./", &walker)
 	if err != nil {
 		reportErrorAndExit(githubRepositoryOwner, fmt.Sprintf("Failed to read Digger config. %s", err), 4)
 	}
 	println("Digger config read successfully")
 
-	lock, err = utils.GetLock()
+	lock, err = locking.GetLock()
 	if err != nil {
 		reportErrorAndExit(githubRepositoryOwner, fmt.Sprintf("Failed to create lock provider. %s", err), 5)
 	}
@@ -60,25 +64,28 @@ func gitHubCI(lock utils.Lock) {
 	eventName := parsedGhContext.EventName
 	splitRepositoryName := strings.Split(parsedGhContext.Repository, "/")
 	repoOwner, repositoryName := splitRepositoryName[0], splitRepositoryName[1]
-	githubPrService := dg_github.NewGithubPullRequestService(ghToken, repositoryName, repoOwner)
+	githubPrService := dg_github.NewGitHubService(ghToken, repositoryName, repoOwner)
 
-	impactedProjects, requestedProject, prNumber, err := digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err := dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	if err != nil {
 		reportErrorAndExit(githubRepositoryOwner, fmt.Sprintf("Failed to process GitHub event. %s", err), 6)
 	}
 	logImpactedProjects(impactedProjects, prNumber)
 	println("GitHub event processed successfully")
 
-	if digger.CheckIfHelpComment(ghEvent) {
+	if dg_github.CheckIfHelpComment(ghEvent) {
 		reply := utils.GetCommands()
-		githubPrService.PublishComment(prNumber, reply)
+		err := githubPrService.PublishComment(prNumber, reply)
+		if err != nil {
+			reportErrorAndExit(githubRepositoryOwner, "Failed to publish help command output", 1)
+		}
 	}
 
 	if len(impactedProjects) == 0 {
 		reportErrorAndExit(githubRepositoryOwner, "No projects impacted", 0)
 	}
 
-	commandsToRunPerProject, coversAllImpactedProjects, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, coversAllImpactedProjects, err := dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	if err != nil {
 		reportErrorAndExit(githubRepositoryOwner, fmt.Sprintf("Failed to convert GitHub event to commands. %s", err), 7)
 	}
@@ -108,9 +115,11 @@ func gitHubCI(lock utils.Lock) {
 	}()
 }
 
-func gitLabCI(lock utils.Lock) {
+func gitLabCI(lock locking.Lock) {
 	println("Using GitLab.")
+
 	projectNamespace := os.Getenv("CI_PROJECT_NAMESPACE")
+	projectName := os.Getenv("CI_PROJECT_NAME")
 	gitlabToken := os.Getenv("GITLAB_TOKEN")
 	if gitlabToken == "" {
 		fmt.Println("GITLAB_TOKEN is empty")
@@ -123,7 +132,7 @@ func gitLabCI(lock utils.Lock) {
 	}
 	fmt.Printf("main: working dir: %s \n", currentDir)
 
-	diggerConfig, err := configuration.NewDiggerConfig(currentDir, &walker)
+	diggerConfig, err := configuration.LoadDiggerConfig(currentDir, &walker)
 	if err != nil {
 		reportErrorAndExit(projectNamespace, fmt.Sprintf("Failed to read Digger config. %s", err), 4)
 	}
@@ -135,6 +144,12 @@ func gitLabCI(lock utils.Lock) {
 		os.Exit(4)
 	}
 
+	// it's ok to not have merge request info if it has been merged
+	if (gitLabContext.MergeRequestIId == nil || len(gitLabContext.OpenMergeRequests) == 0) && gitLabContext.EventType != "merge_request_merge" {
+		fmt.Println("No merge request found.")
+		os.Exit(0)
+	}
+
 	gitlabService, err := gitlab.NewGitLabService(gitlabToken, gitLabContext)
 	if err != nil {
 		fmt.Printf("failed to initialise GitLab service, %v", err)
@@ -157,21 +172,105 @@ func gitLabCI(lock utils.Lock) {
 	}
 	println("GitLab event converted to commands successfully")
 
+	println("Digger commands to be executed:")
 	for _, v := range commandsToRunPerProject {
 		fmt.Printf("command: %s, project: %s\n", strings.Join(v.Commands, ", "), v.ProjectName)
 	}
 
 	//planStorage := newPlanStorage(ghToken, repoOwner, repositoryName, prNumber)
-	var planStorage utils.PlanStorage
+	planStorage := newPlanStorage(gitlabToken, projectNamespace, projectName, *gitLabContext.MergeRequestIId)
 
-	result, err := gitlab.RunCommandsPerProject(commandsToRunPerProject, *gitLabContext, diggerConfig, gitlabService, lock, planStorage, currentDir)
+	allAppliesSuccess, err := gitlab.RunCommandsPerProject(commandsToRunPerProject, *gitLabContext, diggerConfig, gitlabService, lock, planStorage, currentDir)
 	if err != nil {
 		fmt.Printf("failed to execute command, %v", err)
 		os.Exit(8)
 	}
-	print(result)
+
+	if diggerConfig.AutoMerge && allAppliesSuccess {
+		digger.MergePullRequest(gitlabService, *gitLabContext.MergeRequestIId)
+		println("Merge request changes has been applied successfully")
+	}
+
+	println("Commands executed successfully")
+
+	reportErrorAndExit(projectName, "Digger finished successfully", 0)
+
+	defer func() {
+		if r := recover(); r != nil {
+			reportErrorAndExit(projectName, fmt.Sprintf("Panic occurred. %s", r), 1)
+		}
+	}()
+}
+
+func azureCI(lock locking.Lock) {
+	fmt.Println("> Azure CI detected")
+	azureContext := os.Getenv("AZURE_CONTEXT")
+	azureToken := os.Getenv("AZURE_TOKEN")
+	if azureToken == "" {
+		fmt.Println("AZURE_TOKEN is empty")
+	}
+	parsedAzureContext, err := azure.GetAzureReposContext(azureContext)
+	if err != nil {
+		fmt.Printf("failed to parse Azure context. %s\n", err.Error())
+		os.Exit(4)
+	}
+
+	walker := configuration.FileSystemDirWalker{}
+	currentDir, err := os.Getwd()
+	if err != nil {
+		reportErrorAndExit(parsedAzureContext.BaseUrl, fmt.Sprintf("Failed to get current dir. %s", err), 4)
+	}
+	fmt.Printf("main: working dir: %s \n", currentDir)
+
+	diggerConfig, err := configuration.LoadDiggerConfig(currentDir, &walker)
+	if err != nil {
+		reportErrorAndExit(parsedAzureContext.BaseUrl, fmt.Sprintf("Failed to read Digger config. %s", err), 4)
+	}
+	fmt.Println("Digger config read successfully")
+
+	azureService, err := azure.NewAzureReposService(azureToken, parsedAzureContext.BaseUrl, parsedAzureContext.ProjectName, parsedAzureContext.RepositoryId)
+	if err != nil {
+		reportErrorAndExit(parsedAzureContext.BaseUrl, fmt.Sprintf("Failed to initialise azure service. %s", err), 5)
+	}
+
+	impactedProjects, requestedProject, prNumber, err := azure.ProcessAzureReposEvent(parsedAzureContext.Event, diggerConfig, azureService)
+	if err != nil {
+		reportErrorAndExit(parsedAzureContext.BaseUrl, fmt.Sprintf("Failed to process Azure event. %s", err), 6)
+	}
+	fmt.Println("Azure event processed successfully")
+
+	commandsToRunPerProject, coversAllImpactedProjects, err := azure.ConvertAzureEventToCommands(parsedAzureContext, impactedProjects, requestedProject, diggerConfig.Workflows)
+	if err != nil {
+		reportErrorAndExit(parsedAzureContext.BaseUrl, fmt.Sprintf("Failed to convert event to command. %s", err), 7)
+
+	}
+	fmt.Println(fmt.Sprintf("Azure event converted to commands successfully: %v", commandsToRunPerProject))
+
+	for _, v := range commandsToRunPerProject {
+		fmt.Printf("command: %s, project: %s\n", strings.Join(v.Commands, ", "), v.ProjectName)
+	}
+
+	var planStorage storage.PlanStorage
+
+	allAppliesSuccess, atLeastOneApply, err := digger.RunCommandsPerProject(commandsToRunPerProject, parsedAzureContext.ProjectName, parsedAzureContext.ProjectName, parsedAzureContext.EventType, prNumber, azureService, lock, planStorage, currentDir)
+	if err != nil {
+		reportErrorAndExit(parsedAzureContext.BaseUrl, fmt.Sprintf("Failed to run commands. %s", err), 8)
+	}
+
+	if diggerConfig.AutoMerge && allAppliesSuccess && atLeastOneApply && coversAllImpactedProjects {
+		digger.MergePullRequest(azureService, prNumber)
+		fmt.Println("PR merged successfully")
+	}
 
 	println("Commands executed successfully")
+
+	reportErrorAndExit(parsedAzureContext.BaseUrl, "Digger finished successfully", 0)
+
+	defer func() {
+		if r := recover(); r != nil {
+			reportErrorAndExit(parsedAzureContext.BaseUrl, fmt.Sprintf("Panic occurred. %s", r), 1)
+		}
+	}()
 }
 
 /*
@@ -199,7 +298,7 @@ func main() {
 		os.Exit(0)
 	}
 
-	lock, err := utils.GetLock()
+	lock, err := locking.GetLock()
 	if err != nil {
 		fmt.Printf("Failed to create lock provider. %s\n", err)
 		os.Exit(2)
@@ -212,6 +311,8 @@ func main() {
 		gitHubCI(lock)
 	case digger.GitLab:
 		gitLabCI(lock)
+	case digger.Azure:
+		azureCI(lock)
 	case digger.BitBucket:
 	case digger.None:
 		print("No CI detected.")
@@ -219,32 +320,35 @@ func main() {
 	}
 }
 
-func newPlanStorage(ghToken string, repoOwner string, repositoryName string, prNumber int) utils.PlanStorage {
-	var planStorage utils.PlanStorage
+func newPlanStorage(ghToken string, repoOwner string, repositoryName string, prNumber int) storage.PlanStorage {
+	var planStorage storage.PlanStorage
 
-	if os.Getenv("PLAN_UPLOAD_DESTINATION") == "github" {
+	uploadDestination := strings.ToLower(os.Getenv("PLAN_UPLOAD_DESTINATION"))
+	if uploadDestination == "github" {
 		zipManager := utils.Zipper{}
-		planStorage = &utils.GithubPlanStorage{
+		planStorage = &storage.GithubPlanStorage{
 			Client:            github.NewTokenClient(context.Background(), ghToken),
 			Owner:             repoOwner,
 			RepoName:          repositoryName,
 			PullRequestNumber: prNumber,
 			ZipManager:        zipManager,
 		}
-	} else if os.Getenv("PLAN_UPLOAD_DESTINATION") == "gcp" {
+	} else if uploadDestination == "gcp" {
 		ctx, client := gcp.GetGoogleStorageClient()
-
 		bucketName := strings.ToLower(os.Getenv("GOOGLE_STORAGE_BUCKET"))
 		if bucketName == "" {
 			reportErrorAndExit(repoOwner, fmt.Sprintf("GOOGLE_STORAGE_BUCKET is not defined"), 9)
 		}
 		bucket := client.Bucket(bucketName)
-		planStorage = &utils.PlanStorageGcp{
+		planStorage = &storage.PlanStorageGcp{
 			Client:  client,
 			Bucket:  bucket,
 			Context: ctx,
 		}
+	} else if uploadDestination == "gitlab" {
+		//TODO implement me
 	}
+
 	return planStorage
 }
 
@@ -256,7 +360,7 @@ func logImpactedProjects(projects []configuration.Project, prNumber int) {
 	log.Print(logMessage)
 }
 
-func logCommands(projectCommands []digger.ProjectCommand) {
+func logCommands(projectCommands []models.ProjectCommand) {
 	logMessage := fmt.Sprintf("Following commands are going to be executed:\n")
 	for _, pc := range projectCommands {
 		logMessage += fmt.Sprintf("project: %s: commands: ", pc.ProjectName)
@@ -269,7 +373,7 @@ func logCommands(projectCommands []digger.ProjectCommand) {
 }
 
 func reportErrorAndExit(repoOwner string, message string, exitCode int) {
-	fmt.Printf(message)
+	fmt.Println(message)
 	err := usage.SendLogRecord(repoOwner, message)
 	if err != nil {
 		fmt.Printf("Failed to send log record. %s\n", err)
diff --git a/go.mod b/go.mod
index ecb1ba53..8264f313 100644
--- a/go.mod
+++ b/go.mod
@@ -6,11 +6,12 @@ require (
 	cloud.google.com/go/storage v1.30.1
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0
 	github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1
-	github.com/aws/aws-sdk-go v1.44.274
+	github.com/aws/aws-sdk-go v1.44.275
 	github.com/bmatcuk/doublestar/v4 v4.6.0
 	github.com/caarlos0/env/v7 v7.1.0
 	github.com/google/go-github/v51 v51.0.0
 	github.com/google/uuid v1.3.0
+	github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5
 	github.com/stretchr/testify v1.8.4
 	github.com/xanzy/go-gitlab v0.84.0
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index db07dbe8..421ad001 100644
--- a/go.sum
+++ b/go.sum
@@ -31,6 +31,8 @@ github.com/aws/aws-sdk-go v1.44.273 h1:CX8O0gK+cGrgUyv7bgJ6QQP9mQg7u5mweHdNzULH4
 github.com/aws/aws-sdk-go v1.44.273/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/aws/aws-sdk-go v1.44.274 h1:vfreSv19e/9Ka9YytOzgzJasrRZfX7dnttLlbh8NKeA=
 github.com/aws/aws-sdk-go v1.44.274/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.275 h1:VqRULgqrigvQLll4e4hXuc568EQAtZQ6jmBzLlQHzSI=
+github.com/aws/aws-sdk-go v1.44.275/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
 github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
 github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
@@ -83,6 +85,7 @@ github.com/google/go-github/v51 v51.0.0/go.mod h1:kZj/rn/c1lSUbr/PFWl2hhusPV7a5X
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -107,6 +110,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s=
+github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -229,10 +234,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
 google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 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/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/azure/azure.go b/pkg/azure/azure.go
new file mode 100644
index 00000000..66d4115e
--- /dev/null
+++ b/pkg/azure/azure.go
@@ -0,0 +1,466 @@
+package azure
+
+import (
+	"context"
+	"digger/pkg/ci"
+	"digger/pkg/configuration"
+	"digger/pkg/models"
+	"digger/pkg/utils"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/microsoft/azure-devops-go-api/azuredevops"
+	"github.com/microsoft/azure-devops-go-api/azuredevops/git"
+	"strings"
+)
+
+const (
+	AzurePrUpdated   = "git.pullrequest.updated"
+	AzurePrCreated   = "git.pullrequest.created"
+	AzurePrMerged    = "git.pullrequest.merged"
+	AzurePrClosed    = "git.pullrequest.closed"
+	AzurePrReopened  = "git.pullrequest.reopened"
+	AzurePrCommented = "ms.vss-code.git-pullrequest-comment-event"
+)
+
+type AzurePrEvent struct {
+	EventType          string             `json:"eventType"`
+	Resource           Resource           `json:"resource"`
+	ResourceContainers ResourceContainers `json:"resourceContainers"`
+}
+
+type Repository struct {
+	Id      string `json:"id"`
+	Name    string `json:"name"`
+	Project struct {
+		Name string `json:"name"`
+	} `json:"project"`
+	Status string `json:"status"`
+}
+
+type Resource struct {
+	Repository    Repository `json:"repository"`
+	PullRequestId int        `json:"pullRequestId"`
+}
+
+type ResourceContainers struct {
+	Account struct {
+		BaseUrl string `json:"baseUrl"`
+	} `json:"account"`
+}
+type AzureCommentEvent struct {
+	EventType string `json:"eventType"`
+	Resource  struct {
+		Comment struct {
+			Content string `json:"content"`
+		} `json:"comment"`
+		PullRequest struct {
+			Repository    Repository `json:"repository"`
+			PullRequestId int        `json:"pullRequestId"`
+		} `json:"pullRequest"`
+	} `json:"resource"`
+	ResourceContainers ResourceContainers `json:"resourceContainers"`
+}
+
+type Azure struct {
+	EventType    string `json:"eventType"`
+	Event        interface{}
+	ProjectName  string
+	BaseUrl      string
+	RepositoryId string
+}
+
+func GetAzureReposContext(azureContext string) (Azure, error) {
+	var parsedAzureContext Azure
+	err := json.Unmarshal([]byte(azureContext), &parsedAzureContext)
+	if err != nil {
+		return Azure{}, fmt.Errorf("error parsing Azure context JSON: %v", err)
+	}
+	return parsedAzureContext, nil
+}
+
+func (a *Azure) UnmarshalJSON(data []byte) error {
+	type Alias Azure
+	aux := struct {
+		*Alias
+	}{
+		Alias: (*Alias)(a),
+	}
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	switch a.EventType {
+	case "git.pullrequest.updated", "git.pullrequest.created", "git.pullrequest.merged", "git.pullrequest.closed", "git.pullrequest.reopened":
+		var event AzurePrEvent
+		if err := json.Unmarshal(data, &event); err != nil {
+			return err
+		}
+		a.Event = event
+		a.ProjectName = event.Resource.Repository.Project.Name
+		a.BaseUrl = event.ResourceContainers.Account.BaseUrl
+		a.RepositoryId = event.Resource.Repository.Id
+	case "ms.vss-code.git-pullrequest-comment-event":
+		var event AzureCommentEvent
+		if err := json.Unmarshal(data, &event); err != nil {
+			return err
+		}
+		a.Event = event
+		a.ProjectName = event.Resource.PullRequest.Repository.Project.Name
+		a.BaseUrl = event.ResourceContainers.Account.BaseUrl
+		a.RepositoryId = event.Resource.PullRequest.Repository.Id
+	default:
+		return errors.New("unknown Azure event: " + a.EventType)
+	}
+
+	return nil
+}
+
+func NewAzureReposService(patToken string, baseUrl string, projectName string, repositoryId string) (*AzureReposService, error) {
+	client, err := git.NewClient(context.Background(), azuredevops.NewPatConnection(baseUrl, patToken))
+
+	if err != nil {
+		return nil, err
+	}
+	return &AzureReposService{
+		Client:       client,
+		ProjectName:  projectName,
+		RepositoryId: repositoryId,
+	}, nil
+}
+
+type AzureReposService struct {
+	Client       git.Client
+	ProjectName  string
+	RepositoryId string
+}
+
+func (a *AzureReposService) GetChangedFiles(prNumber int) ([]string, error) {
+
+	pullRequest, err := a.Client.GetPullRequestById(context.Background(), git.GetPullRequestByIdArgs{
+		Project:       &a.ProjectName,
+		PullRequestId: &prNumber,
+	})
+	if err != nil {
+		return nil, err
+	}
+	sourceCommitId := pullRequest.LastMergeSourceCommit.CommitId
+	targetCommitId := pullRequest.LastMergeTargetCommit.CommitId
+
+	repositoryId := pullRequest.Repository.Id.String()
+	changes, err := a.Client.GetCommitDiffs(context.Background(), git.GetCommitDiffsArgs{
+		Project:                 &a.ProjectName,
+		RepositoryId:            &repositoryId,
+		BaseVersionDescriptor:   &git.GitBaseVersionDescriptor{BaseVersion: targetCommitId, BaseVersionType: &git.GitVersionTypeValues.Commit},
+		TargetVersionDescriptor: &git.GitTargetVersionDescriptor{TargetVersion: sourceCommitId, TargetVersionType: &git.GitVersionTypeValues.Commit},
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	var changedFiles []string
+	for _, change := range *changes.Changes {
+		if item, ok := change.(map[string]interface{})["item"].(map[string]interface{}); ok {
+			if p, ok := item["path"].(string); ok {
+				changedFiles = append(changedFiles, p)
+			}
+		}
+	}
+	return changedFiles, nil
+}
+
+func (a *AzureReposService) PublishComment(prNumber int, comment string) error {
+	_, err := a.Client.CreateThread(context.Background(), git.CreateThreadArgs{
+		Project:       &a.ProjectName,
+		PullRequestId: &prNumber,
+		RepositoryId:  &a.RepositoryId,
+		CommentThread: &git.GitPullRequestCommentThread{
+			Comments: &[]git.Comment{{
+				Content: &comment,
+			}},
+		},
+	})
+	return err
+}
+
+func (a *AzureReposService) SetStatus(prNumber int, status string, statusContext string) error {
+	var gitStatusState git.GitStatusState
+	if status == "success" {
+		gitStatusState = git.GitStatusStateValues.Succeeded
+	} else if status == "failure" {
+		gitStatusState = git.GitStatusStateValues.Failed
+	} else if status == "pending" {
+		gitStatusState = git.GitStatusStateValues.Pending
+	} else {
+		gitStatusState = git.GitStatusStateValues.NotSet
+	}
+
+	gitStatusContext := git.GitStatusContext{Name: &statusContext}
+	_, err := a.Client.CreatePullRequestStatus(context.Background(), git.CreatePullRequestStatusArgs{
+		Project:       &a.ProjectName,
+		PullRequestId: &prNumber,
+		RepositoryId:  &a.RepositoryId,
+		Status: &git.GitPullRequestStatus{
+			State:       &gitStatusState,
+			Context:     &gitStatusContext,
+			Description: &statusContext,
+		},
+	})
+	return err
+}
+
+func (a *AzureReposService) GetCombinedPullRequestStatus(prNumber int) (string, error) {
+	pullRequestStatuses, err := a.Client.GetPullRequestStatuses(context.Background(), git.GetPullRequestStatusesArgs{
+		Project:       &a.ProjectName,
+		PullRequestId: &prNumber,
+		RepositoryId:  &a.RepositoryId,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	latestUniqueRequestStatuses := make(map[string]*git.GitPullRequestStatus)
+
+	for _, status := range *pullRequestStatuses {
+		if status.Context == nil || status.Context.Name == nil || status.Context.Genre == nil {
+			continue
+		}
+		key := fmt.Sprintf("%s/%s", *status.Context.Name, *status.Context.Genre)
+
+		if res, ok := latestUniqueRequestStatuses[key]; !ok {
+			latestUniqueRequestStatuses[key] = &status
+		} else {
+			if status.CreationDate.Time.After(res.CreationDate.Time) {
+				latestUniqueRequestStatuses[key] = &status
+			}
+		}
+	}
+
+	for _, status := range latestUniqueRequestStatuses {
+		if status.State != nil && (*status.State == git.GitStatusStateValues.Failed || *status.State == git.GitStatusStateValues.Error) {
+			return "failure", nil
+		}
+	}
+
+	var allSuccess = true
+	for _, status := range latestUniqueRequestStatuses {
+		if status.State != nil || *status.State != git.GitStatusStateValues.Succeeded {
+			allSuccess = false
+			break
+		}
+	}
+	if allSuccess {
+		return "success", nil
+	}
+
+	return "pending", nil
+}
+
+func (a *AzureReposService) MergePullRequest(prNumber int) error {
+	pullRequest, err := a.Client.GetPullRequestById(context.Background(), git.GetPullRequestByIdArgs{
+		Project:       &a.ProjectName,
+		PullRequestId: &prNumber,
+	})
+	if err != nil {
+		return err
+	}
+	_, err = a.Client.UpdatePullRequest(context.Background(), git.UpdatePullRequestArgs{
+		Project:       &a.ProjectName,
+		RepositoryId:  &a.RepositoryId,
+		PullRequestId: &prNumber,
+		GitPullRequestToUpdate: &git.GitPullRequest{
+			LastMergeSourceCommit: pullRequest.LastMergeSourceCommit,
+			Status:                &git.PullRequestStatusValues.Completed,
+		},
+	})
+	return err
+}
+
+func (a *AzureReposService) IsMergeable(prNumber int) (bool, error) {
+	pullRequest, err := a.Client.GetPullRequestById(context.Background(), git.GetPullRequestByIdArgs{
+		Project:       &a.ProjectName,
+		PullRequestId: &prNumber,
+	})
+	if err != nil {
+		return false, err
+	}
+	return *pullRequest.MergeStatus == git.PullRequestAsyncStatusValues.Succeeded, nil
+}
+
+func (a *AzureReposService) IsClosed(prNumber int) (bool, error) {
+	pullRequest, err := a.Client.GetPullRequestById(context.Background(), git.GetPullRequestByIdArgs{
+		Project:       &a.ProjectName,
+		PullRequestId: &prNumber,
+	})
+	if err != nil {
+		return false, err
+	}
+	return *pullRequest.Status == git.PullRequestStatusValues.Completed || *pullRequest.Status == git.PullRequestStatusValues.Abandoned, nil
+}
+
+func ProcessAzureReposEvent(azureEvent interface{}, diggerConfig *configuration.DiggerConfig, ciService ci.CIService) ([]configuration.Project, *configuration.Project, int, error) {
+	var impactedProjects []configuration.Project
+	var prNumber int
+
+	switch azureEvent.(type) {
+	case AzurePrEvent:
+		prNumber = azureEvent.(AzurePrEvent).Resource.PullRequestId
+		changedFiles, err := ciService.GetChangedFiles(prNumber)
+
+		if err != nil {
+			return nil, nil, 0, fmt.Errorf("could not get changed files: %v", err)
+		}
+
+		impactedProjects = diggerConfig.GetModifiedProjects(changedFiles)
+	case AzureCommentEvent:
+		prNumber = azureEvent.(AzureCommentEvent).Resource.PullRequest.PullRequestId
+		changedFiles, err := ciService.GetChangedFiles(prNumber)
+
+		if err != nil {
+			return nil, nil, 0, fmt.Errorf("could not get changed files: %v", err)
+		}
+
+		impactedProjects = diggerConfig.GetModifiedProjects(changedFiles)
+		requestedProject := utils.ParseProjectName(azureEvent.(AzureCommentEvent).Resource.Comment.Content)
+
+		if requestedProject == "" {
+			return impactedProjects, nil, prNumber, nil
+		}
+
+		for _, project := range impactedProjects {
+			if project.Name == requestedProject {
+				return impactedProjects, &project, prNumber, nil
+			}
+		}
+		return nil, nil, 0, fmt.Errorf("requested project not found in modified projects")
+
+	default:
+		return nil, nil, 0, fmt.Errorf("unsupported event type")
+	}
+	return impactedProjects, nil, prNumber, nil
+}
+
+func ConvertAzureEventToCommands(parseAzureContext Azure, impactedProjects []configuration.Project, requestedProject *configuration.Project, workflows map[string]configuration.Workflow) ([]models.ProjectCommand, bool, error) {
+	commandsPerProject := make([]models.ProjectCommand, 0)
+	switch parseAzureContext.EventType {
+	case AzurePrCreated, AzurePrUpdated, AzurePrReopened:
+		for _, project := range impactedProjects {
+			workflow, ok := workflows[project.Workflow]
+			if !ok {
+				return nil, false, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
+			}
+
+			stateEnvVars, commandEnvVars := configuration.CollectEnvVars(workflow.EnvVars)
+
+			commandsPerProject = append(commandsPerProject, models.ProjectCommand{
+				ProjectName:      project.Name,
+				ProjectDir:       project.Dir,
+				ProjectWorkspace: project.Workspace,
+				Terragrunt:       project.Terragrunt,
+				Commands:         workflow.Configuration.OnPullRequestPushed,
+				ApplyStage:       workflow.Apply,
+				PlanStage:        workflow.Plan,
+				CommandEnvVars:   commandEnvVars,
+				StateEnvVars:     stateEnvVars,
+			})
+		}
+		return commandsPerProject, true, nil
+	case AzurePrClosed:
+		for _, project := range impactedProjects {
+			workflow, ok := workflows[project.Workflow]
+			if !ok {
+				return nil, false, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
+			}
+
+			stateEnvVars, commandEnvVars := configuration.CollectEnvVars(workflow.EnvVars)
+
+			commandsPerProject = append(commandsPerProject, models.ProjectCommand{
+				ProjectName:      project.Name,
+				ProjectDir:       project.Dir,
+				ProjectWorkspace: project.Workspace,
+				Terragrunt:       project.Terragrunt,
+				Commands:         workflow.Configuration.OnPullRequestClosed,
+				ApplyStage:       workflow.Apply,
+				PlanStage:        workflow.Plan,
+				CommandEnvVars:   commandEnvVars,
+				StateEnvVars:     stateEnvVars,
+			})
+		}
+		return commandsPerProject, true, nil
+	case AzurePrMerged:
+		if parseAzureContext.Event.(AzurePrEvent).Resource.Repository.Status == "completed" {
+			for _, project := range impactedProjects {
+				workflow, ok := workflows[project.Workflow]
+				if !ok {
+					return nil, false, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
+				}
+				stateEnvVars, commandEnvVars := configuration.CollectEnvVars(workflow.EnvVars)
+
+				commandsPerProject = append(commandsPerProject, models.ProjectCommand{
+					ProjectName:      project.Name,
+					ProjectDir:       project.Dir,
+					ProjectWorkspace: project.Workspace,
+					Terragrunt:       project.Terragrunt,
+					Commands:         workflow.Configuration.OnCommitToDefault,
+					ApplyStage:       workflow.Apply,
+					PlanStage:        workflow.Plan,
+					CommandEnvVars:   commandEnvVars,
+					StateEnvVars:     stateEnvVars,
+				})
+			}
+			return commandsPerProject, true, nil
+		}
+		return commandsPerProject, true, nil
+	case AzurePrCommented:
+
+		diggerCommand := parseAzureContext.Event.(AzureCommentEvent).Resource.Comment.Content
+
+		coversAllImpactedProjects := true
+
+		runForProjects := impactedProjects
+
+		if requestedProject != nil {
+			if len(impactedProjects) > 1 {
+				coversAllImpactedProjects = false
+				runForProjects = []configuration.Project{*requestedProject}
+			} else if len(impactedProjects) == 1 && impactedProjects[0].Name != requestedProject.Name {
+				return commandsPerProject, false, fmt.Errorf("requested project %v is not impacted by this PR", requestedProject.Name)
+			}
+		}
+
+		supportedCommands := []string{"digger plan", "digger apply", "digger unlock", "digger lock"}
+		for _, command := range supportedCommands {
+			if strings.Contains(diggerCommand, command) {
+				for _, project := range runForProjects {
+					workspace := project.Workspace
+					workspaceOverride, err := utils.ParseWorkspace(diggerCommand)
+					if err != nil {
+						return []models.ProjectCommand{}, coversAllImpactedProjects, err
+					}
+					if workspaceOverride != "" {
+						workspace = workspaceOverride
+					}
+					workflow, ok := workflows[project.Workflow]
+					if !ok {
+						return nil, false, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
+					}
+					stateEnvVars, commandEnvVars := configuration.CollectEnvVars(workflow.EnvVars)
+
+					commandsPerProject = append(commandsPerProject, models.ProjectCommand{
+						ProjectName:      project.Name,
+						ProjectDir:       project.Dir,
+						ProjectWorkspace: workspace,
+						Terragrunt:       project.Terragrunt,
+						Commands:         []string{command},
+						CommandEnvVars:   commandEnvVars,
+						StateEnvVars:     stateEnvVars,
+					})
+				}
+			}
+		}
+		return commandsPerProject, coversAllImpactedProjects, nil
+
+	default:
+		return []models.ProjectCommand{}, true, fmt.Errorf("unsupported Azure event type: %v", parseAzureContext.EventType)
+	}
+}
diff --git a/pkg/ci/main.go b/pkg/ci/main.go
index 76c2a866..e0d7dd33 100644
--- a/pkg/ci/main.go
+++ b/pkg/ci/main.go
@@ -2,7 +2,8 @@ package ci
 
 type CIService interface {
 	GetChangedFiles(prNumber int) ([]string, error)
-	PublishComment(prNumber int, comment string)
+	PublishComment(prNumber int, comment string) error
+	// SetStatus set status of specified pull/merge request, status could be: "pending", "failure", "success"
 	SetStatus(prNumber int, status string, statusContext string) error
 	GetCombinedPullRequestStatus(prNumber int) (string, error)
 	MergePullRequest(prNumber int) error
diff --git a/pkg/configuration/digger_config.go b/pkg/configuration/digger_config.go
index 7458a258..14235e04 100644
--- a/pkg/configuration/digger_config.go
+++ b/pkg/configuration/digger_config.go
@@ -195,12 +195,69 @@ func (s *Step) extract(stepMap map[string]interface{}, action string) {
 	}
 }
 
+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{},
+				},
+			},
+		},
+	}
+}
+
 func ConvertDiggerYamlToConfig(diggerYaml *DiggerConfigYaml, workingDir string, walker DirWalker) (*DiggerConfig, error) {
 	var diggerConfig DiggerConfig
+	const defaultWorkflowName = "default"
 
 	diggerConfig.AutoMerge = diggerYaml.AutoMerge
-	diggerConfig.Workflows = diggerYaml.Workflows
+
+	// if workflow block is not specified in yaml we create a default one, and add it to every project
+	if diggerYaml.Workflows != nil {
+		diggerConfig.Workflows = diggerYaml.Workflows
+	} else {
+		workflow := *defaultWorkflow()
+		diggerConfig.Workflows = make(map[string]Workflow)
+		diggerConfig.Workflows[defaultWorkflowName] = workflow
+	}
+
 	diggerConfig.Projects = diggerYaml.Projects
+
+	// update project's workflow if needed
+	for _, project := range diggerConfig.Projects {
+		if project.Workflow == "" {
+			project.Workflow = defaultWorkflowName
+		}
+	}
+
+	// check for project name duplicates
+	projectNames := make(map[string]bool)
+	for _, project := range diggerConfig.Projects {
+		if projectNames[project.Name] {
+			return nil, fmt.Errorf("project name '%s' is duplicated", project.Name)
+		}
+		projectNames[project.Name] = true
+	}
+
 	diggerConfig.CollectUsageData = diggerYaml.CollectUsageData
 
 	if diggerYaml.GenerateProjectsConfig != nil {
@@ -222,25 +279,16 @@ func ConvertDiggerYamlToConfig(diggerYaml *DiggerConfigYaml, workingDir string,
 				return nil, err
 			}
 			if includeMatch && !excludeMatch {
-				project := Project{Name: filepath.Base(dir), Dir: filepath.Join(workingDir, dir)}
+				// generate a new project using default workflow
+				project := Project{Name: filepath.Base(dir), Dir: filepath.Join(workingDir, dir), Workflow: defaultWorkflowName}
 				diggerConfig.Projects = append(diggerConfig.Projects, project)
 			}
 		}
 	}
-
-	projectNames := make(map[string]bool)
-
-	for _, project := range diggerConfig.Projects {
-		if projectNames[project.Name] {
-			return nil, fmt.Errorf("project name '%s' is duplicated", project.Name)
-		}
-		projectNames[project.Name] = true
-	}
-
 	return &diggerConfig, nil
 }
 
-func NewDiggerConfig(workingDir string, walker DirWalker) (*DiggerConfig, error) {
+func LoadDiggerConfig(workingDir string, walker DirWalker) (*DiggerConfig, error) {
 	config := &DiggerConfigYaml{}
 	fileName, err := retrieveConfigFile(workingDir)
 	if err != nil {
@@ -303,6 +351,13 @@ func NewDiggerConfig(workingDir string, walker DirWalker) (*DiggerConfig, error)
 	if err != nil {
 		return nil, err
 	}
+
+	for _, p := range c.Projects {
+		_, ok := c.Workflows[p.Workflow]
+		if !ok {
+			return nil, fmt.Errorf("failed to find workflow config '%s' for project '%s'", p.Workflow, p.Name)
+		}
+	}
 	return c, nil
 }
 
@@ -374,20 +429,6 @@ func (c *DiggerConfig) GetWorkflow(workflowName string) *Workflow {
 
 }
 
-func (c *DiggerConfig) GetWorkflowConfiguration(projectName string) WorkflowConfiguration {
-	project := c.GetProject(projectName)
-	workflows := c.Workflows
-	if project == nil {
-		return WorkflowConfiguration{}
-	}
-	workflow, ok := workflows[project.Workflow]
-
-	if !ok {
-		return WorkflowConfiguration{}
-	}
-	return *workflow.Configuration
-}
-
 type File struct {
 	Filename string
 }
@@ -427,3 +468,26 @@ func retrieveConfigFile(workingDir string) (string, error) {
 	// missing which is a non-error
 	return "", nil
 }
+
+func CollectEnvVars(envs EnvVars) (map[string]string, map[string]string) {
+	stateEnvVars := map[string]string{}
+
+	for _, envvar := range envs.State {
+		if envvar.Value != "" {
+			stateEnvVars[envvar.Name] = envvar.Value
+		} else if envvar.ValueFrom != "" {
+			stateEnvVars[envvar.Name] = os.Getenv(envvar.ValueFrom)
+		}
+	}
+
+	commandEnvVars := map[string]string{}
+
+	for _, envvar := range envs.Commands {
+		if envvar.Value != "" {
+			commandEnvVars[envvar.Name] = envvar.Value
+		} else if envvar.ValueFrom != "" {
+			commandEnvVars[envvar.Name] = os.Getenv(envvar.ValueFrom)
+		}
+	}
+	return stateEnvVars, commandEnvVars
+}
diff --git a/pkg/digger/digger.go b/pkg/digger/digger.go
index 0830b95d..237d5004 100644
--- a/pkg/digger/digger.go
+++ b/pkg/digger/digger.go
@@ -4,12 +4,12 @@ import (
 	"bytes"
 	"digger/pkg/ci"
 	"digger/pkg/configuration"
+	"digger/pkg/locking"
 	"digger/pkg/models"
+	"digger/pkg/storage"
 	"digger/pkg/terraform"
 	"digger/pkg/usage"
 	"digger/pkg/utils"
-	"encoding/json"
-	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -19,56 +19,14 @@ import (
 	"path"
 	"regexp"
 	"strings"
+	"time"
 )
 
-func ProcessGitHubEvent(ghEvent models.Event, diggerConfig *configuration.DiggerConfig, ciService ci.CIService) ([]configuration.Project, *configuration.Project, int, error) {
-	var impactedProjects []configuration.Project
-	var prNumber int
-
-	switch ghEvent.(type) {
-	case models.PullRequestEvent:
-		prNumber = ghEvent.(models.PullRequestEvent).PullRequest.Number
-		changedFiles, err := ciService.GetChangedFiles(prNumber)
-
-		if err != nil {
-			return nil, nil, 0, fmt.Errorf("could not get changed files")
-		}
-
-		impactedProjects = diggerConfig.GetModifiedProjects(changedFiles)
-	case models.IssueCommentEvent:
-		prNumber = ghEvent.(models.IssueCommentEvent).Issue.Number
-		changedFiles, err := ciService.GetChangedFiles(prNumber)
-
-		if err != nil {
-			return nil, nil, 0, fmt.Errorf("could not get changed files")
-		}
-
-		impactedProjects = diggerConfig.GetModifiedProjects(changedFiles)
-		requestedProject := parseProjectName(ghEvent.(models.IssueCommentEvent).Comment.Body)
-
-		if requestedProject == "" {
-			return impactedProjects, nil, prNumber, nil
-		}
-
-		for _, project := range impactedProjects {
-			if project.Name == requestedProject {
-				return impactedProjects, &project, prNumber, nil
-			}
-		}
-		return nil, nil, 0, fmt.Errorf("requested project not found in modified projects")
-
-	default:
-		return nil, nil, 0, fmt.Errorf("unsupported event type")
-	}
-	return impactedProjects, nil, prNumber, nil
-}
-
-func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string, repoName string, eventName string, prNumber int, ciService ci.CIService, lock utils.Lock, planStorage utils.PlanStorage, workingDir string) (bool, bool, error) {
-
+func RunCommandsPerProject(commandsPerProject []models.ProjectCommand, repoOwner string, repoName string, eventName string, prNumber int, ciService ci.CIService, lock locking.Lock, planStorage storage.PlanStorage, workingDir string) (bool, bool, error) {
 	appliesPerProject := make(map[string]bool)
 	for _, projectCommands := range commandsPerProject {
 		for _, command := range projectCommands.Commands {
-			projectLock := &utils.ProjectLockImpl{
+			projectLock := &locking.ProjectLockImpl{
 				InternalLock: lock,
 				CIService:    ciService,
 				ProjectName:  projectCommands.ProjectName,
@@ -142,7 +100,6 @@ func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string
 	}
 
 	allAppliesSuccess := true
-
 	for _, success := range appliesPerProject {
 		if !success {
 			allAppliesSuccess = false
@@ -155,6 +112,7 @@ func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string
 }
 
 func MergePullRequest(githubPrService ci.CIService, prNumber int) {
+	time.Sleep(5 * time.Second)
 	combinedStatus, err := githubPrService.GetCombinedPullRequestStatus(prNumber)
 
 	if err != nil {
@@ -181,186 +139,6 @@ func MergePullRequest(githubPrService ci.CIService, prNumber int) {
 	}
 }
 
-func GetGitHubContext(ghContext string) (*models.Github, error) {
-	parsedGhContext := new(models.Github)
-	err := json.Unmarshal([]byte(ghContext), &parsedGhContext)
-	if err != nil {
-		return &models.Github{}, fmt.Errorf("error parsing GitHub context JSON: %v", err)
-	}
-	return parsedGhContext, nil
-}
-
-type ProjectCommand struct {
-	ProjectName      string
-	ProjectDir       string
-	ProjectWorkspace string
-	Terragrunt       bool
-	Commands         []string
-	ApplyStage       *configuration.Stage
-	PlanStage        *configuration.Stage
-	StateEnvVars     map[string]string
-	CommandEnvVars   map[string]string
-}
-
-func ConvertGithubEventToCommands(event models.Event, impactedProjects []configuration.Project, requestedProject *configuration.Project, workflows map[string]configuration.Workflow) ([]ProjectCommand, bool, 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()
-			}
-
-			stateEnvVars, commandEnvVars := collectEnvVars(workflow.EnvVars)
-
-			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:         workflow.Configuration.OnCommitToDefault,
-					ApplyStage:       workflow.Apply,
-					PlanStage:        workflow.Plan,
-					CommandEnvVars:   commandEnvVars,
-					StateEnvVars:     stateEnvVars,
-				})
-			} else if event.Action == "opened" || event.Action == "reopened" || event.Action == "synchronize" {
-				commandsPerProject = append(commandsPerProject, ProjectCommand{
-					ProjectName:      project.Name,
-					ProjectDir:       project.Dir,
-					ProjectWorkspace: project.Workspace,
-					Terragrunt:       project.Terragrunt,
-					Commands:         workflow.Configuration.OnPullRequestPushed,
-					ApplyStage:       workflow.Apply,
-					PlanStage:        workflow.Plan,
-					CommandEnvVars:   commandEnvVars,
-					StateEnvVars:     stateEnvVars,
-				})
-			} else if event.Action == "closed" {
-				commandsPerProject = append(commandsPerProject, ProjectCommand{
-					ProjectName:      project.Name,
-					ProjectDir:       project.Dir,
-					ProjectWorkspace: project.Workspace,
-					Terragrunt:       project.Terragrunt,
-					Commands:         workflow.Configuration.OnPullRequestClosed,
-					ApplyStage:       workflow.Apply,
-					PlanStage:        workflow.Plan,
-					CommandEnvVars:   commandEnvVars,
-					StateEnvVars:     stateEnvVars,
-				})
-			}
-		}
-		return commandsPerProject, true, nil
-	case models.IssueCommentEvent:
-		event := event.(models.IssueCommentEvent)
-		supportedCommands := []string{"digger plan", "digger apply", "digger unlock", "digger lock"}
-
-		coversAllImpactedProjects := true
-
-		runForProjects := impactedProjects
-
-		if requestedProject != nil {
-			if len(impactedProjects) > 1 {
-				coversAllImpactedProjects = false
-				runForProjects = []configuration.Project{*requestedProject}
-			} else if len(impactedProjects) == 1 && impactedProjects[0].Name != requestedProject.Name {
-				return commandsPerProject, false, fmt.Errorf("requested project %v is not impacted by this PR", requestedProject.Name)
-			}
-		}
-
-		for _, command := range supportedCommands {
-			if strings.Contains(event.Comment.Body, command) {
-				for _, project := range runForProjects {
-					workflow, ok := workflows[project.Workflow]
-					if !ok {
-						workflow = *defaultWorkflow()
-					}
-
-					stateEnvVars, commandEnvVars := collectEnvVars(workflow.EnvVars)
-
-					workspace := project.Workspace
-					workspaceOverride, err := parseWorkspace(event.Comment.Body)
-					if err != nil {
-						return []ProjectCommand{}, false, err
-					}
-					if workspaceOverride != "" {
-						workspace = workspaceOverride
-					}
-					commandsPerProject = append(commandsPerProject, ProjectCommand{
-						ProjectName:      project.Name,
-						ProjectDir:       project.Dir,
-						ProjectWorkspace: workspace,
-						Terragrunt:       project.Terragrunt,
-						Commands:         []string{command},
-						ApplyStage:       workflow.Apply,
-						PlanStage:        workflow.Plan,
-						CommandEnvVars:   commandEnvVars,
-						StateEnvVars:     stateEnvVars,
-					})
-				}
-			}
-		}
-		return commandsPerProject, coversAllImpactedProjects, nil
-	default:
-		return []ProjectCommand{}, false, fmt.Errorf("unsupported event type: %T", event)
-	}
-}
-
-func collectEnvVars(envs configuration.EnvVars) (map[string]string, map[string]string) {
-	stateEnvVars := map[string]string{}
-
-	for _, envvar := range envs.State {
-		if envvar.Value != "" {
-			stateEnvVars[envvar.Name] = envvar.Value
-		} else if envvar.ValueFrom != "" {
-			stateEnvVars[envvar.Name] = os.Getenv(envvar.ValueFrom)
-		}
-	}
-
-	commandEnvVars := map[string]string{}
-
-	for _, envvar := range envs.Commands {
-		if envvar.Value != "" {
-			commandEnvVars[envvar.Name] = envvar.Value
-		} else if envvar.ValueFrom != "" {
-			commandEnvVars[envvar.Name] = os.Getenv(envvar.ValueFrom)
-		}
-	}
-	return stateEnvVars, commandEnvVars
-}
-
-func parseWorkspace(comment string) (string, error) {
-	re := regexp.MustCompile(`-w(?:\s+(\S+)|$)`)
-	matches := re.FindAllStringSubmatch(comment, -1)
-
-	if len(matches) == 0 {
-		return "", nil
-	}
-
-	if len(matches) > 1 {
-		return "", errors.New("more than one -w flag found")
-	}
-
-	if len(matches[0]) < 2 || matches[0][1] == "" {
-		return "", errors.New("no value found after -w flag")
-	}
-
-	return matches[0][1], nil
-}
-
-func parseProjectName(comment string) string {
-	re := regexp.MustCompile(`-p ([0-9a-zA-Z\-_]+)`)
-	match := re.FindStringSubmatch(comment)
-	if len(match) > 1 {
-		return match[1]
-	}
-	return ""
-}
-
 type DiggerExecutor struct {
 	RepoOwner         string
 	RepoName          string
@@ -373,8 +151,8 @@ type DiggerExecutor struct {
 	CommandRunner     CommandRun
 	TerraformExecutor terraform.TerraformExecutor
 	CIService         ci.CIService
-	ProjectLock       utils.ProjectLock
-	PlanStorage       utils.PlanStorage
+	ProjectLock       locking.ProjectLock
+	PlanStorage       storage.PlanStorage
 }
 
 type CommandRun interface {
@@ -489,7 +267,10 @@ func (d DiggerExecutor) Plan(prNumber int) (bool, error) {
 				}
 				plan := cleanupTerraformPlan(isNonEmptyPlan, err, stdout, stderr)
 				comment := utils.GetTerraformOutputAsCollapsibleComment("Plan for **"+d.ProjectLock.LockId()+"**", plan)
-				d.CIService.PublishComment(prNumber, comment)
+				err = d.CIService.PublishComment(prNumber, comment)
+				if err != nil {
+					fmt.Printf("error publishing comment: %v", err)
+				}
 			}
 			if step.Action == "run" {
 				var commands []string
@@ -525,7 +306,10 @@ func (d DiggerExecutor) Apply(prNumber int) (bool, error) {
 	}
 	if !isMergeable {
 		comment := "Cannot perform Apply since the PR is not currently mergeable."
-		d.CIService.PublishComment(prNumber, comment)
+		err = d.CIService.PublishComment(prNumber, comment)
+		if err != nil {
+			fmt.Printf("error publishing comment: %v", err)
+		}
 		return false, nil
 	} else {
 		locked, err := d.ProjectLock.Lock(prNumber)
@@ -561,9 +345,15 @@ func (d DiggerExecutor) Apply(prNumber int) (bool, error) {
 					stdout, stderr, err := d.TerraformExecutor.Apply(step.ExtraArgs, plansFilename, d.CommandEnvVars)
 					applyOutput := cleanupTerraformApply(true, err, stdout, stderr)
 					comment := utils.GetTerraformOutputAsCollapsibleComment("Apply for **"+d.ProjectLock.LockId()+"**", applyOutput)
-					d.CIService.PublishComment(prNumber, comment)
+					commentErr := d.CIService.PublishComment(prNumber, comment)
+					if commentErr != nil {
+						fmt.Printf("error publishing comment: %v", err)
+					}
 					if err != nil {
-						d.CIService.PublishComment(prNumber, "Error during applying.")
+						commentErr = d.CIService.PublishComment(prNumber, "Error during applying.")
+						if commentErr != nil {
+							fmt.Printf("error publishing comment: %v", err)
+						}
 						return false, fmt.Errorf("error executing apply: %v", err)
 					}
 				}
@@ -661,55 +451,6 @@ func cleanupTerraformPlan(nonEmptyPlan bool, planError error, stdout string, std
 	return cleanupTerraformOutput(nonEmptyPlan, planError, stdout, stderr, &regex)
 }
 
-func issueCommentEventContainsComment(event models.Event, comment string) bool {
-	switch event.(type) {
-	case models.IssueCommentEvent:
-		event := event.(models.IssueCommentEvent)
-		if strings.Contains(event.Comment.Body, comment) {
-			return true
-		}
-	}
-	return false
-}
-
-func CheckIfHelpComment(event models.Event) bool {
-	return issueCommentEventContainsComment(event, "digger help")
-}
-
-func CheckIfApplyComment(event models.Event) bool {
-	return issueCommentEventContainsComment(event, "digger apply")
-}
-
-func defaultWorkflow() *configuration.Workflow {
-	return &configuration.Workflow{
-		Configuration: &configuration.WorkflowConfiguration{
-			OnCommitToDefault:   []string{"digger unlock"},
-			OnPullRequestPushed: []string{"digger plan"},
-			OnPullRequestClosed: []string{"digger unlock"},
-		},
-		Plan: &configuration.Stage{
-			Steps: []configuration.Step{
-				{
-					Action: "init", ExtraArgs: []string{},
-				},
-				{
-					Action: "plan", ExtraArgs: []string{},
-				},
-			},
-		},
-		Apply: &configuration.Stage{
-			Steps: []configuration.Step{
-				{
-					Action: "init", ExtraArgs: []string{},
-				},
-				{
-					Action: "apply", ExtraArgs: []string{},
-				},
-			},
-		},
-	}
-}
-
 type CIName string
 
 const (
@@ -717,6 +458,7 @@ const (
 	GitHub    = CIName("github")
 	GitLab    = CIName("gitlab")
 	BitBucket = CIName("bitbucket")
+	Azure     = CIName("azure")
 )
 
 func (ci CIName) String() string {
@@ -738,6 +480,9 @@ func DetectCI() CIName {
 	if notEmpty("BITBUCKET_BUILD_NUMBER") {
 		return BitBucket
 	}
+	if notEmpty("AZURE_CI") {
+		return Azure
+	}
 	return None
 
 }
diff --git a/pkg/github/github.go b/pkg/github/github.go
index 4e4967cd..cffcd991 100644
--- a/pkg/github/github.go
+++ b/pkg/github/github.go
@@ -3,13 +3,19 @@ package github
 import (
 	"context"
 	"digger/pkg/ci"
+	"digger/pkg/configuration"
+	"digger/pkg/github/models"
+	dg_models "digger/pkg/models"
+	"digger/pkg/utils"
+	"encoding/json"
+	"fmt"
 	"log"
 	"strings"
 
 	"github.com/google/go-github/v51/github"
 )
 
-func NewGithubPullRequestService(ghToken string, repoName string, owner string) ci.CIService {
+func NewGitHubService(ghToken string, repoName string, owner string) ci.CIService {
 	client := github.NewTokenClient(context.Background(), ghToken)
 	return &GithubService{
 		Client:   client,
@@ -38,11 +44,9 @@ func (svc *GithubService) GetChangedFiles(prNumber int) ([]string, error) {
 	return fileNames, nil
 }
 
-func (svc *GithubService) PublishComment(prNumber int, comment string) {
+func (svc *GithubService) PublishComment(prNumber int, comment string) error {
 	_, _, err := svc.Client.Issues.CreateComment(context.Background(), svc.Owner, svc.RepoName, prNumber, &github.IssueComment{Body: &comment})
-	if err != nil {
-		log.Fatalf("error publishing comment: %v", err)
-	}
+	return err
 }
 
 func (svc *GithubService) SetStatus(prNumber int, status string, statusContext string) error {
@@ -118,3 +122,181 @@ func (svc *GithubService) IsClosed(prNumber int) (bool, error) {
 
 	return pr.GetState() == "closed", nil
 }
+
+func GetGitHubContext(ghContext string) (*models.Github, error) {
+	parsedGhContext := new(models.Github)
+	err := json.Unmarshal([]byte(ghContext), &parsedGhContext)
+	if err != nil {
+		return &models.Github{}, fmt.Errorf("error parsing GitHub context JSON: %v", err)
+	}
+	return parsedGhContext, nil
+}
+
+func ConvertGithubEventToCommands(event models.Event, impactedProjects []configuration.Project, requestedProject *configuration.Project, workflows map[string]configuration.Workflow) ([]dg_models.ProjectCommand, bool, error) {
+	commandsPerProject := make([]dg_models.ProjectCommand, 0)
+
+	switch event.(type) {
+	case models.PullRequestEvent:
+		event := event.(models.PullRequestEvent)
+		for _, project := range impactedProjects {
+			workflow, ok := workflows[project.Workflow]
+			if !ok {
+				return nil, false, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
+			}
+
+			stateEnvVars, commandEnvVars := configuration.CollectEnvVars(workflow.EnvVars)
+
+			if event.Action == "closed" && event.PullRequest.Merged && event.PullRequest.Base.Ref == event.Repository.DefaultBranch {
+				commandsPerProject = append(commandsPerProject, dg_models.ProjectCommand{
+					ProjectName:      project.Name,
+					ProjectDir:       project.Dir,
+					ProjectWorkspace: project.Workspace,
+					Terragrunt:       project.Terragrunt,
+					Commands:         workflow.Configuration.OnCommitToDefault,
+					ApplyStage:       workflow.Apply,
+					PlanStage:        workflow.Plan,
+					CommandEnvVars:   commandEnvVars,
+					StateEnvVars:     stateEnvVars,
+				})
+			} else if event.Action == "opened" || event.Action == "reopened" || event.Action == "synchronize" {
+				commandsPerProject = append(commandsPerProject, dg_models.ProjectCommand{
+					ProjectName:      project.Name,
+					ProjectDir:       project.Dir,
+					ProjectWorkspace: project.Workspace,
+					Terragrunt:       project.Terragrunt,
+					Commands:         workflow.Configuration.OnPullRequestPushed,
+					ApplyStage:       workflow.Apply,
+					PlanStage:        workflow.Plan,
+					CommandEnvVars:   commandEnvVars,
+					StateEnvVars:     stateEnvVars,
+				})
+			} else if event.Action == "closed" {
+				commandsPerProject = append(commandsPerProject, dg_models.ProjectCommand{
+					ProjectName:      project.Name,
+					ProjectDir:       project.Dir,
+					ProjectWorkspace: project.Workspace,
+					Terragrunt:       project.Terragrunt,
+					Commands:         workflow.Configuration.OnPullRequestClosed,
+					ApplyStage:       workflow.Apply,
+					PlanStage:        workflow.Plan,
+					CommandEnvVars:   commandEnvVars,
+					StateEnvVars:     stateEnvVars,
+				})
+			}
+		}
+		return commandsPerProject, true, nil
+	case models.IssueCommentEvent:
+		event := event.(models.IssueCommentEvent)
+		supportedCommands := []string{"digger plan", "digger apply", "digger unlock", "digger lock"}
+
+		coversAllImpactedProjects := true
+
+		runForProjects := impactedProjects
+
+		if requestedProject != nil {
+			if len(impactedProjects) > 1 {
+				coversAllImpactedProjects = false
+				runForProjects = []configuration.Project{*requestedProject}
+			} else if len(impactedProjects) == 1 && impactedProjects[0].Name != requestedProject.Name {
+				return commandsPerProject, false, fmt.Errorf("requested project %v is not impacted by this PR", requestedProject.Name)
+			}
+		}
+
+		for _, command := range supportedCommands {
+			if strings.Contains(event.Comment.Body, command) {
+				for _, project := range runForProjects {
+					workflow, ok := workflows[project.Workflow]
+					if !ok {
+						return nil, false, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
+					}
+
+					stateEnvVars, commandEnvVars := configuration.CollectEnvVars(workflow.EnvVars)
+
+					workspace := project.Workspace
+					workspaceOverride, err := utils.ParseWorkspace(event.Comment.Body)
+					if err != nil {
+						return []dg_models.ProjectCommand{}, false, err
+					}
+					if workspaceOverride != "" {
+						workspace = workspaceOverride
+					}
+					commandsPerProject = append(commandsPerProject, dg_models.ProjectCommand{
+						ProjectName:      project.Name,
+						ProjectDir:       project.Dir,
+						ProjectWorkspace: workspace,
+						Terragrunt:       project.Terragrunt,
+						Commands:         []string{command},
+						ApplyStage:       workflow.Apply,
+						PlanStage:        workflow.Plan,
+						CommandEnvVars:   commandEnvVars,
+						StateEnvVars:     stateEnvVars,
+					})
+				}
+			}
+		}
+		return commandsPerProject, coversAllImpactedProjects, nil
+	default:
+		return []dg_models.ProjectCommand{}, false, fmt.Errorf("unsupported event type: %T", event)
+	}
+}
+
+func ProcessGitHubEvent(ghEvent models.Event, diggerConfig *configuration.DiggerConfig, ciService ci.CIService) ([]configuration.Project, *configuration.Project, int, error) {
+	var impactedProjects []configuration.Project
+	var prNumber int
+
+	switch ghEvent.(type) {
+	case models.PullRequestEvent:
+		prNumber = ghEvent.(models.PullRequestEvent).PullRequest.Number
+		changedFiles, err := ciService.GetChangedFiles(prNumber)
+
+		if err != nil {
+			return nil, nil, 0, fmt.Errorf("could not get changed files")
+		}
+
+		impactedProjects = diggerConfig.GetModifiedProjects(changedFiles)
+	case models.IssueCommentEvent:
+		prNumber = ghEvent.(models.IssueCommentEvent).Issue.Number
+		changedFiles, err := ciService.GetChangedFiles(prNumber)
+
+		if err != nil {
+			return nil, nil, 0, fmt.Errorf("could not get changed files")
+		}
+
+		impactedProjects = diggerConfig.GetModifiedProjects(changedFiles)
+		requestedProject := utils.ParseProjectName(ghEvent.(models.IssueCommentEvent).Comment.Body)
+
+		if requestedProject == "" {
+			return impactedProjects, nil, prNumber, nil
+		}
+
+		for _, project := range impactedProjects {
+			if project.Name == requestedProject {
+				return impactedProjects, &project, prNumber, nil
+			}
+		}
+		return nil, nil, 0, fmt.Errorf("requested project not found in modified projects")
+
+	default:
+		return nil, nil, 0, fmt.Errorf("unsupported event type")
+	}
+	return impactedProjects, nil, prNumber, nil
+}
+
+func issueCommentEventContainsComment(event models.Event, comment string) bool {
+	switch event.(type) {
+	case models.IssueCommentEvent:
+		event := event.(models.IssueCommentEvent)
+		if strings.Contains(event.Comment.Body, comment) {
+			return true
+		}
+	}
+	return false
+}
+
+func CheckIfHelpComment(event models.Event) bool {
+	return issueCommentEventContainsComment(event, "digger help")
+}
+
+func CheckIfApplyComment(event models.Event) bool {
+	return issueCommentEventContainsComment(event, "digger apply")
+}
diff --git a/pkg/github/models/models.go b/pkg/github/models/models.go
new file mode 100644
index 00000000..36b2dab0
--- /dev/null
+++ b/pkg/github/models/models.go
@@ -0,0 +1,112 @@
+package models
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+)
+
+type Github struct {
+	Action           string `json:"action"`
+	ActionPath       string `json:"action_path"`
+	ActionRef        string `json:"action_ref"`
+	ActionRepository string `json:"action_repository"`
+	ActionStatus     string `json:"action_status"`
+	Actor            string `json:"actor"`
+	BaseRef          string `json:"base_ref"`
+	Env              string `json:"env"`
+	Event            Event  `json:"event"`
+	EventName        string `json:"event_name"`
+	EventPath        string `json:"event_path"`
+	Path             string `json:"path"`
+	RefType          string `json:"ref_type"`
+	Repository       string `json:"repository"`
+	RepositoryOwner  string `json:"repository_owner"`
+}
+
+type Event interface{}
+
+func (g *Github) UnmarshalJSON(data []byte) error {
+	type Alias Github
+	aux := struct {
+		*Alias
+	}{
+		Alias: (*Alias)(g),
+	}
+	if err := json.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+
+	var rawEvent json.RawMessage
+	auxEvent := struct {
+		Event *json.RawMessage `json:"event"`
+	}{
+		Event: &rawEvent,
+	}
+	if err := json.Unmarshal(data, &auxEvent); err != nil {
+		return err
+	}
+
+	switch g.EventName {
+	case "pull_request":
+		var event PullRequestEvent
+		if err := json.Unmarshal(rawEvent, &event); err != nil {
+			return err
+		}
+		g.Event = event
+	case "issue_comment":
+		var event IssueCommentEvent
+		if err := json.Unmarshal(rawEvent, &event); err != nil {
+			return err
+		}
+		g.Event = event
+	default:
+		return errors.New("unknown GitHub event: " + g.EventName)
+	}
+
+	return nil
+}
+
+type PullRequestEvent struct {
+	Action      string      `json:"action"`
+	Number      int         `json:"number"`
+	PullRequest PullRequest `json:"pull_request"`
+	Repository  Repository  `json:"repository"`
+}
+
+type PullRequest struct {
+	Number int  `json:"number"`
+	Merged bool `json:"merged"`
+	Base   Base `json:"base"`
+}
+
+type IssueCommentEvent struct {
+	Action  string  `json:"action"`
+	Comment Comment `json:"comment"`
+	Issue   Issue   `json:"issue"`
+}
+
+type Base struct {
+	Ref string `json:"ref"`
+}
+
+type Comment struct {
+	Body string `json:"body"`
+}
+
+type Issue struct {
+	Number int `json:"number"`
+}
+
+type Repository struct {
+	DefaultBranch string `json:"default_branch"`
+}
+
+func GetGitHubContext(ghContext string) (Github, error) {
+	var parsedGhContext Github
+	err := json.Unmarshal([]byte(ghContext), &parsedGhContext)
+	if err != nil {
+		return Github{}, fmt.Errorf("error parsing GitHub context JSON: %v", err)
+	}
+	return parsedGhContext, nil
+}
diff --git a/pkg/gitlab/gitlab.go b/pkg/gitlab/gitlab.go
index dea32a43..73239eb4 100644
--- a/pkg/gitlab/gitlab.go
+++ b/pkg/gitlab/gitlab.go
@@ -4,9 +4,11 @@ import (
 	"digger/pkg/ci"
 	"digger/pkg/configuration"
 	"digger/pkg/digger"
+	"digger/pkg/locking"
+	"digger/pkg/models"
+	"digger/pkg/storage"
 	"digger/pkg/terraform"
 	"digger/pkg/usage"
-	"digger/pkg/utils"
 	"fmt"
 	"github.com/caarlos0/env/v7"
 	go_gitlab "github.com/xanzy/go-gitlab"
@@ -30,9 +32,11 @@ type GitLabContext struct {
 	ProjectNamespace   string          `env:"CI_PROJECT_NAMESPACE"`
 	ProjectId          *int            `env:"CI_PROJECT_ID"`
 	ProjectNamespaceId *int            `env:"CI_PROJECT_NAMESPACE_ID"`
+	OpenMergeRequests  []string        `env:"CI_OPEN_MERGE_REQUESTS"`
 	Token              string          `env:"GITLAB_TOKEN"`
 	DiggerCommand      string          `env:"DIGGER_COMMAND"`
 	DiscussionID       string          `env:"DISCUSSION_ID"`
+	IsMeargeable       bool            `env:"IS_MERGEABLE"`
 }
 
 type PipelineSourceType string
@@ -88,8 +92,7 @@ func ProcessGitLabEvent(gitlabContext *GitLabContext, diggerConfig *configuratio
 	var impactedProjects []configuration.Project
 
 	if gitlabContext.MergeRequestIId == nil {
-		println("Merge Request ID is not found.")
-		return nil, nil
+		return nil, fmt.Errorf("value for 'Merge Request ID' parameter is not found")
 	}
 
 	mergeRequestId := gitlabContext.MergeRequestIId
@@ -133,67 +136,66 @@ func (gitlabService GitLabService) GetChangedFiles(mergeRequestId int) ([]string
 	return fileNames, nil
 }
 
-func (gitlabService GitLabService) PublishComment(mergeRequestID int, comment string) {
+func (gitlabService GitLabService) PublishComment(mergeRequestID int, comment string) error {
 	discussionId := gitlabService.Context.DiscussionID
 	projectId := *gitlabService.Context.ProjectId
 	mergeRequestIID := *gitlabService.Context.MergeRequestIId
 	commentOpt := &go_gitlab.AddMergeRequestDiscussionNoteOptions{Body: &comment}
 
-	fmt.Printf("PublishComment mergeRequestID : %d, projectId: %d, mergeRequestIID: %d, \n", mergeRequestID, projectId, mergeRequestIID)
+	fmt.Printf("PublishComment mergeRequestID : %d, projectId: %d, mergeRequestIID: %d, discussionId: %s \n", mergeRequestID, projectId, mergeRequestIID, discussionId)
 
-	_, _, err := gitlabService.Client.Discussions.AddMergeRequestDiscussionNote(projectId, mergeRequestIID, discussionId, commentOpt)
-	if err != nil {
-		fmt.Printf("Failed to publish a comment. %v\n", err)
-		print(err.Error())
+	if discussionId == "" {
+		commentOpt := &go_gitlab.CreateMergeRequestDiscussionOptions{Body: &comment}
+		discussion, _, err := gitlabService.Client.Discussions.CreateMergeRequestDiscussion(projectId, mergeRequestIID, commentOpt)
+		if err != nil {
+			fmt.Printf("Failed to publish a comment. %v\n", err)
+			print(err.Error())
+		}
+		discussionId = discussion.ID
+		return err
+	} else {
+		_, _, err := gitlabService.Client.Discussions.AddMergeRequestDiscussionNote(projectId, mergeRequestIID, discussionId, commentOpt)
+		if err != nil {
+			fmt.Printf("Failed to publish a comment. %v\n", err)
+			print(err.Error())
+		}
+		return err
 	}
 }
 
+// SetStatus GitLab implementation is using https://docs.gitlab.com/15.11/ee/api/status_checks.html (external status checks)
+// https://docs.gitlab.com/ee/user/project/merge_requests/status_checks.html#add-a-status-check-service
+// only supported by 'Ultimate' plan
 func (gitlabService GitLabService) SetStatus(mergeRequestID int, status string, statusContext string) error {
 	//TODO implement me
 	fmt.Printf("SetStatus: mergeRequest: %d, status: %s, statusContext: %s\n", mergeRequestID, status, statusContext)
 	return nil
-	//panic("SetStatus: implement me")
 }
 
 func (gitlabService GitLabService) GetCombinedPullRequestStatus(mergeRequestID int) (string, error) {
 	//TODO implement me
 
-	panic("GetCombinedPullRequestStatus: implement me")
+	return "success", nil
 }
 
 func (gitlabService GitLabService) MergePullRequest(mergeRequestID int) error {
 	projectId := *gitlabService.Context.ProjectId
 	mergeRequestIID := *gitlabService.Context.MergeRequestIId
-	opt := &go_gitlab.AcceptMergeRequestOptions{}
+	mergeWhenPipelineSucceeds := true
+	opt := &go_gitlab.AcceptMergeRequestOptions{MergeWhenPipelineSucceeds: &mergeWhenPipelineSucceeds}
 
 	fmt.Printf("MergePullRequest mergeRequestID : %d, projectId: %d, mergeRequestIID: %d, \n", mergeRequestID, projectId, mergeRequestIID)
 
 	_, _, err := gitlabService.Client.MergeRequests.AcceptMergeRequest(projectId, mergeRequestIID, opt)
 	if err != nil {
 		fmt.Printf("Failed to merge Merge Request. %v\n", err)
-		return err
+		return fmt.Errorf("Failed to merge Merge Request. %v\n", err)
 	}
 	return nil
 }
 
 func (gitlabService GitLabService) IsMergeable(mergeRequestID int) (bool, error) {
-	projectId := *gitlabService.Context.ProjectId
-	mergeRequestIID := *gitlabService.Context.MergeRequestIId
-
-	fmt.Printf("IsMergeable mergeRequestIID : %d, projectId: %d \n", mergeRequestIID, projectId)
-	opt := &go_gitlab.GetMergeRequestsOptions{}
-
-	mergeRequest, _, err := gitlabService.Client.MergeRequests.GetMergeRequest(projectId, mergeRequestIID, opt)
-
-	if err != nil {
-		fmt.Printf("Failed to get a MergeRequest: %d, %v \n", mergeRequestIID, err)
-		print(err.Error())
-	}
-
-	if mergeRequest.DetailedMergeStatus == "mergeable" {
-		return true, nil
-	}
-	return false, nil
+	return gitlabService.Context.IsMeargeable, nil
 }
 
 func (gitlabService GitLabService) IsClosed(mergeRequestID int) (bool, error) {
@@ -227,24 +229,32 @@ func (e GitLabEventType) String() string {
 }
 
 const (
-	MergeRequestOpened  = GitLabEventType("merge_request_opened")
-	MergeRequestUpdated = GitLabEventType("merge_request_updated")
-	MergeRequestClosed  = GitLabEventType("merge_request_closed")
+	MergeRequestOpened     = GitLabEventType("merge_request_opened")
+	MergeRequestClosed     = GitLabEventType("merge_request_closed")
+	MergeRequestReopened   = GitLabEventType("merge_request_reopened")
+	MergeRequestUpdated    = GitLabEventType("merge_request_updated")
+	MergeRequestApproved   = GitLabEventType("merge_request_approved")
+	MergeRequestUnapproved = GitLabEventType("merge_request_unapproved")
+	MergeRequestApproval   = GitLabEventType("merge_request_approval")
+	MergeRequestUnapproval = GitLabEventType("merge_request_unapproval")
+	MergeRequestMerged     = GitLabEventType("merge_request_merge")
+
 	MergeRequestComment = GitLabEventType("merge_request_commented")
 )
 
-func ConvertGitLabEventToCommands(event GitLabEvent, gitLabContext *GitLabContext, impactedProjects []configuration.Project, workflows map[string]configuration.Workflow) ([]digger.ProjectCommand, error) {
-	commandsPerProject := make([]digger.ProjectCommand, 0)
+func ConvertGitLabEventToCommands(event GitLabEvent, gitLabContext *GitLabContext, impactedProjects []configuration.Project, workflows map[string]configuration.Workflow) ([]models.ProjectCommand, error) {
+	commandsPerProject := make([]models.ProjectCommand, 0)
 
+	fmt.Printf("ConvertGitLabEventToCommands, event.EventType: %s\n", event.EventType)
 	switch event.EventType {
-	case MergeRequestOpened:
+	case MergeRequestOpened, MergeRequestReopened, MergeRequestUpdated:
 		for _, project := range impactedProjects {
 			workflow, ok := workflows[project.Workflow]
 			if !ok {
-				workflow = workflows["default"]
+				return nil, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
 			}
 
-			commandsPerProject = append(commandsPerProject, digger.ProjectCommand{
+			commandsPerProject = append(commandsPerProject, models.ProjectCommand{
 				ProjectName:      project.Name,
 				ProjectDir:       project.Dir,
 				ProjectWorkspace: project.Workspace,
@@ -255,31 +265,13 @@ func ConvertGitLabEventToCommands(event GitLabEvent, gitLabContext *GitLabContex
 			})
 		}
 		return commandsPerProject, nil
-	case MergeRequestUpdated:
+	case MergeRequestClosed, MergeRequestMerged:
 		for _, project := range impactedProjects {
 			workflow, ok := workflows[project.Workflow]
 			if !ok {
-				workflow = workflows["default"]
+				return nil, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name)
 			}
-
-			commandsPerProject = append(commandsPerProject, digger.ProjectCommand{
-				ProjectName:      project.Name,
-				ProjectDir:       project.Dir,
-				ProjectWorkspace: project.Workspace,
-				Terragrunt:       project.Terragrunt,
-				Commands:         workflow.Configuration.OnPullRequestPushed,
-				ApplyStage:       workflow.Apply,
-				PlanStage:        workflow.Plan,
-			})
-		}
-		return commandsPerProject, nil
-	case MergeRequestClosed:
-		for _, project := range impactedProjects {
-			workflow, ok := workflows[project.Workflow]
-			if !ok {
-				workflow = workflows["default"]
-			}
-			commandsPerProject = append(commandsPerProject, digger.ProjectCommand{
+			commandsPerProject = append(commandsPerProject, models.ProjectCommand{
 				ProjectName:      project.Name,
 				ProjectDir:       project.Dir,
 				ProjectWorkspace: project.Workspace,
@@ -304,7 +296,7 @@ func ConvertGitLabEventToCommands(event GitLabEvent, gitLabContext *GitLabContex
 					//if workspaceOverride != "" {
 					//	workspace = workspaceOverride
 					//}
-					commandsPerProject = append(commandsPerProject, digger.ProjectCommand{
+					commandsPerProject = append(commandsPerProject, models.ProjectCommand{
 						ProjectName:      project.Name,
 						ProjectDir:       project.Dir,
 						ProjectWorkspace: workspace,
@@ -317,18 +309,19 @@ func ConvertGitLabEventToCommands(event GitLabEvent, gitLabContext *GitLabContex
 		return commandsPerProject, nil
 
 	default:
-		return []digger.ProjectCommand{}, fmt.Errorf("unsupported GitLab event type: %v", event)
+		return []models.ProjectCommand{}, fmt.Errorf("unsupported GitLab event type: %v", event)
 	}
+	return nil, nil
 }
 
-func RunCommandsPerProject(commandsPerProject []digger.ProjectCommand, gitLabContext GitLabContext, diggerConfig *configuration.DiggerConfig, service ci.CIService, lock utils.Lock, planStorage utils.PlanStorage, workingDir string) (bool, error) {
+func RunCommandsPerProject(commandsPerProject []models.ProjectCommand, gitLabContext GitLabContext, diggerConfig *configuration.DiggerConfig, service ci.CIService, lock locking.Lock, planStorage storage.PlanStorage, workingDir string) (bool, error) {
 
 	allAppliesSuccess := true
 	appliesPerProject := make(map[string]bool)
 	for _, projectCommands := range commandsPerProject {
 		appliesPerProject[projectCommands.ProjectName] = false
 		for _, command := range projectCommands.Commands {
-			projectLock := &utils.ProjectLockImpl{
+			projectLock := &locking.ProjectLockImpl{
 				InternalLock: lock,
 				CIService:    service,
 				ProjectName:  projectCommands.ProjectName,
@@ -338,7 +331,6 @@ func RunCommandsPerProject(commandsPerProject []digger.ProjectCommand, gitLabCon
 
 			var terraformExecutor terraform.TerraformExecutor
 			projectPath := path.Join(workingDir, projectCommands.ProjectDir)
-			fmt.Printf("pprojectPath: %s\n\n", projectPath)
 			if projectCommands.Terragrunt {
 				terraformExecutor = terraform.Terragrunt{WorkingDir: path.Join(workingDir, projectCommands.ProjectDir)}
 			} else {
@@ -417,52 +409,4 @@ func RunCommandsPerProject(commandsPerProject []digger.ProjectCommand, gitLabCon
 		}
 	}
 	return allAppliesSuccess, nil
-	/*
-
-		lockAcquisitionSuccess := true
-		for _, projectCommands := range commandsPerProject {
-			for _, command := range projectCommands.Commands {
-				projectLock := &utils.ProjectLockImpl{
-					InternalLock: lock,
-					CIService:    service,
-					ProjectName:  projectCommands.ProjectName,
-					RepoName:     gitLabContext.ProjectName,
-					RepoOwner:    gitLabContext.ProjectNamespace,
-				}
-				diggerExecutor := digger.DiggerExecutor{
-					workingDir,
-					projectCommands.ProjectWorkspace,
-					gitLabContext.ProjectNamespace,
-					projectCommands.ProjectName,
-					projectCommands.ProjectDir,
-					gitLabContext.ProjectName,
-					projectCommands.Terragrunt,
-					service,
-					projectLock,
-					diggerConfig,
-				}
-				switch command {
-				case "digger plan":
-					utils.SendUsageRecord(gitLabContext.ProjectNamespace, gitLabContext.EventType.String(), "plan")
-					diggerExecutor.Plan(*gitLabContext.MergeRequestIId)
-				case "digger apply":
-					utils.SendUsageRecord(gitLabContext.ProjectName, gitLabContext.EventType.String(), "apply")
-					diggerExecutor.Apply(*gitLabContext.MergeRequestIId)
-				case "digger unlock":
-					utils.SendUsageRecord(gitLabContext.ProjectNamespace, gitLabContext.EventType.String(), "unlock")
-					diggerExecutor.Unlock(*gitLabContext.MergeRequestIId)
-				case "digger lock":
-					utils.SendUsageRecord(gitLabContext.ProjectNamespace, gitLabContext.EventType.String(), "lock")
-					lockAcquisitionSuccess = diggerExecutor.Lock(*gitLabContext.MergeRequestIId)
-				}
-			}
-		}
-
-		if !lockAcquisitionSuccess {
-			os.Exit(1)
-		}
-		return nil
-
-	*/
-
 }
diff --git a/pkg/utils/locking.go b/pkg/locking/locking.go
similarity index 99%
rename from pkg/utils/locking.go
rename to pkg/locking/locking.go
index 014a1e2c..a8610193 100644
--- a/pkg/utils/locking.go
+++ b/pkg/locking/locking.go
@@ -1,4 +1,4 @@
-package utils
+package locking
 
 import (
 	"digger/pkg/aws"
diff --git a/pkg/models/models.go b/pkg/models/models.go
index 36b2dab0..19f5956b 100644
--- a/pkg/models/models.go
+++ b/pkg/models/models.go
@@ -1,112 +1,15 @@
 package models
 
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-)
-
-type Github struct {
-	Action           string `json:"action"`
-	ActionPath       string `json:"action_path"`
-	ActionRef        string `json:"action_ref"`
-	ActionRepository string `json:"action_repository"`
-	ActionStatus     string `json:"action_status"`
-	Actor            string `json:"actor"`
-	BaseRef          string `json:"base_ref"`
-	Env              string `json:"env"`
-	Event            Event  `json:"event"`
-	EventName        string `json:"event_name"`
-	EventPath        string `json:"event_path"`
-	Path             string `json:"path"`
-	RefType          string `json:"ref_type"`
-	Repository       string `json:"repository"`
-	RepositoryOwner  string `json:"repository_owner"`
-}
-
-type Event interface{}
-
-func (g *Github) UnmarshalJSON(data []byte) error {
-	type Alias Github
-	aux := struct {
-		*Alias
-	}{
-		Alias: (*Alias)(g),
-	}
-	if err := json.Unmarshal(data, &aux); err != nil {
-		return err
-	}
-
-	var rawEvent json.RawMessage
-	auxEvent := struct {
-		Event *json.RawMessage `json:"event"`
-	}{
-		Event: &rawEvent,
-	}
-	if err := json.Unmarshal(data, &auxEvent); err != nil {
-		return err
-	}
-
-	switch g.EventName {
-	case "pull_request":
-		var event PullRequestEvent
-		if err := json.Unmarshal(rawEvent, &event); err != nil {
-			return err
-		}
-		g.Event = event
-	case "issue_comment":
-		var event IssueCommentEvent
-		if err := json.Unmarshal(rawEvent, &event); err != nil {
-			return err
-		}
-		g.Event = event
-	default:
-		return errors.New("unknown GitHub event: " + g.EventName)
-	}
-
-	return nil
-}
-
-type PullRequestEvent struct {
-	Action      string      `json:"action"`
-	Number      int         `json:"number"`
-	PullRequest PullRequest `json:"pull_request"`
-	Repository  Repository  `json:"repository"`
-}
-
-type PullRequest struct {
-	Number int  `json:"number"`
-	Merged bool `json:"merged"`
-	Base   Base `json:"base"`
-}
-
-type IssueCommentEvent struct {
-	Action  string  `json:"action"`
-	Comment Comment `json:"comment"`
-	Issue   Issue   `json:"issue"`
-}
-
-type Base struct {
-	Ref string `json:"ref"`
-}
-
-type Comment struct {
-	Body string `json:"body"`
-}
-
-type Issue struct {
-	Number int `json:"number"`
-}
-
-type Repository struct {
-	DefaultBranch string `json:"default_branch"`
-}
-
-func GetGitHubContext(ghContext string) (Github, error) {
-	var parsedGhContext Github
-	err := json.Unmarshal([]byte(ghContext), &parsedGhContext)
-	if err != nil {
-		return Github{}, fmt.Errorf("error parsing GitHub context JSON: %v", err)
-	}
-	return parsedGhContext, nil
+import "digger/pkg/configuration"
+
+type ProjectCommand struct {
+	ProjectName      string
+	ProjectDir       string
+	ProjectWorkspace string
+	Terragrunt       bool
+	Commands         []string
+	ApplyStage       *configuration.Stage
+	PlanStage        *configuration.Stage
+	StateEnvVars     map[string]string
+	CommandEnvVars   map[string]string
 }
diff --git a/pkg/utils/plan_storage.go b/pkg/storage/plan_storage.go
similarity index 98%
rename from pkg/utils/plan_storage.go
rename to pkg/storage/plan_storage.go
index 87d715ff..9bf216a3 100644
--- a/pkg/utils/plan_storage.go
+++ b/pkg/storage/plan_storage.go
@@ -1,7 +1,8 @@
-package utils
+package storage
 
 import (
 	"context"
+	"digger/pkg/utils"
 	"fmt"
 	"io"
 	"net/http"
@@ -32,7 +33,7 @@ type GithubPlanStorage struct {
 	Owner             string
 	RepoName          string
 	PullRequestNumber int
-	ZipManager        Zipper
+	ZipManager        utils.Zipper
 }
 
 func (psg *PlanStorageGcp) PlanExists(storedPlanFilePath string) (bool, error) {
diff --git a/pkg/usage/usage.go b/pkg/usage/usage.go
index dba316d8..c6d4c105 100644
--- a/pkg/usage/usage.go
+++ b/pkg/usage/usage.go
@@ -77,7 +77,7 @@ func init() {
 		fmt.Printf("Failed to get current dir. %s", err)
 	}
 	walker := configuration.FileSystemDirWalker{}
-	config, err := configuration.NewDiggerConfig(currentDir, &walker)
+	config, err := configuration.LoadDiggerConfig(currentDir, &walker)
 	if err != nil {
 		return
 	}
diff --git a/pkg/utils/commands.go b/pkg/utils/commands.go
index 305a8513..21e6cd9b 100644
--- a/pkg/utils/commands.go
+++ b/pkg/utils/commands.go
@@ -1,6 +1,10 @@
 package utils
 
-import "fmt"
+import (
+	"errors"
+	"fmt"
+	"regexp"
+)
 
 type Commad struct {
 	Name        string
@@ -31,3 +35,31 @@ func GetCommands() string {
 	}
 	return commands
 }
+
+func ParseProjectName(comment string) string {
+	re := regexp.MustCompile(`-p ([0-9a-zA-Z\-_]+)`)
+	match := re.FindStringSubmatch(comment)
+	if len(match) > 1 {
+		return match[1]
+	}
+	return ""
+}
+
+func ParseWorkspace(comment string) (string, error) {
+	re := regexp.MustCompile(`-w(?:\s+(\S+)|$)`)
+	matches := re.FindAllStringSubmatch(comment, -1)
+
+	if len(matches) == 0 {
+		return "", nil
+	}
+
+	if len(matches) > 1 {
+		return "", errors.New("more than one -w flag found")
+	}
+
+	if len(matches[0]) < 2 || matches[0][1] == "" {
+		return "", errors.New("no value found after -w flag")
+	}
+
+	return matches[0][1], nil
+}
diff --git a/pkg/utils/mocks.go b/pkg/utils/mocks.go
index 39392e4e..4d1e6913 100644
--- a/pkg/utils/mocks.go
+++ b/pkg/utils/mocks.go
@@ -46,8 +46,8 @@ type MockPullRequestManager struct {
 func (t MockPullRequestManager) GetChangedFiles(prNumber int) ([]string, error) {
 	return t.ChangedFiles, nil
 }
-func (t MockPullRequestManager) PublishComment(prNumber int, comment string) {
-
+func (t MockPullRequestManager) PublishComment(prNumber int, comment string) error {
+	return nil
 }
 
 func (t MockPullRequestManager) SetStatus(prNumber int, status string, statusContext string) error {
diff --git a/pkg/github/pullrequestmanager_mock.go b/pkg/utils/pullrequestmanager_mock.go
similarity index 95%
rename from pkg/github/pullrequestmanager_mock.go
rename to pkg/utils/pullrequestmanager_mock.go
index 4647baac..ad80df8e 100644
--- a/pkg/github/pullrequestmanager_mock.go
+++ b/pkg/utils/pullrequestmanager_mock.go
@@ -1,4 +1,4 @@
-package github
+package utils
 
 type MockGithubPullrequestManager struct {
 	commands []string
@@ -9,8 +9,9 @@ func (mockGithubPullrequestManager *MockGithubPullrequestManager) GetChangedFile
 	return nil, nil
 }
 
-func (mockGithubPullrequestManager *MockGithubPullrequestManager) PublishComment(prNumber int, comment string) {
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) PublishComment(prNumber int, comment string) error {
 	mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "PublishComment")
+	return nil
 }
 
 func (mockGithubPullrequestManager *MockGithubPullrequestManager) SetStatus(prNumber int, status string, statusContext string) error {


Test patch:
diff --git a/cmd/digger/main_test.go b/cmd/digger/main_test.go
index 84442006..500ca663 100644
--- a/cmd/digger/main_test.go
+++ b/cmd/digger/main_test.go
@@ -3,7 +3,8 @@ package main
 import (
 	"digger/pkg/configuration"
 	"digger/pkg/digger"
-	"digger/pkg/models"
+	"digger/pkg/github"
+	"digger/pkg/github/models"
 	"digger/pkg/utils"
 	"fmt"
 	"testing"
@@ -863,7 +864,7 @@ var githubInvalidContextJson = `{
 
 func TestGitHubNewPullRequestContext(t *testing.T) {
 
-	context, err := digger.GetGitHubContext(githubContextNewPullRequestJson)
+	context, err := github.GetGitHubContext(githubContextNewPullRequestJson)
 	assert.NoError(t, err)
 	if err != nil {
 		fmt.Println(err)
@@ -876,9 +877,9 @@ func TestGitHubNewPullRequestContext(t *testing.T) {
 	prManager := &utils.MockPullRequestManager{ChangedFiles: []string{"dev/test.tf"}}
 	planStorage := &utils.MockPlanStorage{}
 
-	impactedProjects, requestedProject, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
+	impactedProjects, requestedProject, prNumber, err := github.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
 
-	commandsToRunPerProject, _, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, map[string]configuration.Workflow{})
+	commandsToRunPerProject, _, err := github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, map[string]configuration.Workflow{})
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, prManager, lock, planStorage, "")
 
 	assert.NoError(t, err)
@@ -899,9 +900,9 @@ func TestGitHubNewCommentContext(t *testing.T) {
 	lock := &utils.MockLock{}
 	prManager := &utils.MockPullRequestManager{ChangedFiles: []string{"dev/test.tf"}}
 	planStorage := &utils.MockPlanStorage{}
-	impactedProjects, requestedProject, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
+	impactedProjects, requestedProject, prNumber, err := github.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
 
-	commandsToRunPerProject, _, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, map[string]configuration.Workflow{})
+	commandsToRunPerProject, _, err := github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, map[string]configuration.Workflow{})
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, prManager, lock, planStorage, "")
 
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, prManager, lock, planStorage, "")
@@ -921,7 +922,7 @@ func TestInvalidGitHubContext(t *testing.T) {
 }
 
 func TestGitHubNewPullRequestInMultiEnvProjectContext(t *testing.T) {
-	context, err := digger.GetGitHubContext(githubContextNewPullRequestJson)
+	context, err := github.GetGitHubContext(githubContextNewPullRequestJson)
 	assert.NoError(t, err)
 	ghEvent := context.Event
 	pullRequestNumber := 11
@@ -965,9 +966,9 @@ func TestGitHubNewPullRequestInMultiEnvProjectContext(t *testing.T) {
 	// PullRequestManager Mock
 	prManager := &utils.MockPullRequestManager{ChangedFiles: []string{"dev/test.tf"}}
 	lock := &utils.MockLock{}
-	impactedProjects, requestedProject, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
+	impactedProjects, requestedProject, prNumber, err := github.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, workflows)
+	commandsToRunPerProject, _, err := github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, workflows)
 	spew.Dump(lock.MapLock)
 	assert.Equal(t, pullRequestNumber, prNumber)
 	assert.Equal(t, 1, len(commandsToRunPerProject))
diff --git a/pkg/azure/azure_test.go b/pkg/azure/azure_test.go
new file mode 100644
index 00000000..b3ad4264
--- /dev/null
+++ b/pkg/azure/azure_test.go
@@ -0,0 +1,14 @@
+package azure
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestGetAzureReposContext(t *testing.T) {
+
+	context := "{\n    \"id\": \"a5425068-eade-4753-a703-2a7670b94fca\",\n    \"eventType\": \"ms.vss-code.git-pullrequest-comment-event\",\n    \"publisherId\": \"tfs\",\n    \"message\": {\n        \"text\": \"Mohamed Habib has commented on a pull request\",\n        \"html\": \"Mohamed Habib has <a href=\\\"https://dev.azure.com/moehabib9/digger-test/_git/digger-test/pullrequest/1?discussionId=1\\\">commented</a> on a pull request\",\n        \"markdown\": \"Mohamed Habib has [commented](https://dev.azure.com/moehabib9/digger-test/_git/digger-test/pullrequest/1?discussionId=1) on a pull request\"\n    },\n    \"detailedMessage\": {\n        \"text\": \"Mohamed Habib has commented on a pull request\\r\\ndigger plan\\r\\n\",\n        \"html\": \"Mohamed Habib has <a href=\\\"https://dev.azure.com/moehabib9/digger-test/_git/digger-test/pullrequest/1?discussionId=1\\\">commented</a> on a pull request<p>digger plan</p>\",\n        \"markdown\": \"Mohamed Habib has [commented](https://dev.azure.com/moehabib9/digger-test/_git/digger-test/pullrequest/1?discussionId=1) on a pull request\\r\\ndigger plan\\r\\n\"\n    },\n    \"resource\": {\n        \"comment\": {\n            \"id\": 1,\n            \"parentCommentId\": 0,\n            \"author\": {\n                \"displayName\": \"Mohamed Habib\",\n                \"url\": \"https://spsprodweu5.vssps.visualstudio.com/A8e479c0b-ee1b-43c7-9ed8-98f2a7ec6d01/_apis/Identities/d08e23e0-8b42-6ab5-a2b4-5f1dd4d1ed68\",\n                \"_links\": {\n                    \"avatar\": {\n                        \"href\": \"https://dev.azure.com/moehabib9/_apis/GraphProfile/MemberAvatars/msa.ZDA4ZTIzZTAtOGI0Mi03YWI1LWEyYjQtNWYxZGQ0ZDFlZDY4\"\n                    }\n                },\n                \"id\": \"d08e23e0-8b42-6ab5-a2b4-5f1dd4d1ed68\",\n                \"uniqueName\": \"moe.habib9@gmail.com\",\n                \"imageUrl\": \"https://dev.azure.com/moehabib9/_apis/GraphProfile/MemberAvatars/msa.ZDA4ZTIzZTAtOGI0Mi03YWI1LWEyYjQtNWYxZGQ0ZDFlZDY4\",\n                \"descriptor\": \"msa.ZDA4ZTIzZTAtOGI0Mi03YWI1LWEyYjQtNWYxZGQ0ZDFlZDY4\"\n            },\n            \"content\": \"digger plan\",\n            \"publishedDate\": \"2023-05-31T14:43:11.517Z\",\n            \"lastUpdatedDate\": \"2023-05-31T14:43:11.517Z\",\n            \"lastContentUpdatedDate\": \"2023-05-31T14:43:11.517Z\",\n            \"commentType\": \"text\",\n            \"usersLiked\": [],\n            \"_links\": {\n                \"self\": {\n                    \"href\": \"https://dev.azure.com/moehabib9/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331/pullRequests/1/threads/1/comments/1\"\n                },\n                \"repository\": {\n                    \"href\": \"https://dev.azure.com/moehabib9/a2def61f-157f-4382-b12f-184921223688/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331\"\n                },\n                \"threads\": {\n                    \"href\": \"https://dev.azure.com/moehabib9/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331/pullRequests/1/threads/1\"\n                },\n                \"pullRequests\": {\n                    \"href\": \"https://dev.azure.com/moehabib9/_apis/git/pullRequests/1\"\n                }\n            }\n        },\n        \"pullRequest\": {\n            \"repository\": {\n                \"id\": \"7eca30de-5afd-48e0-8c1f-e557c437d331\",\n                \"name\": \"digger-test\",\n                \"url\": \"https://dev.azure.com/moehabib9/a2def61f-157f-4382-b12f-184921223688/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331\",\n                \"project\": {\n                    \"id\": \"a2def61f-157f-4382-b12f-184921223688\",\n                    \"name\": \"digger-test\",\n                    \"url\": \"https://dev.azure.com/moehabib9/_apis/projects/a2def61f-157f-4382-b12f-184921223688\",\n                    \"state\": \"wellFormed\",\n                    \"revision\": 37,\n                    \"visibility\": \"private\",\n                    \"lastUpdateTime\": \"2023-05-31T14:04:47.047Z\"\n                },\n                \"size\": 44640,\n                \"remoteUrl\": \"https://moehabib9@dev.azure.com/moehabib9/digger-test/_git/digger-test\",\n                \"sshUrl\": \"git@ssh.dev.azure.com:v3/moehabib9/digger-test/digger-test\",\n                \"webUrl\": \"https://dev.azure.com/moehabib9/digger-test/_git/digger-test\",\n                \"isDisabled\": false,\n                \"isInMaintenance\": false\n            },\n            \"pullRequestId\": 1,\n            \"codeReviewId\": 1,\n            \"status\": \"active\",\n            \"createdBy\": {\n                \"displayName\": \"Mohamed Habib\",\n                \"url\": \"https://spsprodweu5.vssps.visualstudio.com/A8e479c0b-ee1b-43c7-9ed8-98f2a7ec6d01/_apis/Identities/d08e23e0-8b42-6ab5-a2b4-5f1dd4d1ed68\",\n                \"_links\": {\n                    \"avatar\": {\n                        \"href\": \"https://dev.azure.com/moehabib9/_apis/GraphProfile/MemberAvatars/msa.ZDA4ZTIzZTAtOGI0Mi03YWI1LWEyYjQtNWYxZGQ0ZDFlZDY4\"\n                    }\n                },\n                \"id\": \"d08e23e0-8b42-6ab5-a2b4-5f1dd4d1ed68\",\n                \"uniqueName\": \"moe.habib9@gmail.com\",\n                \"imageUrl\": \"https://dev.azure.com/moehabib9/_api/_common/identityImage?id=d08e23e0-8b42-6ab5-a2b4-5f1dd4d1ed68\",\n                \"descriptor\": \"msa.ZDA4ZTIzZTAtOGI0Mi03YWI1LWEyYjQtNWYxZGQ0ZDFlZDY4\"\n            },\n            \"creationDate\": \"2023-05-31T14:41:30.7525172Z\",\n            \"title\": \"Updated main.tf\",\n            \"description\": \"Updated main.tf\",\n            \"sourceRefName\": \"refs/heads/test-1\",\n            \"targetRefName\": \"refs/heads/main\",\n            \"mergeStatus\": \"succeeded\",\n            \"isDraft\": false,\n            \"mergeId\": \"58795d35-ab4e-445b-9c73-859a3eea1bbe\",\n            \"lastMergeSourceCommit\": {\n                \"commitId\": \"881bdbdda340529887c8ad46c06b0342715c4653\",\n                \"url\": \"https://dev.azure.com/moehabib9/a2def61f-157f-4382-b12f-184921223688/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331/commits/881bdbdda340529887c8ad46c06b0342715c4653\"\n            },\n            \"lastMergeTargetCommit\": {\n                \"commitId\": \"c254effb63be65b0f924b473385c45f4dc4c4e96\",\n                \"url\": \"https://dev.azure.com/moehabib9/a2def61f-157f-4382-b12f-184921223688/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331/commits/c254effb63be65b0f924b473385c45f4dc4c4e96\"\n            },\n            \"lastMergeCommit\": {\n                \"commitId\": \"dccb8e2c3232118d6023d862472afb4bf146dc39\",\n                \"author\": {\n                    \"name\": \"Mohamed Habib\",\n                    \"email\": \"moe.habib9@gmail.com\",\n                    \"date\": \"2023-05-31T14:41:30Z\"\n                },\n                \"committer\": {\n                    \"name\": \"Mohamed Habib\",\n                    \"email\": \"moe.habib9@gmail.com\",\n                    \"date\": \"2023-05-31T14:41:30Z\"\n                },\n                \"comment\": \"Merge pull request 1 from test-1 into main\",\n                \"url\": \"https://dev.azure.com/moehabib9/a2def61f-157f-4382-b12f-184921223688/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331/commits/dccb8e2c3232118d6023d862472afb4bf146dc39\"\n            },\n            \"reviewers\": [],\n            \"url\": \"https://dev.azure.com/moehabib9/a2def61f-157f-4382-b12f-184921223688/_apis/git/repositories/7eca30de-5afd-48e0-8c1f-e557c437d331/pullRequests/1\",\n            \"supportsIterations\": true,\n            \"artifactId\": \"vstfs:///Git/PullRequestId/a2def61f-157f-4382-b12f-184921223688%2f7eca30de-5afd-48e0-8c1f-e557c437d331%2f1\"\n        }\n    },\n    \"resourceVersion\": \"2.0\",\n    \"resourceContainers\": {\n        \"collection\": {\n            \"id\": \"f658bb7d-1dad-4fd9-b188-2fe9e914b810\",\n            \"baseUrl\": \"https://dev.azure.com/moehabib9/\"\n        },\n        \"account\": {\n            \"id\": \"8e479c0b-ee1b-43c7-9ed8-98f2a7ec6d01\",\n            \"baseUrl\": \"https://dev.azure.com/moehabib9/\"\n        },\n        \"project\": {\n            \"id\": \"a2def61f-157f-4382-b12f-184921223688\",\n            \"baseUrl\": \"https://dev.azure.com/moehabib9/\"\n        }\n    },\n    \"createdDate\": \"2023-05-31T14:43:18.079Z\"\n}"
+
+	az, _ := GetAzureReposContext(context)
+	assert.Equal(t, "digger plan", az.Event.(AzureCommentEvent).Resource.Comment.Content)
+}
diff --git a/pkg/configuration/digger_config_test.go b/pkg/configuration/digger_config_test.go
index 3044fd96..9dd42af6 100644
--- a/pkg/configuration/digger_config_test.go
+++ b/pkg/configuration/digger_config_test.go
@@ -26,7 +26,7 @@ func setUp() (string, func()) {
 }
 
 func TestDiggerConfigFileDoesNotExist(t *testing.T) {
-	dg, err := NewDiggerConfig("", &FileSystemDirWalker{})
+	dg, err := LoadDiggerConfig("", &FileSystemDirWalker{})
 	assert.NoError(t, err, "expected error to be not nil")
 	assert.Equal(t, dg.Projects[0].Name, "default", "expected default project to have name 'default'")
 	assert.Equal(t, dg.Projects[0].Dir, ".", "expected default project dir to be '.'")
@@ -46,7 +46,7 @@ func TestDiggerConfigWhenMultipleConfigExist(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	dg, err := NewDiggerConfig(tempDir, &FileSystemDirWalker{})
+	dg, err := LoadDiggerConfig(tempDir, &FileSystemDirWalker{})
 	assert.Error(t, err, "expected error to be returned")
 	assert.ErrorContains(t, err, ErrDiggerConfigConflict.Error(), "expected error to match target error")
 	assert.Nil(t, dg, "expected diggerConfig to be nil")
@@ -66,12 +66,33 @@ projects:
 	deleteFile := createFile(path.Join(tempDir, "digger.yaml"), diggerCfg)
 	defer deleteFile()
 
-	dg, err := NewDiggerConfig(tempDir, &FileSystemDirWalker{})
+	dg, err := LoadDiggerConfig(tempDir, &FileSystemDirWalker{})
 	assert.NoError(t, err, "expected error to be nil")
 	assert.NotNil(t, dg, "expected digger config to be not nil")
 	assert.Equal(t, "path/to/module/test", dg.GetDirectory("prod"))
 }
 
+func TestDiggerConfigDefaultWorkflow(t *testing.T) {
+	tempDir, teardown := setUp()
+	defer teardown()
+
+	diggerCfg := `
+projects:
+- name: prod
+  branch: /main/
+  dir: path/to/module/test
+`
+	deleteFile := createFile(path.Join(tempDir, "digger.yaml"), diggerCfg)
+	defer deleteFile()
+
+	dg, err := LoadDiggerConfig(tempDir, &FileSystemDirWalker{})
+	assert.NoError(t, err, "expected error to be nil")
+	assert.NotNil(t, dg, "expected digger config to be not nil")
+	assert.Equal(t, "default", dg.Projects[0].Workflow)
+	_, ok := dg.Workflows["default"]
+	assert.True(t, ok)
+}
+
 func TestDiggerConfigWhenOnlyYmlExists(t *testing.T) {
 	tempDir, teardown := setUp()
 	defer teardown()
@@ -86,7 +107,7 @@ projects:
 	deleteFile := createFile(path.Join(tempDir, "digger.yml"), diggerCfg)
 	defer deleteFile()
 
-	dg, err := NewDiggerConfig(tempDir, &FileSystemDirWalker{})
+	dg, err := LoadDiggerConfig(tempDir, &FileSystemDirWalker{})
 	assert.NoError(t, err, "expected error to be nil")
 	assert.NotNil(t, dg, "expected digger config to be not nil")
 	assert.Equal(t, "path/to/module", dg.GetDirectory("dev"))
@@ -111,7 +132,7 @@ workflows:
 	deleteFile := createFile(path.Join(tempDir, "digger.yaml"), diggerCfg)
 	defer deleteFile()
 
-	dg, err := NewDiggerConfig(tempDir, &FileSystemDirWalker{})
+	dg, err := LoadDiggerConfig(tempDir, &FileSystemDirWalker{})
 	assert.NoError(t, err, "expected error to be nil")
 	assert.Equal(t, Step{Action: "run", Value: "echo \"hello\"", Shell: ""}, dg.Workflows["myworkflow"].Plan.Steps[0], "parsed struct does not match expected struct")
 }
@@ -156,7 +177,7 @@ workflows:
 	deleteFile := createFile(path.Join(tempDir, "digger.yaml"), diggerCfg)
 	defer deleteFile()
 
-	dg, err := NewDiggerConfig(tempDir, &FileSystemDirWalker{})
+	dg, err := LoadDiggerConfig(tempDir, &FileSystemDirWalker{})
 	assert.NoError(t, err, "expected error to be nil")
 	assert.Equal(t, []EnvVarConfig{
 		{Name: "TF_VAR_state", Value: "s3://mybucket/terraform.tfstate"},
@@ -186,7 +207,7 @@ workflows:
 	deleteFile := createFile(path.Join(tempDir, "digger.yaml"), diggerCfg)
 	defer deleteFile()
 
-	dg, err := NewDiggerConfig(tempDir, &FileSystemDirWalker{})
+	dg, err := LoadDiggerConfig(tempDir, &FileSystemDirWalker{})
 	assert.NoError(t, err, "expected error to be nil")
 	assert.Equal(t, Step{Action: "init", ExtraArgs: nil, Shell: ""}, dg.Workflows["default"].Plan.Steps[0], "parsed struct does not match expected struct")
 	assert.Equal(t, Step{Action: "plan", ExtraArgs: []string{"-var-file=terraform.tfvars"}, Shell: ""}, dg.Workflows["default"].Plan.Steps[1], "parsed struct does not match expected struct")
@@ -212,7 +233,7 @@ generate_projects:
 	walker.Files = append(walker.Files, "dev/project")
 	walker.Files = append(walker.Files, "testtt")
 
-	dg, err := NewDiggerConfig(tempDir, walker)
+	dg, err := LoadDiggerConfig(tempDir, walker)
 	assert.NoError(t, err, "expected error to be nil")
 	assert.NotNil(t, dg, "expected digger config to be not nil")
 	assert.Equal(t, "test1", dg.Projects[0].Name)
@@ -242,7 +263,7 @@ generate_projects:
 	walker.Files = append(walker.Files, "dev/project")
 	walker.Files = append(walker.Files, "testtt")
 
-	dg, err := NewDiggerConfig(tempDir, walker)
+	dg, err := LoadDiggerConfig(tempDir, walker)
 	assert.NoError(t, err, "expected error to be nil")
 	assert.NotNil(t, dg, "expected digger config to be not nil")
 	assert.Equal(t, "test1", dg.Projects[0].Name)
@@ -271,7 +292,7 @@ generate_projects:
 	walker.Files = append(walker.Files, "dev/project")
 	walker.Files = append(walker.Files, "testtt")
 
-	dg, err := NewDiggerConfig(tempDir, walker)
+	dg, err := LoadDiggerConfig(tempDir, walker)
 	assert.NoError(t, err, "expected error to be nil")
 	assert.NotNil(t, dg, "expected digger config to be not nil")
 	assert.Equal(t, "dev", dg.Projects[0].Name)
@@ -288,7 +309,7 @@ func TestMissingProjectsReturnsError(t *testing.T) {
 	defer deleteFile()
 	walker := &MockDirWalker{}
 
-	_, err := NewDiggerConfig(tempDir, walker)
+	_, err := LoadDiggerConfig(tempDir, walker)
 	assert.ErrorContains(t, err, "no projects configuration found")
 }
 
diff --git a/pkg/digger/digger_test.go b/pkg/digger/digger_test.go
index a0ad1155..23ff3afa 100644
--- a/pkg/digger/digger_test.go
+++ b/pkg/digger/digger_test.go
@@ -2,6 +2,7 @@ package digger
 
 import (
 	"digger/pkg/configuration"
+	"digger/pkg/utils"
 	"sort"
 	"strconv"
 	"strings"
@@ -57,8 +58,9 @@ func (m *MockPRManager) GetChangedFiles(prNumber int) ([]string, error) {
 	return []string{}, nil
 }
 
-func (m *MockPRManager) PublishComment(prNumber int, comment string) {
+func (m *MockPRManager) PublishComment(prNumber int, comment string) error {
 	m.Commands = append(m.Commands, RunInfo{"PublishComment", strconv.Itoa(prNumber) + " " + comment, time.Now()})
+	return nil
 }
 
 func (m *MockPRManager) SetStatus(prNumber int, status string, statusContext string) error {
@@ -275,17 +277,17 @@ func TestParseWorkspace(t *testing.T) {
 	}
 
 	for _, tt := range commentTests {
-		out, err := parseWorkspace(tt.in)
+		out, err := utils.ParseWorkspace(tt.in)
 		if tt.err {
 			if err == nil {
-				t.Errorf("parseWorkspace(%q) = %q, want error", tt.in, out)
+				t.Errorf("ParseWorkspace(%q) = %q, want error", tt.in, out)
 			}
 		} else {
 			if err != nil {
-				t.Errorf("parseWorkspace(%q) = %q, want %q", tt.in, err, tt.out)
+				t.Errorf("ParseWorkspace(%q) = %q, want %q", tt.in, err, tt.out)
 			}
 			if out != tt.out {
-				t.Errorf("parseWorkspace(%q) = %q, want %q", tt.in, out, tt.out)
+				t.Errorf("ParseWorkspace(%q) = %q, want %q", tt.in, out, tt.out)
 			}
 		}
 	}
diff --git a/pkg/integration/integration_test.go b/pkg/integration/integration_test.go
index cef34326..d1f11977 100644
--- a/pkg/integration/integration_test.go
+++ b/pkg/integration/integration_test.go
@@ -6,6 +6,8 @@ import (
 	"digger/pkg/configuration"
 	"digger/pkg/digger"
 	dg_github "digger/pkg/github"
+	"digger/pkg/locking"
+	"digger/pkg/storage"
 	"digger/pkg/terraform"
 	"digger/pkg/utils"
 	"log"
@@ -27,7 +29,7 @@ func SkipCI(t *testing.T) {
 	}
 }
 
-func getProjectLockForTests() (error, *utils.ProjectLockImpl) {
+func getProjectLockForTests() (error, *locking.ProjectLockImpl) {
 	sess, err := session.NewSessionWithOptions(session.Options{
 		Profile: "digger-test",
 		Config: awssdk.Config{
@@ -40,9 +42,9 @@ func getProjectLockForTests() (error, *utils.ProjectLockImpl) {
 	repoOwner := "diggerhq"
 	repositoryName := "test_dynamodb_lock"
 	ghToken := "token"
-	githubPrService := dg_github.NewGithubPullRequestService(ghToken, repositoryName, repoOwner)
+	githubPrService := dg_github.NewGitHubService(ghToken, repositoryName, repoOwner)
 
-	projectLock := &utils.ProjectLockImpl{
+	projectLock := &locking.ProjectLockImpl{
 		InternalLock: &dynamoDbLock,
 		CIService:    githubPrService,
 		ProjectName:  "test_dynamodb_lock",
@@ -350,10 +352,10 @@ func TestHappyPath(t *testing.T) {
 	terraform.CreateValidTerraformTestFile(dir)
 	terraform.CreateSingleEnvDiggerYmlFile(dir)
 
-	diggerConfig, err := configuration.NewDiggerConfig(dir, &configuration.FileSystemDirWalker{})
+	diggerConfig, err := configuration.LoadDiggerConfig(dir, &configuration.FileSystemDirWalker{})
 	assert.NoError(t, err)
 
-	lock, err := utils.GetLock()
+	lock, err := locking.GetLock()
 	assert.NoError(t, err)
 	assert.NotNil(t, lock, "failed to create lock")
 
@@ -362,36 +364,36 @@ func TestHappyPath(t *testing.T) {
 
 	println("--- new pull request ---")
 	newPullRequestContext := githubContextNewPullRequestMinJson
-	parsedNewPullRequestContext, err := digger.GetGitHubContext(newPullRequestContext)
+	parsedNewPullRequestContext, err := dg_github.GetGitHubContext(newPullRequestContext)
 	assert.NoError(t, err)
 
 	diggerPlanCommentContext := githubContextDiggerPlanCommentMinJson
-	parsedDiggerPlanCommentContext, err := digger.GetGitHubContext(diggerPlanCommentContext)
+	parsedDiggerPlanCommentContext, err := dg_github.GetGitHubContext(diggerPlanCommentContext)
 	assert.NoError(t, err)
 
 	diggerApplyCommentContext := githubContextDiggerApplyCommentMinJson
-	parsedDiggerApplyCommentContext, err := digger.GetGitHubContext(diggerApplyCommentContext)
+	parsedDiggerApplyCommentContext, err := dg_github.GetGitHubContext(diggerApplyCommentContext)
 	assert.NoError(t, err)
 
 	diggerUnlockCommentContext := githubContextDiggerUnlockCommentMinJson
-	parsedDiggerUnlockCommentContext, err := digger.GetGitHubContext(diggerUnlockCommentContext)
+	parsedDiggerUnlockCommentContext, err := dg_github.GetGitHubContext(diggerUnlockCommentContext)
 	assert.NoError(t, err)
 
 	ghEvent := parsedNewPullRequestContext.Event
 	eventName := parsedNewPullRequestContext.EventName
 	repoOwner := parsedNewPullRequestContext.RepositoryOwner
 	repositoryName := parsedNewPullRequestContext.Repository
-	githubPrService := dg_github.NewGithubPullRequestService(ghToken, repositoryName, repoOwner)
+	githubPrService := dg_github.NewGitHubService(ghToken, repositoryName, repoOwner)
 
 	assert.Equal(t, "pull_request", parsedNewPullRequestContext.EventName)
 
 	//  new pr should lock the project
-	impactedProjects, requestedProject, prNumber, err := digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err := dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err := dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	zipManager := utils.Zipper{}
-	planStorage := &utils.GithubPlanStorage{
+	planStorage := &storage.GithubPlanStorage{
 		Client:            github.NewTokenClient(context.Background(), ghToken),
 		Owner:             repoOwner,
 		RepoName:          repositoryName,
@@ -401,7 +403,7 @@ func TestHappyPath(t *testing.T) {
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock := &utils.ProjectLockImpl{
+	projectLock := &locking.ProjectLockImpl{
 		InternalLock: lock,
 		CIService:    githubPrService,
 		ProjectName:  "dev",
@@ -421,9 +423,9 @@ func TestHappyPath(t *testing.T) {
 	repositoryName = parsedDiggerPlanCommentContext.Repository
 
 	// 'digger plan' comment should trigger terraform execution
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
@@ -435,14 +437,14 @@ func TestHappyPath(t *testing.T) {
 	repositoryName = parsedDiggerApplyCommentContext.Repository
 
 	// 'digger apply' comment should trigger terraform execution and unlock the project
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock = &utils.ProjectLockImpl{
+	projectLock = &locking.ProjectLockImpl{
 		InternalLock: lock,
 		CIService:    githubPrService,
 		ProjectName:  "dev",
@@ -459,14 +461,14 @@ func TestHappyPath(t *testing.T) {
 	repoOwner = parsedDiggerUnlockCommentContext.RepositoryOwner
 	repositoryName = parsedDiggerUnlockCommentContext.Repository
 
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock = &utils.ProjectLockImpl{
+	projectLock = &locking.ProjectLockImpl{
 		InternalLock: lock,
 		CIService:    githubPrService,
 		ProjectName:  "dev",
@@ -494,7 +496,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	terraform.CreateValidTerraformTestFile(dir)
 	terraform.CreateMultiEnvDiggerYmlFile(dir)
 
-	diggerConfig, err := configuration.NewDiggerConfig(dir, &configuration.FileSystemDirWalker{})
+	diggerConfig, err := configuration.LoadDiggerConfig(dir, &configuration.FileSystemDirWalker{})
 	assert.NoError(t, err)
 
 	sess, err := session.NewSessionWithOptions(session.Options{
@@ -513,37 +515,37 @@ func TestMultiEnvHappyPath(t *testing.T) {
 
 	println("--- new pull request ---")
 	newPullRequestContext := githubContextNewPullRequestMinJson
-	parsedNewPullRequestContext, err := digger.GetGitHubContext(newPullRequestContext)
+	parsedNewPullRequestContext, err := dg_github.GetGitHubContext(newPullRequestContext)
 	assert.NoError(t, err)
 
 	diggerPlanCommentContext := githubContextDiggerPlanCommentMinJson
-	parsedDiggerPlanCommentContext, err := digger.GetGitHubContext(diggerPlanCommentContext)
+	parsedDiggerPlanCommentContext, err := dg_github.GetGitHubContext(diggerPlanCommentContext)
 	assert.NoError(t, err)
 
 	diggerApplyCommentContext := githubContextDiggerApplyCommentMinJson
-	parsedDiggerApplyCommentContext, err := digger.GetGitHubContext(diggerApplyCommentContext)
+	parsedDiggerApplyCommentContext, err := dg_github.GetGitHubContext(diggerApplyCommentContext)
 	assert.NoError(t, err)
 
 	diggerUnlockCommentContext := githubContextDiggerUnlockCommentMinJson
-	parsedDiggerUnlockCommentContext, err := digger.GetGitHubContext(diggerUnlockCommentContext)
+	parsedDiggerUnlockCommentContext, err := dg_github.GetGitHubContext(diggerUnlockCommentContext)
 	assert.NoError(t, err)
 
 	ghEvent := parsedNewPullRequestContext.Event
 	eventName := parsedNewPullRequestContext.EventName
 	repoOwner := parsedNewPullRequestContext.RepositoryOwner
 	repositoryName := parsedNewPullRequestContext.Repository
-	githubPrService := dg_github.NewGithubPullRequestService(ghToken, repositoryName, repoOwner)
+	githubPrService := dg_github.NewGitHubService(ghToken, repositoryName, repoOwner)
 
 	assert.Equal(t, "pull_request", parsedNewPullRequestContext.EventName)
 
 	// no files changed, no locks
-	impactedProjects, requestedProject, prNumber, err := digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err := dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err := dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 
 	zipManager := utils.Zipper{}
-	planStorage := &utils.GithubPlanStorage{
+	planStorage := &storage.GithubPlanStorage{
 		Client:            github.NewTokenClient(context.Background(), ghToken),
 		Owner:             repoOwner,
 		RepoName:          repositoryName,
@@ -553,7 +555,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, &dynamoDbLock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock := &utils.ProjectLockImpl{
+	projectLock := &locking.ProjectLockImpl{
 		InternalLock: &dynamoDbLock,
 		CIService:    githubPrService,
 		ProjectName:  "digger_demo",
@@ -572,9 +574,9 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	repositoryName = parsedDiggerPlanCommentContext.Repository
 
 	// 'digger plan' comment should trigger terraform execution
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, &dynamoDbLock, planStorage, dir)
 	assert.NoError(t, err)
@@ -586,14 +588,14 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	repositoryName = parsedDiggerApplyCommentContext.Repository
 
 	// 'digger apply' comment should trigger terraform execution and unlock the project
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, &dynamoDbLock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock = &utils.ProjectLockImpl{
+	projectLock = &locking.ProjectLockImpl{
 		InternalLock: &dynamoDbLock,
 		CIService:    githubPrService,
 		ProjectName:  "digger_demo",
@@ -610,14 +612,14 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	repoOwner = parsedDiggerUnlockCommentContext.RepositoryOwner
 	repositoryName = parsedDiggerUnlockCommentContext.Repository
 
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, &dynamoDbLock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock = &utils.ProjectLockImpl{
+	projectLock = &locking.ProjectLockImpl{
 		InternalLock: &dynamoDbLock,
 		CIService:    githubPrService,
 		ProjectName:  "digger_demo",
@@ -678,7 +680,7 @@ func TestUnLock(t *testing.T) {
 func TestNonExistentGitHubEvent(t *testing.T) {
 
 	unknownEventContext := githubContextUnknownEventJson
-	_, err := digger.GetGitHubContext(unknownEventContext)
+	_, err := dg_github.GetGitHubContext(unknownEventContext)
 	println(err.Error())
 	assert.Error(t, err)
 	assert.Equal(t, "error parsing GitHub context JSON: unknown GitHub event: non_existent_event", err.Error())
@@ -711,7 +713,7 @@ workflows:
 	terraform.CreateValidTerraformTestFile(dir)
 	terraform.CreateCustomDiggerYmlFile(dir, diggerCfg)
 
-	diggerConfig, err := configuration.NewDiggerConfig(dir, &configuration.FileSystemDirWalker{})
+	diggerConfig, err := configuration.LoadDiggerConfig(dir, &configuration.FileSystemDirWalker{})
 	assert.NoError(t, err)
 
 	assert.NotNil(t, diggerConfig.Workflows)
@@ -719,7 +721,7 @@ workflows:
 	assert.NotNil(t, diggerConfig.Workflows["myworkflow"].Configuration)
 	assert.NotNil(t, diggerConfig.Workflows["myworkflow"].Configuration.OnCommitToDefault)
 
-	lock, err := utils.GetLock()
+	lock, err := locking.GetLock()
 	assert.NoError(t, err)
 	assert.NotNil(t, lock, "failed to create lock")
 
@@ -728,37 +730,37 @@ workflows:
 
 	println("--- new pull request ---")
 	newPullRequestContext := githubContextNewPullRequestMinJson
-	parsedNewPullRequestContext, err := digger.GetGitHubContext(newPullRequestContext)
+	parsedNewPullRequestContext, err := dg_github.GetGitHubContext(newPullRequestContext)
 	assert.NoError(t, err)
 
 	diggerPlanCommentContext := githubContextDiggerPlanCommentMinJson
-	parsedDiggerPlanCommentContext, err := digger.GetGitHubContext(diggerPlanCommentContext)
+	parsedDiggerPlanCommentContext, err := dg_github.GetGitHubContext(diggerPlanCommentContext)
 	assert.NoError(t, err)
 
 	diggerApplyCommentContext := githubContextDiggerApplyCommentMinJson
-	parsedDiggerApplyCommentContext, err := digger.GetGitHubContext(diggerApplyCommentContext)
+	parsedDiggerApplyCommentContext, err := dg_github.GetGitHubContext(diggerApplyCommentContext)
 	assert.NoError(t, err)
 
 	diggerUnlockCommentContext := githubContextDiggerUnlockCommentMinJson
-	parsedDiggerUnlockCommentContext, err := digger.GetGitHubContext(diggerUnlockCommentContext)
+	parsedDiggerUnlockCommentContext, err := dg_github.GetGitHubContext(diggerUnlockCommentContext)
 	assert.NoError(t, err)
 
 	ghEvent := parsedNewPullRequestContext.Event
 	eventName := parsedNewPullRequestContext.EventName
 	repoOwner := parsedNewPullRequestContext.RepositoryOwner
 	repositoryName := parsedNewPullRequestContext.Repository
-	githubPrService := dg_github.NewGithubPullRequestService(ghToken, repositoryName, repoOwner)
+	githubPrService := dg_github.NewGitHubService(ghToken, repositoryName, repoOwner)
 
 	assert.Equal(t, "pull_request", parsedNewPullRequestContext.EventName)
 
 	//  new pr should lock the project
-	impactedProjects, requestedProject, prNumber, err := digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err := dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err := dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 
 	zipManager := utils.Zipper{}
-	planStorage := &utils.GithubPlanStorage{
+	planStorage := &storage.GithubPlanStorage{
 		Client:            github.NewTokenClient(context.Background(), ghToken),
 		Owner:             repoOwner,
 		RepoName:          repositoryName,
@@ -768,7 +770,7 @@ workflows:
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock := &utils.ProjectLockImpl{
+	projectLock := &locking.ProjectLockImpl{
 		InternalLock: lock,
 		CIService:    githubPrService,
 		ProjectName:  "dev",
@@ -788,9 +790,9 @@ workflows:
 	repositoryName = parsedDiggerPlanCommentContext.Repository
 
 	// 'digger plan' comment should trigger terraform execution
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
@@ -802,14 +804,14 @@ workflows:
 	repositoryName = parsedDiggerApplyCommentContext.Repository
 
 	// 'digger apply' comment should trigger terraform execution and unlock the project
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock = &utils.ProjectLockImpl{
+	projectLock = &locking.ProjectLockImpl{
 		InternalLock: lock,
 		CIService:    githubPrService,
 		ProjectName:  "dev",
@@ -825,14 +827,14 @@ workflows:
 	repoOwner = parsedDiggerUnlockCommentContext.RepositoryOwner
 	repositoryName = parsedDiggerUnlockCommentContext.Repository
 
-	impactedProjects, requestedProject, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	impactedProjects, requestedProject, prNumber, err = dg_github.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
 	assert.NoError(t, err)
-	commandsToRunPerProject, _, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
+	commandsToRunPerProject, _, err = dg_github.ConvertGithubEventToCommands(ghEvent, impactedProjects, requestedProject, diggerConfig.Workflows)
 	assert.NoError(t, err)
 	_, _, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, planStorage, dir)
 	assert.NoError(t, err)
 
-	projectLock = &utils.ProjectLockImpl{
+	projectLock = &locking.ProjectLockImpl{
 		InternalLock: lock,
 		CIService:    githubPrService,
 		ProjectName:  "dev",
diff --git a/pkg/utils/locking_test.go b/pkg/locking/locking_test.go
similarity index 76%
rename from pkg/utils/locking_test.go
rename to pkg/locking/locking_test.go
index 8270ef82..7655cac6 100644
--- a/pkg/utils/locking_test.go
+++ b/pkg/locking/locking_test.go
@@ -1,14 +1,14 @@
-package utils
+package locking
 
 import (
-	"digger/pkg/github"
+	"digger/pkg/utils"
 	"github.com/stretchr/testify/assert"
 	"testing"
 )
 
 func TestLockingTwiceThrowsError(t *testing.T) {
-	mockDynamoDB := MockLock{make(map[string]int)}
-	mockPrManager := github.MockGithubPullrequestManager{}
+	mockDynamoDB := utils.MockLock{make(map[string]int)}
+	mockPrManager := utils.MockGithubPullrequestManager{}
 	pl := ProjectLockImpl{
 		InternalLock: &mockDynamoDB,
 		CIService:    &mockPrManager,
diff --git a/pkg/utils/io_test.go b/pkg/utils/io_test.go
index 17767e9f..294fdfa5 100644
--- a/pkg/utils/io_test.go
+++ b/pkg/utils/io_test.go
@@ -108,6 +108,7 @@ func TestMatchIncludeExcludePatternsToFile(t *testing.T) {
 		{"modules/moduleA/main.tf", []string{"dev**", "./modules/**"}, []string{"dev/"}, true},
 		{"modules/moduleA/main.tf", []string{"dev**"}, []string{"dev/"}, false},
 		{"modules/moduleA/main.tf", []string{"dev**", "modules/**"}, []string{"modules/moduleA/**"}, false},
+		{"modules/moduleA/main.tf", []string{""}, []string{""}, false},
 	}
 	for _, tt := range normTests {
 		res := MatchIncludeExcludePatternsToFile(tt.fileToMatch, tt.includePatterns, tt.excludePatterns)