Repository: diggerhq/digger
Pull request #169 (2023-04-25 23:12:32)

feat: post collapsible plan and apply output PR comments
The output of `terraform plan` and `terraform apply` commands can be very big, and could clutter the PR everytime being posted in comments. Therefore making use of collapsible comments instead:

![image](https://user-images.githubusercontent.com/17277004/234440539-96472320-f5b2-4ac4-8b0e-e4ce68e38730.png)

![image](https://user-images.githubusercontent.com/17277004/234440948-82b87c16-f94a-41ca-91a5-4b233275ed9b.png)


veziak: just tested it manually, if comment looks like this :

![image](https://user-images.githubusercontent.com/2407061/234642901-809a0a99-5af9-489e-93d9-7b3b5c615144.png)

then it works as expected, it's important that there is a new line between summary and terraform line:

<details>
  <summary>Plan</summary>

```terraform
resource "null_resource" "test" {
}
```
</details>


fleroux514: > just tested it manually, if comment looks like this :
> 
> ![image](https://user-images.githubusercontent.com/2407061/234642901-809a0a99-5af9-489e-93d9-7b3b5c615144.png)
> 
> then it works as expected, it's important that there is a new line between summary and terraform line:
> 
> Plan

Thanks @veziak, may have been a testing error on my part, it does work.

Patch:
diff --git a/pkg/digger/digger.go b/pkg/digger/digger.go
index edf2fd25..1d3906ec 100644
--- a/pkg/digger/digger.go
+++ b/pkg/digger/digger.go
@@ -370,7 +370,7 @@ func (d DiggerExecutor) Plan(prNumber int) error {
 					return fmt.Errorf("error executing plan: %v", err)
 				}
 				plan := cleanupTerraformPlan(isNonEmptyPlan, err, stdout, stderr)
-				comment := "Plan for **" + d.lock.LockId() + "**\n" + plan
+				comment := utils.GetTerraformOutputAsCollapsibleComment("Plan for **"+d.lock.LockId()+"**", plan)
 				d.prManager.PublishComment(prNumber, comment)
 			}
 			if step.Action == "run" {
@@ -442,7 +442,7 @@ func (d DiggerExecutor) Apply(prNumber int) error {
 				if step.Action == "apply" {
 					stdout, stderr, err := d.terraformExecutor.Apply(step.ExtraArgs, plansFilename)
 					applyOutput := cleanupTerraformApply(true, err, stdout, stderr)
-					comment := "Apply for **" + d.lock.LockId() + "**\n" + applyOutput
+					comment := utils.GetTerraformOutputAsCollapsibleComment("Apply for **"+d.lock.LockId()+"**", applyOutput)
 					d.prManager.PublishComment(prNumber, comment)
 					if err == nil {
 						_, err := d.lock.Unlock(prNumber)
@@ -483,7 +483,7 @@ func (d DiggerExecutor) Lock(prNumber int) error {
 }
 
 func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string, stderr string, regexStr string) string {
-	var errorStr, result, start string
+	var errorStr, start string
 
 	// removes output of terraform -version command that terraform-exec executes on every run
 	i := strings.Index(stdout, "Initializing the backend...")
@@ -498,7 +498,7 @@ func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string,
 		} else if stderr != "" {
 			errorStr = stderr
 		}
-		return "```terraform\n" + errorStr + "\n```"
+		return errorStr
 	} else if nonEmptyOutput {
 		start = "Terraform will perform the following actions:"
 	} else {
@@ -516,8 +516,7 @@ func cleanupTerraformOutput(nonEmptyOutput bool, planError error, stdout string,
 		endPos = strings.Index(stdout, matches[0]) + len(matches[0])
 	}
 
-	result = stdout[startPos:endPos]
-	return "```terraform\n" + result + "\n```"
+	return stdout[startPos:endPos]
 }
 
 func cleanupTerraformApply(nonEmptyPlan bool, planError error, stdout string, stderr string) string {
diff --git a/pkg/terraform/utils.go b/pkg/terraform/utils.go
index bb3dde81..ece62b4f 100644
--- a/pkg/terraform/utils.go
+++ b/pkg/terraform/utils.go
@@ -126,3 +126,22 @@ workflows:
 		log.Fatal(err2)
 	}
 }
+
+func CreateCustomDiggerYmlFile(dir string, diggerYaml string) {
+	f, err := os.Create(dir + "/digger.yml")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	defer func(f *os.File) {
+		err := f.Close()
+		if err != nil {
+
+		}
+	}(f)
+
+	_, err2 := f.WriteString(diggerYaml)
+	if err2 != nil {
+		log.Fatal(err2)
+	}
+}
diff --git a/pkg/utils/comments.go b/pkg/utils/comments.go
new file mode 100644
index 00000000..10dca154
--- /dev/null
+++ b/pkg/utils/comments.go
@@ -0,0 +1,13 @@
+package utils
+
+func GetTerraformOutputAsCollapsibleComment(summary string, collapsedComment string) string {
+	str := `<details>
+  <summary>` + summary + `</summary>
+
+  ` + "```terraform" + `
+` + collapsedComment + `
+  ` + "```" + `
+</details>`
+
+	return str
+}


Test patch:
diff --git a/pkg/configuration/digger_config_test.go b/pkg/configuration/digger_config_test.go
index 5fe0e794..55930eb4 100644
--- a/pkg/configuration/digger_config_test.go
+++ b/pkg/configuration/digger_config_test.go
@@ -92,6 +92,30 @@ projects:
 	assert.Equal(t, "path/to/module", dg.GetDirectory("dev"))
 }
 
+func TestCustomCommandsConfiguration(t *testing.T) {
+	tempDir, teardown := setUp()
+	defer teardown()
+
+	diggerCfg := `
+projects:
+- name: dev
+  dir: infra/dev
+  workflow: myworkflow
+
+workflows:
+  myworkflow:
+    plan:
+      steps:
+      - run: echo "hello"
+`
+	deleteFile := createFile(path.Join(tempDir, "digger.yaml"), diggerCfg)
+	defer deleteFile()
+
+	dg, err := NewDiggerConfig(tempDir, &FileSystemDirWalker{})
+	assert.NoError(t, err, "expected error to be nil")
+	assert.Equal(t, Step{Action: "run", Value: "echo \"hello\""}, dg.Workflows["myworkflow"].Plan.Steps[0], "parsed struct does not match expected struct")
+}
+
 func TestDefaultValuesForWorkflowConfiguration(t *testing.T) {
 	tempDir, teardown := setUp()
 	defer teardown()
diff --git a/pkg/digger/digger_test.go b/pkg/digger/digger_test.go
index f49fc58b..27335d40 100644
--- a/pkg/digger/digger_test.go
+++ b/pkg/digger/digger_test.go
@@ -156,7 +156,7 @@ func TestCorrectCommandExecutionWhenApplying(t *testing.T) {
 
 	commandStrings := allCommandsInOrderWithParams(terraformExecutor, commandRunner, prManager, lock)
 
-	assert.Equal(t, []string{"DownloadLatestPlans 1", "IsMergeable 1", "Lock 1", "Init ", "Apply  plan", "LockId ", "PublishComment 1 Apply for ****\n```terraform\n\n```", "Unlock 1", "Run echo", "LockId "}, commandStrings)
+	assert.Equal(t, []string{"DownloadLatestPlans 1", "IsMergeable 1", "Lock 1", "Init ", "Apply  plan", "LockId ", "PublishComment 1 <details>\n  <summary>Apply for ****</summary>\n\n  ```terraform\n\n  ```\n</details>", "Unlock 1", "Run echo", "LockId "}, commandStrings)
 }
 
 func TestCorrectCommandExecutionWhenPlanning(t *testing.T) {
@@ -197,7 +197,7 @@ func TestCorrectCommandExecutionWhenPlanning(t *testing.T) {
 
 	commandStrings := allCommandsInOrderWithParams(terraformExecutor, commandRunner, prManager, lock)
 
-	assert.Equal(t, []string{"Lock 1", "Init ", "Plan -out .tfplan", "LockId ", "PublishComment 1 Plan for ****\n```terraform\n\n```", "Run echo", "LockId "}, commandStrings)
+	assert.Equal(t, []string{"Lock 1", "Init ", "Plan -out .tfplan", "LockId ", "PublishComment 1 <details>\n  <summary>Plan for ****</summary>\n\n  ```terraform\n\n  ```\n</details>", "Run echo", "LockId "}, commandStrings)
 }
 
 func allCommandsInOrderWithParams(terraformExecutor *MockTerraformExecutor, commandRunner *MockCommandRunner, prManager *MockPRManager, lock *MockProjectLock) []string {
diff --git a/pkg/integration/integration_test.go b/pkg/integration/integration_test.go
index abde35c6..b8565165 100644
--- a/pkg/integration/integration_test.go
+++ b/pkg/integration/integration_test.go
@@ -328,6 +328,11 @@ var githubContextUnknownEventJson = `{
 }`
 
 func TestHappyPath(t *testing.T) {
+	/*
+		to be able to run this test following env vars should be configured:
+		AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, GITHUB_TOKEN
+	*/
+
 	SkipCI(t)
 
 	dir := terraform.CreateTestTerraformProject()
@@ -658,3 +663,153 @@ func TestNonExistentGitHubEvent(t *testing.T) {
 	assert.Error(t, err)
 	assert.Equal(t, "error parsing GitHub context JSON: unknown GitHub event: non_existent_event", err.Error())
 }
+
+func TestCustomCommandHappyPath(t *testing.T) {
+	SkipCI(t)
+	diggerCfg := `
+projects:
+- name: dev
+  dir: infra/dev
+  workflow: myworkflow
+
+workflows:
+  myworkflow:
+    plan:
+      steps:
+      - run: echo "hello"
+`
+
+	dir := terraform.CreateTestTerraformProject()
+
+	defer func(name string) {
+		err := os.RemoveAll(name)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}(dir)
+
+	terraform.CreateValidTerraformTestFile(dir)
+	terraform.CreateCustomDiggerYmlFile(dir, diggerCfg)
+
+	diggerConfig, err := configuration.NewDiggerConfig(dir, &configuration.FileSystemDirWalker{})
+	assert.NoError(t, err)
+
+	assert.NotNil(t, diggerConfig.Workflows)
+	assert.NotNil(t, diggerConfig.Workflows["myworkflow"])
+	assert.NotNil(t, diggerConfig.Workflows["myworkflow"].Configuration)
+	assert.NotNil(t, diggerConfig.Workflows["myworkflow"].Configuration.OnCommitToDefault)
+
+	lock, err := utils.GetLock()
+	assert.NoError(t, err)
+	assert.NotNil(t, lock, "failed to create lock")
+
+	ghToken := os.Getenv("GITHUB_TOKEN")
+	assert.NotEmpty(t, ghToken)
+
+	println("--- new pull request ---")
+	newPullRequestContext := githubContextNewPullRequestMinJson
+	parsedNewPullRequestContext, err := digger.GetGitHubContext(newPullRequestContext)
+	assert.NoError(t, err)
+
+	diggerPlanCommentContext := githubContextDiggerPlanCommentMinJson
+	parsedDiggerPlanCommentContext, err := digger.GetGitHubContext(diggerPlanCommentContext)
+	assert.NoError(t, err)
+
+	diggerApplyCommentContext := githubContextDiggerApplyCommentMinJson
+	parsedDiggerApplyCommentContext, err := digger.GetGitHubContext(diggerApplyCommentContext)
+	assert.NoError(t, err)
+
+	diggerUnlockCommentContext := githubContextDiggerUnlockCommentMinJson
+	parsedDiggerUnlockCommentContext, err := digger.GetGitHubContext(diggerUnlockCommentContext)
+	assert.NoError(t, err)
+
+	ghEvent := parsedNewPullRequestContext.Event
+	eventName := parsedNewPullRequestContext.EventName
+	repoOwner := parsedNewPullRequestContext.RepositoryOwner
+	repositoryName := parsedNewPullRequestContext.Repository
+	githubPrService := github.NewGithubPullRequestService(ghToken, repositoryName, repoOwner)
+
+	assert.Equal(t, "pull_request", parsedNewPullRequestContext.EventName)
+
+	//  new pr should lock the project
+	impactedProjects, prNumber, err := digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	assert.NoError(t, err)
+	commandsToRunPerProject, err := digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]configuration.Workflow{})
+	assert.NoError(t, err)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, dir)
+	assert.NoError(t, err)
+
+	projectLock := &utils.ProjectLockImpl{
+		InternalLock: lock,
+		PrManager:    githubPrService,
+		ProjectName:  "dev",
+		RepoName:     repositoryName,
+		RepoOwner:    repoOwner,
+	}
+	resource := repositoryName + "#" + projectLock.ProjectName
+	transactionId, err := projectLock.InternalLock.GetLock(resource)
+	assert.NoError(t, err)
+	assert.NotNil(t, transactionId)
+	assert.Equal(t, 42, *transactionId, "TransactionId")
+
+	println("--- digger plan comment ---")
+	ghEvent = parsedDiggerPlanCommentContext.Event
+	eventName = parsedDiggerPlanCommentContext.EventName
+	repoOwner = parsedDiggerPlanCommentContext.RepositoryOwner
+	repositoryName = parsedDiggerPlanCommentContext.Repository
+
+	// 'digger plan' comment should trigger terraform execution
+	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	assert.NoError(t, err)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]configuration.Workflow{})
+	assert.NoError(t, err)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, dir)
+	assert.NoError(t, err)
+
+	println("--- digger apply comment ---")
+	ghEvent = parsedDiggerApplyCommentContext.Event
+	eventName = parsedDiggerApplyCommentContext.EventName
+	repoOwner = parsedDiggerApplyCommentContext.RepositoryOwner
+	repositoryName = parsedDiggerApplyCommentContext.Repository
+
+	// 'digger apply' comment should trigger terraform execution and unlock the project
+	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	assert.NoError(t, err)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]configuration.Workflow{})
+	assert.NoError(t, err)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, dir)
+	assert.NoError(t, err)
+
+	projectLock = &utils.ProjectLockImpl{
+		InternalLock: lock,
+		PrManager:    githubPrService,
+		ProjectName:  "dev",
+		RepoName:     repositoryName,
+		RepoOwner:    repoOwner,
+	}
+	transactionId, err = projectLock.InternalLock.GetLock(resource)
+	assert.NoError(t, err)
+
+	println("--- digger unlock comment ---")
+	ghEvent = parsedDiggerUnlockCommentContext.Event
+	eventName = parsedDiggerUnlockCommentContext.EventName
+	repoOwner = parsedDiggerUnlockCommentContext.RepositoryOwner
+	repositoryName = parsedDiggerUnlockCommentContext.Repository
+
+	impactedProjects, prNumber, err = digger.ProcessGitHubEvent(ghEvent, diggerConfig, githubPrService)
+	assert.NoError(t, err)
+	commandsToRunPerProject, err = digger.ConvertGithubEventToCommands(ghEvent, impactedProjects, map[string]configuration.Workflow{})
+	assert.NoError(t, err)
+	_, err = digger.RunCommandsPerProject(commandsToRunPerProject, repoOwner, repositoryName, eventName, prNumber, githubPrService, lock, dir)
+	assert.NoError(t, err)
+
+	projectLock = &utils.ProjectLockImpl{
+		InternalLock: lock,
+		PrManager:    githubPrService,
+		ProjectName:  "dev",
+		RepoName:     repositoryName,
+		RepoOwner:    repoOwner,
+	}
+	transactionId, err = projectLock.InternalLock.GetLock(resource)
+	assert.NoError(t, err)
+}