Repository: diggerhq/digger
Pull request #220 (2023-05-09 15:41:07)

Add gitlab CI


Patch:
diff --git a/cmd/digger/main.go b/cmd/digger/main.go
index a8ae6791..2f3ebc17 100644
--- a/cmd/digger/main.go
+++ b/cmd/digger/main.go
@@ -6,17 +6,18 @@ import (
 	"digger/pkg/digger"
 	"digger/pkg/gcp"
 	dg_github "digger/pkg/github"
+	"digger/pkg/gitlab"
 	"digger/pkg/models"
 	"digger/pkg/utils"
 	"fmt"
+	"github.com/google/go-github/v51/github"
 	"log"
 	"os"
 	"strings"
-
-	"github.com/google/go-github/v51/github"
 )
 
-func main() {
+func gitHubCI(lock utils.Lock) {
+	println("Using GitHub.")
 	githubRepositoryOwner := os.Getenv("GITHUB_REPOSITORY_OWNER")
 	if githubRepositoryOwner != "" {
 		utils.SendUsageRecord(githubRepositoryOwner, "log", "initialize")
@@ -24,16 +25,6 @@ func main() {
 		utils.SendUsageRecord("", "log", "non github initialisation")
 	}
 
-	args := os.Args[1:]
-	if len(args) > 0 && args[0] == "version" {
-		fmt.Println(utils.GetVersion())
-		os.Exit(0)
-	}
-	if len(args) > 0 && args[0] == "help" {
-		utils.DisplayCommands()
-		os.Exit(0)
-	}
-
 	ghToken := os.Getenv("GITHUB_TOKEN")
 	if ghToken == "" {
 		reportErrorAndExit(githubRepositoryOwner, "GITHUB_TOKEN is not defined", 1)
@@ -58,7 +49,7 @@ func main() {
 	}
 	println("Digger config read successfully")
 
-	lock, err := utils.GetLock()
+	lock, err = utils.GetLock()
 	if err != nil {
 		reportErrorAndExit(githubRepositoryOwner, fmt.Sprintf("Failed to create lock provider. %s", err), 5)
 	}
@@ -110,7 +101,117 @@ func main() {
 			reportErrorAndExit(githubRepositoryOwner, fmt.Sprintf("Panic occurred. %s", r), 1)
 		}
 	}()
+}
+
+func gitLabCI(lock utils.Lock) {
+	println("Using GitLab.")
+	projectNamespace := os.Getenv("CI_PROJECT_NAMESPACE")
+	gitlabToken := os.Getenv("GITLAB_TOKEN")
+	if gitlabToken == "" {
+		fmt.Println("GITLAB_TOKEN is empty")
+	}
+
+	walker := configuration.FileSystemDirWalker{}
+	currentDir, err := os.Getwd()
+	if err != nil {
+		reportErrorAndExit(projectNamespace, fmt.Sprintf("Failed to get current dir. %s", err), 4)
+	}
+	fmt.Printf("main: working dir: %s \n", currentDir)
+
+	diggerConfig, err := configuration.NewDiggerConfig(currentDir, &walker)
+	if err != nil {
+		reportErrorAndExit(projectNamespace, fmt.Sprintf("Failed to read Digger config. %s", err), 4)
+	}
+	println("Digger config read successfully")
 
+	gitLabContext, err := gitlab.ParseGitLabContext()
+	if err != nil {
+		fmt.Printf("failed to parse GitLab context. %s\n", err.Error())
+		os.Exit(4)
+	}
+
+	gitlabService, err := gitlab.NewGitLabService(gitlabToken, gitLabContext)
+	if err != nil {
+		fmt.Printf("failed to initialise GitLab service, %v", err)
+		os.Exit(4)
+	}
+
+	gitlabEvent := gitlab.GitLabEvent{EventType: gitLabContext.EventType}
+
+	impactedProjects, err := gitlab.ProcessGitLabEvent(gitLabContext, diggerConfig, gitlabService)
+	if err != nil {
+		fmt.Printf("failed to process GitLab event, %v", err)
+		os.Exit(6)
+	}
+	println("GitLab event processed successfully")
+
+	commandsToRunPerProject, err := gitlab.ConvertGitLabEventToCommands(gitlabEvent, gitLabContext, impactedProjects, diggerConfig.Workflows)
+	if err != nil {
+		fmt.Printf("failed to convert event to command, %v", err)
+		os.Exit(7)
+	}
+	println("GitLab event converted to commands successfully")
+
+	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
+
+	result, 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)
+
+	println("Commands executed successfully")
+}
+
+/*
+Exit codes:
+0 - No errors
+1 - Failed to read digger config
+2 - Failed to create lock provider
+3 - Failed to find auth token
+4 - Failed to initialise CI context
+5 -
+6 - failed to process CI event
+7 - failed to convert event to command
+8 - failed to execute command
+10 - No CI detected
+*/
+
+func main() {
+	args := os.Args[1:]
+	if len(args) > 0 && args[0] == "version" {
+		fmt.Println(utils.GetVersion())
+		os.Exit(0)
+	}
+	if len(args) > 0 && args[0] == "help" {
+		utils.DisplayCommands()
+		os.Exit(0)
+	}
+
+	lock, err := utils.GetLock()
+	if err != nil {
+		fmt.Printf("Failed to create lock provider. %s\n", err)
+		os.Exit(2)
+	}
+	println("Lock provider has been created successfully")
+
+	ci := digger.DetectCI()
+	switch ci {
+	case digger.GitHub:
+		gitHubCI(lock)
+	case digger.GitLab:
+		gitLabCI(lock)
+	case digger.BitBucket:
+	case digger.None:
+		print("No CI detected.")
+		os.Exit(10)
+	}
 }
 
 func newPlanStorage(ghToken string, repoOwner string, repositoryName string, prNumber int) utils.PlanStorage {
diff --git a/go.mod b/go.mod
index a068c847..c0271266 100644
--- a/go.mod
+++ b/go.mod
@@ -8,9 +8,11 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.0.1
 	github.com/aws/aws-sdk-go v1.44.262
 	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/stretchr/testify v1.8.2
+	github.com/xanzy/go-gitlab v0.83.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -26,11 +28,13 @@ require (
 	github.com/cloudflare/circl v1.1.0 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
-	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
 	github.com/googleapis/gax-go/v2 v2.7.1 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
 	github.com/kr/pretty v0.1.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
@@ -41,6 +45,7 @@ require (
 	golang.org/x/oauth2 v0.7.0 // indirect
 	golang.org/x/sys v0.7.0 // indirect
 	golang.org/x/text v0.9.0 // indirect
+	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	google.golang.org/api v0.114.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index 204598cc..6eb51f2e 100644
--- a/go.sum
+++ b/go.sum
@@ -32,6 +32,8 @@ github.com/aws/aws-sdk-go v1.44.262/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8
 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=
+github.com/caarlos0/env/v7 v7.1.0 h1:9lzTF5amyQeWHZzuZeKlCb5FWSUxpG1js43mhbY8ozg=
+github.com/caarlos0/env/v7 v7.1.0/go.mod h1:LPPWniDUq4JaO6Q41vtlyikhMknqymCLBw0eX4dcH1E=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
@@ -62,8 +64,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
 github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -86,6 +88,12 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9
 github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
 github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
 github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
+github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -97,6 +105,9 @@ 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/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI=
+github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
 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=
@@ -112,6 +123,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw=
+github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
@@ -168,6 +181,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
diff --git a/pkg/configuration/digger_config.go b/pkg/configuration/digger_config.go
index 1bd608cf..5da790ea 100644
--- a/pkg/configuration/digger_config.go
+++ b/pkg/configuration/digger_config.go
@@ -106,7 +106,6 @@ func (p *Project) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	}
 	*p = Project(raw)
 	return nil
-
 }
 
 func (w *Workflow) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -237,6 +236,7 @@ func NewDiggerConfig(workingDir string, walker DirWalker) (*DiggerConfig, error)
 	}
 
 	if fileName == "" {
+		fmt.Println("No digger config found, using default one")
 		config.Projects = make([]Project, 1)
 		config.Projects[0] = defaultProject()
 		config.Workflows = make(map[string]Workflow)
@@ -328,6 +328,8 @@ func (c *DiggerConfig) GetModifiedProjects(changedFiles []string) []Project {
 		for _, file := range changedFiles {
 			absoluteFile, _ := filepath.Abs(path.Join("/", file))
 			absoluteDir, _ := filepath.Abs(path.Join("/", project.Dir))
+
+			//fmt.Printf("absoluteFile: %s, absoluteDir: %s \n", absoluteFile, absoluteDir)
 			if strings.HasPrefix(absoluteFile, absoluteDir) {
 				result = append(result, project)
 				break
diff --git a/pkg/digger/digger.go b/pkg/digger/digger.go
index a240581d..98032ad0 100644
--- a/pkg/digger/digger.go
+++ b/pkg/digger/digger.go
@@ -22,7 +22,7 @@ import (
 func ProcessGitHubEvent(ghEvent models.Event, diggerConfig *configuration.DiggerConfig, prManager ci.CIService) ([]configuration.Project, int, bool, error) {
 	var impactedProjects []configuration.Project
 	var prNumber int
-	var mergePrIfCmdSuccessfull = false
+	var mergePrIfCmdSuccessful = false
 
 	switch ghEvent.(type) {
 	case models.PullRequestEvent:
@@ -42,7 +42,7 @@ func ProcessGitHubEvent(ghEvent models.Event, diggerConfig *configuration.Digger
 			if len(newImpactedProjects) == 0 {
 				prManager.PublishComment(prNumber, "Error: Invalid project name '"+requestedProject+"'. The requested operation cannot be performed.")
 			} else if len(impactedProjects) == 1 && CheckIfApplyComment(ghEvent) {
-				mergePrIfCmdSuccessfull = true
+				mergePrIfCmdSuccessful = true
 			}
 			impactedProjects = newImpactedProjects
 		} else {
@@ -55,7 +55,7 @@ func ProcessGitHubEvent(ghEvent models.Event, diggerConfig *configuration.Digger
 	default:
 		return nil, 0, false, fmt.Errorf("unsupported event type")
 	}
-	return impactedProjects, prNumber, mergePrIfCmdSuccessfull, nil
+	return impactedProjects, prNumber, mergePrIfCmdSuccessful, nil
 }
 
 func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string, repoName string, eventName string, prNumber int, prManager ci.CIService, lock utils.Lock, planStorage utils.PlanStorage, workingDir string) (bool, error) {
@@ -351,7 +351,7 @@ type DiggerExecutor struct {
 	PlanStage         *configuration.Stage
 	CommandRunner     CommandRun
 	TerraformExecutor terraform.TerraformExecutor
-	ciService         ci.CIService
+	CIService         ci.CIService
 	ProjectLock       utils.ProjectLock
 	PlanStorage       utils.PlanStorage
 }
@@ -438,7 +438,7 @@ func (d DiggerExecutor) Plan(prNumber int) error {
 				}
 				plan := cleanupTerraformPlan(isNonEmptyPlan, err, stdout, stderr)
 				comment := utils.GetTerraformOutputAsCollapsibleComment("Plan for **"+d.ProjectLock.LockId()+"**", plan)
-				d.ciService.PublishComment(prNumber, comment)
+				d.CIService.PublishComment(prNumber, comment)
 			}
 			if step.Action == "run" {
 				stdout, stderr, err := d.CommandRunner.Run(step.Value)
@@ -462,14 +462,14 @@ func (d DiggerExecutor) Apply(prNumber int) error {
 		}
 	}
 
-	isMergeable, _, err := d.ciService.IsMergeable(prNumber)
+	isMergeable, _, err := d.CIService.IsMergeable(prNumber)
 	if err != nil {
 		return fmt.Errorf("error validating is PR is mergeable: %v", err)
 	}
 
 	if !isMergeable {
 		comment := "Cannot perform Apply since the PR is not currently mergeable."
-		d.ciService.PublishComment(prNumber, comment)
+		d.CIService.PublishComment(prNumber, comment)
 	} else {
 
 		if res, _ := d.ProjectLock.Lock(prNumber); res {
@@ -499,9 +499,9 @@ func (d DiggerExecutor) Apply(prNumber int) 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)
+					d.CIService.PublishComment(prNumber, comment)
 					if err != nil {
-						d.ciService.PublishComment(prNumber, "Error during applying.")
+						d.CIService.PublishComment(prNumber, "Error during applying.")
 						return fmt.Errorf("error executing apply: %v", err)
 					}
 				}
@@ -634,3 +634,35 @@ func defaultWorkflow() *configuration.Workflow {
 		},
 	}
 }
+
+type CIName string
+
+const (
+	None      = CIName("")
+	GitHub    = CIName("github")
+	GitLab    = CIName("gitlab")
+	BitBucket = CIName("bitbucket")
+)
+
+func (ci CIName) String() string {
+	return string(ci)
+}
+
+func DetectCI() CIName {
+
+	notEmpty := func(key string) bool {
+		return os.Getenv(key) != ""
+	}
+
+	if notEmpty("GITHUB_ACTIONS") {
+		return GitHub
+	}
+	if notEmpty("GITLAB_CI") {
+		return GitLab
+	}
+	if notEmpty("BITBUCKET_BUILD_NUMBER") {
+		return BitBucket
+	}
+	return None
+
+}
diff --git a/pkg/gitlab/gitlab.go b/pkg/gitlab/gitlab.go
new file mode 100644
index 00000000..06dab0f8
--- /dev/null
+++ b/pkg/gitlab/gitlab.go
@@ -0,0 +1,464 @@
+package gitlab
+
+import (
+	"digger/pkg/ci"
+	"digger/pkg/configuration"
+	"digger/pkg/digger"
+	"digger/pkg/terraform"
+	"digger/pkg/utils"
+	"fmt"
+	"github.com/caarlos0/env/v7"
+	go_gitlab "github.com/xanzy/go-gitlab"
+	"log"
+	"path"
+	"strings"
+)
+
+// based on https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
+
+type GitLabContext struct {
+	PipelineSource PipelineSourceType `env:"CI_PIPELINE_SOURCE"`
+
+	// this env var should be set by webhook that trigger pipeline
+	EventType          GitLabEventType `env:"MERGE_REQUEST_EVENT_NAME"`
+	PipelineId         *int            `env:"CI_PIPELINE_ID"`
+	PipelineIId        *int            `env:"CI_PIPELINE_IID"`
+	MergeRequestId     *int            `env:"CI_MERGE_REQUEST_ID"`
+	MergeRequestIId    *int            `env:"CI_MERGE_REQUEST_IID"`
+	ProjectName        string          `env:"CI_PROJECT_NAME"`
+	ProjectNamespace   string          `env:"CI_PROJECT_NAMESPACE"`
+	ProjectId          *int            `env:"CI_PROJECT_ID"`
+	ProjectNamespaceId *int            `env:"CI_PROJECT_NAMESPACE_ID"`
+	Token              string          `env:"GITLAB_TOKEN"`
+	DiggerCommand      string          `env:"DIGGER_COMMAND"`
+	DiscussionID       string          `env:"DISCUSSION_ID"`
+}
+
+type PipelineSourceType string
+
+func (t PipelineSourceType) String() string {
+	return string(t)
+}
+
+const (
+	Push                     = PipelineSourceType("push")
+	Web                      = PipelineSourceType("web")
+	Schedule                 = PipelineSourceType("schedule")
+	Api                      = PipelineSourceType("api")
+	External                 = PipelineSourceType("external")
+	Chat                     = PipelineSourceType("chat")
+	WebIDE                   = PipelineSourceType("webide")
+	ExternalPullRequestEvent = PipelineSourceType("external_pull_request_event")
+	ParentPipeline           = PipelineSourceType("parent_pipeline")
+	Trigger                  = PipelineSourceType("trigger")
+	Pipeline                 = PipelineSourceType("pipeline")
+)
+
+func ParseGitLabContext() (*GitLabContext, error) {
+	var parsedGitLabContext GitLabContext
+
+	if err := env.Parse(&parsedGitLabContext); err != nil {
+		fmt.Printf("%+v\n", err)
+	}
+
+	fmt.Printf("%+v\n", parsedGitLabContext)
+	return &parsedGitLabContext, nil
+}
+
+func NewGitLabService(token string, gitLabContext *GitLabContext) (*GitLabService, error) {
+	client, err := go_gitlab.NewClient(token)
+	if err != nil {
+		log.Fatalf("failed to create gitlab client: %v", err)
+	}
+
+	user, _, err := client.Users.CurrentUser()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get current GitLab user info, %v", err)
+	}
+	fmt.Printf("current GitLab user: %s\n", user.Name)
+
+	return &GitLabService{
+		Client:  client,
+		Context: gitLabContext,
+	}, nil
+}
+
+func ProcessGitLabEvent(gitlabContext *GitLabContext, diggerConfig *configuration.DiggerConfig, service *GitLabService) ([]configuration.Project, error) {
+	var impactedProjects []configuration.Project
+
+	if gitlabContext.MergeRequestIId == nil {
+		println("Merge Request ID is not found.")
+		return nil, nil
+	}
+
+	mergeRequestId := gitlabContext.MergeRequestIId
+	changedFiles, err := service.GetChangedFiles(*mergeRequestId)
+
+	if err != nil {
+		return nil, fmt.Errorf("could not get changed files")
+	}
+
+	impactedProjects = diggerConfig.GetModifiedProjects(changedFiles)
+
+	fmt.Println("Impacted projects:")
+	for _, v := range impactedProjects {
+		fmt.Printf("%s\n", v.Name)
+	}
+
+	return impactedProjects, nil
+}
+
+type GitLabService struct {
+	Client  *go_gitlab.Client
+	Context *GitLabContext
+}
+
+func (gitlabService GitLabService) GetChangedFiles(mergeRequestId int) ([]string, error) {
+	opt := &go_gitlab.GetMergeRequestChangesOptions{}
+
+	log.Printf("projectId: %d", *gitlabService.Context.ProjectId)
+	log.Printf("mergeRequestId: %d", mergeRequestId)
+	mergeRequestChanges, _, err := gitlabService.Client.MergeRequests.GetMergeRequestChanges(*gitlabService.Context.ProjectId, mergeRequestId, opt)
+	if err != nil {
+		log.Fatalf("error getting gitlab's merge request: %v", err)
+	}
+
+	fileNames := make([]string, len(mergeRequestChanges.Changes))
+
+	for i, change := range mergeRequestChanges.Changes {
+		fileNames[i] = change.NewPath
+		//fmt.Printf("changed file: %s \n", change.NewPath)
+	}
+	return fileNames, nil
+}
+
+func (gitlabService GitLabService) PublishComment(mergeRequestID int, comment string) {
+	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)
+
+	_, _, 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())
+	}
+}
+
+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")
+}
+
+func (gitlabService GitLabService) MergePullRequest(mergeRequestID int) error {
+	projectId := *gitlabService.Context.ProjectId
+	mergeRequestIID := *gitlabService.Context.MergeRequestIId
+	opt := &go_gitlab.AcceptMergeRequestOptions{}
+
+	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 nil
+}
+
+func (gitlabService GitLabService) IsMergeable(mergeRequestID int) (bool, string, 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
+}
+
+func (gitlabService GitLabService) IsClosed(mergeRequestID int) (bool, error) {
+	projectId := *gitlabService.Context.ProjectId
+	mergeRequestIID := *gitlabService.Context.MergeRequestIId
+
+	fmt.Printf("IsClosed 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.State == "closed" || mergeRequest.State == "merged" {
+		return true, nil
+	}
+	return false, nil
+}
+
+type GitLabEvent struct {
+	EventType GitLabEventType
+}
+
+type GitLabEventType string
+
+func (e GitLabEventType) String() string {
+	return string(e)
+}
+
+const (
+	MergeRequestOpened  = GitLabEventType("merge_request_opened")
+	MergeRequestUpdated = GitLabEventType("merge_request_updated")
+	MergeRequestClosed  = GitLabEventType("merge_request_closed")
+	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)
+
+	switch event.EventType {
+	case MergeRequestOpened:
+		for _, project := range impactedProjects {
+			workflow, ok := workflows[project.Workflow]
+			if !ok {
+				workflow = workflows["default"]
+			}
+
+			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 MergeRequestUpdated:
+		for _, project := range impactedProjects {
+			workflow, ok := workflows[project.Workflow]
+			if !ok {
+				workflow = workflows["default"]
+			}
+
+			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{
+				ProjectName:      project.Name,
+				ProjectDir:       project.Dir,
+				ProjectWorkspace: project.Workspace,
+				Terragrunt:       project.Terragrunt,
+				Commands:         workflow.Configuration.OnPullRequestClosed,
+				ApplyStage:       workflow.Apply,
+				PlanStage:        workflow.Plan,
+			})
+		}
+		return commandsPerProject, nil
+	case MergeRequestComment:
+		supportedCommands := []string{"digger plan", "digger apply", "digger unlock", "digger lock"}
+
+		for _, command := range supportedCommands {
+			if strings.Contains(gitLabContext.DiggerCommand, command) {
+				for _, project := range impactedProjects {
+					workspace := project.Workspace
+					//workspaceOverride, err := parseWorkspace(gitLabContext.DiggerCommand)
+					//if err != nil {
+					//	return []digger.ProjectCommand{}, err
+					//}
+					//if workspaceOverride != "" {
+					//	workspace = workspaceOverride
+					//}
+					commandsPerProject = append(commandsPerProject, digger.ProjectCommand{
+						ProjectName:      project.Name,
+						ProjectDir:       project.Dir,
+						ProjectWorkspace: workspace,
+						Terragrunt:       project.Terragrunt,
+						Commands:         []string{command},
+					})
+				}
+			}
+		}
+		return commandsPerProject, nil
+
+	default:
+		return []digger.ProjectCommand{}, fmt.Errorf("unsupported GitLab event type: %v", event)
+	}
+}
+
+func RunCommandsPerProject(commandsPerProject []digger.ProjectCommand, gitLabContext GitLabContext, diggerConfig *configuration.DiggerConfig, service ci.CIService, lock utils.Lock, planStorage utils.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{
+				InternalLock: lock,
+				PrManager:    service,
+				ProjectName:  projectCommands.ProjectName,
+				RepoName:     gitLabContext.ProjectName,
+				RepoOwner:    gitLabContext.ProjectNamespace,
+			}
+
+			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 {
+				terraformExecutor = terraform.Terraform{WorkingDir: path.Join(workingDir, projectCommands.ProjectDir), Workspace: projectCommands.ProjectWorkspace}
+			}
+			commandRunner := digger.CommandRunner{}
+
+			diggerExecutor := digger.DiggerExecutor{
+				gitLabContext.ProjectNamespace,
+				gitLabContext.ProjectName,
+				projectCommands.ProjectName,
+				projectPath,
+				projectCommands.StateEnvVars,
+				projectCommands.CommandEnvVars,
+				projectCommands.ApplyStage,
+				projectCommands.PlanStage,
+				commandRunner,
+				terraformExecutor,
+				service,
+				projectLock,
+				planStorage,
+			}
+
+			repoOwner := gitLabContext.ProjectNamespace
+			repoName := gitLabContext.ProjectName
+			prNumber := *gitLabContext.MergeRequestIId
+			eventName := ""
+			switch command {
+			case "digger plan":
+				utils.SendUsageRecord(repoOwner, eventName, "plan")
+				service.SetStatus(prNumber, "pending", projectCommands.ProjectName+"/plan")
+				err := diggerExecutor.Plan(prNumber)
+				if err != nil {
+					log.Printf("Failed to run digger plan command. %v", err)
+					service.SetStatus(prNumber, "failure", projectCommands.ProjectName+"/plan")
+
+					return false, fmt.Errorf("failed to run digger plan command. %v", err)
+				} else {
+					service.SetStatus(prNumber, "success", projectCommands.ProjectName+"/plan")
+				}
+			case "digger apply":
+				utils.SendUsageRecord(repoName, eventName, "apply")
+				service.SetStatus(prNumber, "pending", projectCommands.ProjectName+"/apply")
+				err := diggerExecutor.Apply(prNumber)
+				if err != nil {
+					log.Printf("Failed to run digger apply command. %v", err)
+					service.SetStatus(prNumber, "failure", projectCommands.ProjectName+"/apply")
+
+					return false, fmt.Errorf("failed to run digger apply command. %v", err)
+				} else {
+					service.SetStatus(prNumber, "success", projectCommands.ProjectName+"/apply")
+					appliesPerProject[projectCommands.ProjectName] = true
+				}
+			case "digger unlock":
+				utils.SendUsageRecord(repoOwner, eventName, "unlock")
+				err := diggerExecutor.Unlock(prNumber)
+				if err != nil {
+					return false, fmt.Errorf("failed to unlock project. %v", err)
+				}
+			case "digger lock":
+				utils.SendUsageRecord(repoOwner, eventName, "lock")
+				err := diggerExecutor.Lock(prNumber)
+				if err != nil {
+					return false, fmt.Errorf("failed to lock project. %v", err)
+				}
+			}
+		}
+	}
+
+	for _, success := range appliesPerProject {
+		if !success {
+			allAppliesSuccess = false
+		}
+	}
+	return allAppliesSuccess, nil
+	/*
+
+		lockAcquisitionSuccess := true
+		for _, projectCommands := range commandsPerProject {
+			for _, command := range projectCommands.Commands {
+				projectLock := &utils.ProjectLockImpl{
+					InternalLock: lock,
+					PrManager:    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/usage.go b/pkg/utils/usage.go
index 0cd8a872..d24e3839 100644
--- a/pkg/utils/usage.go
+++ b/pkg/utils/usage.go
@@ -6,6 +6,7 @@ import (
 	"digger/pkg/configuration"
 	"encoding/hex"
 	"encoding/json"
+	"fmt"
 	"log"
 	"net/http"
 	"os"
@@ -71,8 +72,12 @@ func sendPayload(payload interface{}) error {
 }
 
 func init() {
+	currentDir, err := os.Getwd()
+	if err != nil {
+		fmt.Printf("Failed to get current dir. %s", err)
+	}
 	walker := configuration.FileSystemDirWalker{}
-	config, err := configuration.NewDiggerConfig("", &walker)
+	config, err := configuration.NewDiggerConfig(currentDir, &walker)
 	if err != nil {
 		return
 	}


Test patch:
diff --git a/pkg/digger/digger_test.go b/pkg/digger/digger_test.go
index 75ded76f..b2643603 100644
--- a/pkg/digger/digger_test.go
+++ b/pkg/digger/digger_test.go
@@ -173,7 +173,7 @@ func TestCorrectCommandExecutionWhenApplying(t *testing.T) {
 		PlanStage:         &configuration.Stage{},
 		CommandRunner:     commandRunner,
 		TerraformExecutor: terraformExecutor,
-		ciService:         prManager,
+		CIService:         prManager,
 		ProjectLock:       lock,
 		PlanStorage:       planStorage,
 	}
@@ -215,7 +215,7 @@ func TestCorrectCommandExecutionWhenPlanning(t *testing.T) {
 		},
 		CommandRunner:     commandRunner,
 		TerraformExecutor: terraformExecutor,
-		ciService:         prManager,
+		CIService:         prManager,
 		ProjectLock:       lock,
 		PlanStorage:       planStorage,
 	}
diff --git a/pkg/gitlab/gitlab_test.go b/pkg/gitlab/gitlab_test.go
new file mode 100644
index 00000000..90cb0d17
--- /dev/null
+++ b/pkg/gitlab/gitlab_test.go
@@ -0,0 +1,36 @@
+package gitlab
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestParseGitLabContext(t *testing.T) {
+	t.Setenv("CI_PIPELINE_SOURCE", "push")
+	t.Setenv("CI_PIPELINE_ID", "1")
+	t.Setenv("CI_PIPELINE_IID", "2")
+
+	context, err := ParseGitLabContext()
+	assert.NoError(t, err)
+	assert.NotNil(t, context)
+	assert.Equal(t, PipelineSourceType("push"), context.PipelineSource)
+	assert.Equal(t, 1, *context.PipelineId)
+	assert.Equal(t, 2, *context.PipelineIId)
+	assert.Nil(t, context.MergeRequestId)
+	assert.Nil(t, context.MergeRequestIId)
+}
+
+func TestOpenMergeRequestEvent(t *testing.T) {
+	t.Setenv("CI_PIPELINE_SOURCE", "push")
+	t.Setenv("CI_PIPELINE_ID", "1")
+	t.Setenv("CI_PIPELINE_IID", "2")
+
+	context, err := ParseGitLabContext()
+	assert.NoError(t, err)
+	assert.NotNil(t, context)
+	assert.Equal(t, PipelineSourceType("push"), context.PipelineSource)
+	assert.Equal(t, 1, *context.PipelineId)
+	assert.Equal(t, 2, *context.PipelineIId)
+	assert.Nil(t, context.MergeRequestId)
+	assert.Nil(t, context.MergeRequestIId)
+}