Repository: diggerhq/digger
Pull request #100 (2023-04-04 11:52:38)

Set pull request statuses


Patch:
diff --git a/cmd/digger/main.go b/cmd/digger/main.go
index fbaad7d4..04bcb341 100644
--- a/cmd/digger/main.go
+++ b/cmd/digger/main.go
@@ -73,10 +73,16 @@ func main() {
 	}
 	println("GitHub event converted to commands successfully")
 
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, "")
+	allAppliesSuccess, err := digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, "")
 	if err != nil {
 		reportErrorAndExit(repoOwner, fmt.Sprintf("Failed to run commands. %s", err), 8)
 	}
+
+	if diggerConfig.AutoMerge && allAppliesSuccess {
+		digger.MergePullRequest(githubPrService, prNumber)
+		println("PR merged successfully")
+	}
+
 	println("Commands executed successfully")
 
 	reportErrorAndExit(repoOwner, "Digger finished successfully", 0)
diff --git a/pkg/digger/digger.go b/pkg/digger/digger.go
index 33308ac0..f607b21e 100644
--- a/pkg/digger/digger.go
+++ b/pkg/digger/digger.go
@@ -47,9 +47,12 @@ func ProcessGitHubEvent(ghEvent models.Event, diggerConfig *DiggerConfig, prMana
 	return impactedProjects, prNumber, nil
 }
 
-func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string, repoName string, eventName string, prNumber int, diggerConfig *DiggerConfig, prManager github.PullRequestManager, lock utils.Lock, workingDir string) error {
+func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string, repoName string, eventName string, prNumber int, diggerConfig *DiggerConfig, prManager github.PullRequestManager, lock utils.Lock, workingDir string) (bool, error) {
 	lockAcquisitionSuccess := true
+	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,
@@ -75,10 +78,23 @@ func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string
 			switch command {
 			case "digger plan":
 				utils.SendUsageRecord(repoOwner, eventName, "plan")
-				diggerExecutor.Plan(prNumber)
+				prManager.SetStatus(prNumber, "pending", projectCommands.ProjectName+"/plan")
+				err := diggerExecutor.Plan(prNumber)
+				if err != nil {
+					prManager.SetStatus(prNumber, "failure", projectCommands.ProjectName+"/plan")
+				} else {
+					prManager.SetStatus(prNumber, "success", projectCommands.ProjectName+"/plan")
+				}
 			case "digger apply":
 				utils.SendUsageRecord(repoName, eventName, "apply")
-				diggerExecutor.Apply(prNumber)
+				prManager.SetStatus(prNumber, "pending", projectCommands.ProjectName+"/apply")
+				err := diggerExecutor.Apply(prNumber)
+				if err != nil {
+					prManager.SetStatus(prNumber, "failure", projectCommands.ProjectName+"/apply")
+				} else {
+					prManager.SetStatus(prNumber, "success", projectCommands.ProjectName+"/apply")
+					appliesPerProject[projectCommands.ProjectName] = true
+				}
 			case "digger unlock":
 				utils.SendUsageRecord(repoOwner, eventName, "unlock")
 				diggerExecutor.Unlock(prNumber)
@@ -92,7 +108,39 @@ func RunCommandsPerProject(commandsPerProject []ProjectCommand, repoOwner string
 	if !lockAcquisitionSuccess {
 		os.Exit(1)
 	}
-	return nil
+	for _, success := range appliesPerProject {
+		if !success {
+			allAppliesSuccess = false
+		}
+	}
+	return allAppliesSuccess, nil
+}
+
+func MergePullRequest(githubPrService github.PullRequestManager, prNumber int) {
+	combinedStatus, err := githubPrService.GetCombinedPullRequestStatus(prNumber)
+
+	if err != nil {
+		log.Fatalf("failed to get combined status, %v", err)
+	}
+
+	if combinedStatus != "success" {
+		log.Fatalf("PR is not mergeable. Status: %v", combinedStatus)
+	}
+
+	prIsMergeable, mergeableState, err := githubPrService.IsMergeable(prNumber)
+
+	if err != nil {
+		log.Fatalf("failed to check if PR is mergeable, %v", err)
+	}
+
+	if !prIsMergeable {
+		log.Fatalf("PR is not mergeable. State: %v", mergeableState)
+	}
+
+	err = githubPrService.MergePullRequest(prNumber)
+	if err != nil {
+		log.Fatalf("failed to merge PR, %v", err)
+	}
 }
 
 func GetGitHubContext(ghContext string) (*models.Github, error) {
@@ -242,7 +290,7 @@ func (d DiggerExecutor) LockId() string {
 	return d.repoOwner + "/" + d.repoName + "#" + d.projectName
 }
 
-func (d DiggerExecutor) Plan(prNumber int) {
+func (d DiggerExecutor) Plan(prNumber int) error {
 
 	var terraformExecutor terraform.TerraformExecutor
 
@@ -254,7 +302,7 @@ func (d DiggerExecutor) Plan(prNumber int) {
 
 	res, err := d.lock.Lock(d.LockId(), prNumber)
 	if err != nil {
-		log.Fatalf("Error locking project: %v", err)
+		return fmt.Errorf("Error locking project: %v", err)
 	}
 	if res {
 		var initArgs []string
@@ -271,16 +319,16 @@ func (d DiggerExecutor) Plan(prNumber int) {
 		isNonEmptyPlan, stdout, stderr, err := terraformExecutor.Plan(initArgs, planArgs)
 
 		if err != nil {
-			log.Fatalf("Error executing plan: %v", err)
+			return fmt.Errorf("error executing plan: %v", err)
 		}
 		plan := cleanupTerraformPlan(isNonEmptyPlan, err, stdout, stderr)
 		comment := "Plan for **" + d.LockId() + "**\n" + plan
 		d.prManager.PublishComment(prNumber, comment)
 	}
-
+	return nil
 }
 
-func (d DiggerExecutor) Apply(prNumber int) {
+func (d DiggerExecutor) Apply(prNumber int) error {
 	var terraformExecutor terraform.TerraformExecutor
 
 	if d.terragrunt {
@@ -308,14 +356,13 @@ func (d DiggerExecutor) Apply(prNumber int) {
 		if err == nil {
 			_, err := d.lock.Unlock(d.LockId(), prNumber)
 			if err != nil {
-				fmt.Errorf("error unlocking project: %v", err)
+				return fmt.Errorf("error unlocking project: %v", err)
 			}
 		} else {
-
 			d.prManager.PublishComment(prNumber, "Error during applying. Project lock will persist")
 		}
 	}
-
+	return nil
 }
 
 func (d DiggerExecutor) Unlock(prNumber int) {
diff --git a/pkg/digger/digger_config.go b/pkg/digger/digger_config.go
index 5c347df1..61cd387f 100644
--- a/pkg/digger/digger_config.go
+++ b/pkg/digger/digger_config.go
@@ -18,6 +18,7 @@ type WorkflowConfiguration struct {
 
 type DiggerConfig struct {
 	Projects  []Project           `yaml:"projects"`
+	AutoMerge bool                `yaml:"auto_merge"`
 	Workflows map[string]Workflow `yaml:"workflows"`
 }
 
diff --git a/pkg/github/github.go b/pkg/github/github.go
index 98d6ee63..4e737efe 100644
--- a/pkg/github/github.go
+++ b/pkg/github/github.go
@@ -25,6 +25,10 @@ type GithubPullRequestService struct {
 type PullRequestManager interface {
 	GetChangedFiles(prNumber int) ([]string, error)
 	PublishComment(prNumber int, comment string)
+	SetStatus(prNumber int, status string, statusContext string) error
+	GetCombinedPullRequestStatus(prNumber int) (string, error)
+	MergePullRequest(prNumber int) error
+	IsMergeable(prNumber int) (bool, string, error)
 }
 
 func (svc *GithubPullRequestService) GetChangedFiles(prNumber int) ([]string, error) {
@@ -47,3 +51,53 @@ func (svc *GithubPullRequestService) PublishComment(prNumber int, comment string
 		log.Fatalf("error publishing comment: %v", err)
 	}
 }
+
+func (svc *GithubPullRequestService) SetStatus(prNumber int, status string, statusContext string) error {
+	pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber)
+	if err != nil {
+		log.Fatalf("error getting pull request: %v", err)
+	}
+
+	_, _, err = svc.Client.Repositories.CreateStatus(context.Background(), svc.Owner, svc.RepoName, *pr.Head.SHA, &github.RepoStatus{
+		State:       &status,
+		Context:     &statusContext,
+		Description: &statusContext,
+	})
+	return err
+}
+
+func (svc *GithubPullRequestService) GetCombinedPullRequestStatus(prNumber int) (string, error) {
+	pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber)
+	if err != nil {
+		log.Fatalf("error getting pull request: %v", err)
+	}
+
+	statuses, _, err := svc.Client.Repositories.GetCombinedStatus(context.Background(), svc.Owner, svc.RepoName, pr.Head.GetSHA(), nil)
+	if err != nil {
+		log.Fatalf("error getting combined status: %v", err)
+	}
+
+	return *statuses.State, nil
+}
+
+func (svc *GithubPullRequestService) MergePullRequest(prNumber int) error {
+	pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber)
+	if err != nil {
+		log.Fatalf("error getting pull request: %v", err)
+	}
+
+	_, _, err = svc.Client.PullRequests.Merge(context.Background(), svc.Owner, svc.RepoName, prNumber, "auto-merge", &github.PullRequestOptions{
+		MergeMethod: "squash",
+		SHA:         pr.Head.GetSHA(),
+	})
+	return err
+}
+
+func (svc *GithubPullRequestService) IsMergeable(prNumber int) (bool, string, error) {
+	pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber)
+	if err != nil {
+		log.Fatalf("error getting pull request: %v", err)
+	}
+
+	return pr.GetMergeable(), pr.GetMergeableState(), nil
+}
diff --git a/pkg/github/pullrequestmanager_mock.go b/pkg/github/pullrequestmanager_mock.go
index 8ca420e0..69979f4a 100644
--- a/pkg/github/pullrequestmanager_mock.go
+++ b/pkg/github/pullrequestmanager_mock.go
@@ -12,3 +12,23 @@ func (mockGithubPullrequestManager *MockGithubPullrequestManager) GetChangedFile
 func (mockGithubPullrequestManager *MockGithubPullrequestManager) PublishComment(prNumber int, comment string) {
 	mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "PublishComment")
 }
+
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) SetStatus(prNumber int, status string, statusContext string) error {
+	mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "SetStatus")
+	return nil
+}
+
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) GetCombinedPullRequestStatus(prNumber int) (string, error) {
+	mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "GetCombinedPullRequestStatus")
+	return "", nil
+}
+
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) MergePullRequest(prNumber int) error {
+	mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "MergePullRequest")
+	return nil
+}
+
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) IsMergeable(prNumber int) (bool, string, error) {
+	mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "IsMergeable")
+	return true, "", nil
+}
diff --git a/pkg/utils/mocks.go b/pkg/utils/mocks.go
index 68163988..acb40570 100644
--- a/pkg/utils/mocks.go
+++ b/pkg/utils/mocks.go
@@ -49,3 +49,19 @@ func (t MockPullRequestManager) GetChangedFiles(prNumber int) ([]string, error)
 func (t MockPullRequestManager) PublishComment(prNumber int, comment string) {
 
 }
+
+func (t MockPullRequestManager) SetStatus(prNumber int, status string, statusContext string) error {
+	return nil
+}
+
+func (t MockPullRequestManager) GetCombinedPullRequestStatus(prNumber int) (string, error) {
+	return "", nil
+}
+
+func (t MockPullRequestManager) MergePullRequest(prNumber int) error {
+	return nil
+}
+
+func (t MockPullRequestManager) IsMergeable(prNumber int) (bool, string, error) {
+	return true, "", nil
+}


Test patch:
diff --git a/cmd/digger/main_test.go b/cmd/digger/main_test.go
index 48f82d30..b119f509 100644
--- a/cmd/digger/main_test.go
+++ b/cmd/digger/main_test.go
@@ -873,8 +873,10 @@ func TestGitHubNewPullRequestContext(t *testing.T) {
 	lock := &utils.MockLock{}
 	prManager := &utils.MockPullRequestManager{ChangedFiles: []string{"dev/test.tf"}}
 	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
+
 	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, &diggerConfig, prManager, lock, "")
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, &diggerConfig, prManager, lock, "")
+
 	assert.NoError(t, err)
 	if err != nil {
 		fmt.Println(err)
@@ -893,8 +895,10 @@ func TestGitHubNewCommentContext(t *testing.T) {
 	lock := &utils.MockLock{}
 	prManager := &utils.MockPullRequestManager{ChangedFiles: []string{"dev/test.tf"}}
 	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, &diggerConfig, prManager)
+
 	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, &diggerConfig, prManager, lock, "")
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, context.RepositoryOwner, context.Repository, eventName, prNumber, &diggerConfig, prManager, lock, "")
+
 	assert.NoError(t, err)
 	if err != nil {
 		fmt.Println(err)
diff --git a/pkg/integration/integration_test.go b/pkg/integration/integration_test.go
index 4a0836a4..1bea3f69 100644
--- a/pkg/integration/integration_test.go
+++ b/pkg/integration/integration_test.go
@@ -381,7 +381,7 @@ func TestHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
 
 	projectLock := &utils.ProjectLockImpl{
@@ -408,7 +408,7 @@ func TestHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
 
 	println("--- digger apply comment ---")
@@ -422,7 +422,7 @@ func TestHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
 
 	projectLock = &utils.ProjectLockImpl{
@@ -446,7 +446,7 @@ func TestHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, lock, dir)
 	assert.NoError(t, err)
 
 	projectLock = &utils.ProjectLockImpl{
@@ -524,7 +524,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
 
 	projectLock := &utils.ProjectLockImpl{
@@ -550,7 +550,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
 
 	println("--- digger apply comment ---")
@@ -564,7 +564,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
 
 	projectLock = &utils.ProjectLockImpl{
@@ -588,7 +588,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
 	assert.NoError(t, err)
 	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]digger.Workflow{})
 	assert.NoError(t, err)
-	err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, diggerConfig, githubPrService, &dynamoDbLock, dir)
 	assert.NoError(t, err)
 
 	projectLock = &utils.ProjectLockImpl{