diff --git a/action.yml b/action.yml
index c8a8580a7..c7f1d88ab 100644
--- a/action.yml
+++ b/action.yml
@@ -86,6 +86,10 @@ inputs:
description: Post plans as one comment
required: false
default: 'false'
+ reporting-strategy:
+ description: 'comments_per_run or latest_run_comment, anything else will default to original behavior of multiple comments'
+ required: false
+ default: 'comments_per_run'
outputs:
output:
value: ${{ steps.digger.outputs.output }}
@@ -186,6 +190,7 @@ runs:
DIGGER_TOKEN: ${{ inputs.digger-token }}
DIGGER_HOSTNAME: ${{ inputs.digger-hostname }}
ACCUMULATE_PLANS: ${{ inputs.post-plans-as-one-comment == 'true' }}
+ REPORTING_STRATEGY: ${{ inputs.reporting-strategy }}
run: |
cd ${{ github.action_path }}
go build -o digger ./cmd/digger
@@ -203,6 +208,7 @@ runs:
DIGGER_TOKEN: ${{ inputs.digger-token }}
DIGGER_HOSTNAME: ${{ inputs.digger-hostname }}
ACCUMULATE_PLANS: ${{ inputs.post-plans-as-one-comment == 'true' }}
+ REPORTING_STRATEGY: ${{ inputs.reporting-strategy }}
id: digger
shell: bash
run: |
diff --git a/cmd/digger/main.go b/cmd/digger/main.go
index 5d22b2f8f..26b8c8c25 100644
--- a/cmd/digger/main.go
+++ b/cmd/digger/main.go
@@ -24,11 +24,12 @@ import (
"net/http"
"os"
"strings"
+ "time"
"github.com/google/go-github/v53/github"
)
-func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
+func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker, reportingStrategy reporting.ReportStrategy) {
println("Using GitHub.")
githubActor := os.Getenv("GITHUB_ACTOR")
if githubActor != "" {
@@ -108,8 +109,9 @@ func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
planStorage := newPlanStorage(ghToken, repoOwner, repositoryName, githubActor, prNumber)
reporter := &reporting.CiReporter{
- CiService: githubPrService,
- PrNumber: prNumber,
+ CiService: githubPrService,
+ PrNumber: prNumber,
+ ReportStrategy: reportingStrategy,
}
currentDir, err := os.Getwd()
if err != nil {
@@ -136,7 +138,7 @@ func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
}()
}
-func gitLabCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
+func gitLabCI(lock core_locking.Lock, policyChecker core_policy.Checker, reportingStrategy reporting.ReportStrategy) {
println("Using GitLab.")
projectNamespace := os.Getenv("CI_PROJECT_NAMESPACE")
@@ -201,8 +203,9 @@ func gitLabCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
diggerProjectNamespace := gitLabContext.ProjectNamespace + "/" + gitLabContext.ProjectName
planStorage := newPlanStorage("", "", "", gitLabContext.GitlabUserName, *gitLabContext.MergeRequestIId)
reporter := &reporting.CiReporter{
- CiService: gitlabService,
- PrNumber: *gitLabContext.MergeRequestIId,
+ CiService: gitlabService,
+ PrNumber: *gitLabContext.MergeRequestIId,
+ ReportStrategy: reportingStrategy,
}
allAppliesSuccess, atLeastOneApply, err := digger.RunCommandsPerProject(commandsToRunPerProject, &dependencyGraph, diggerProjectNamespace, gitLabContext.GitlabUserName, gitLabContext.EventType.String(), *gitLabContext.MergeRequestIId, gitlabService, lock, reporter, planStorage, policyChecker, currentDir)
@@ -227,7 +230,7 @@ func gitLabCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
}()
}
-func azureCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
+func azureCI(lock core_locking.Lock, policyChecker core_policy.Checker, reportingStrategy reporting.ReportStrategy) {
fmt.Println("> Azure CI detected")
azureContext := os.Getenv("AZURE_CONTEXT")
azureToken := os.Getenv("AZURE_TOKEN")
@@ -279,8 +282,9 @@ func azureCI(lock core_locking.Lock, policyChecker core_policy.Checker) {
diggerProjectNamespace := parsedAzureContext.BaseUrl + "/" + parsedAzureContext.ProjectName
reporter := &reporting.CiReporter{
- CiService: azureService,
- PrNumber: prNumber,
+ CiService: azureService,
+ PrNumber: prNumber,
+ ReportStrategy: reportingStrategy,
}
allAppliesSuccess, atLeastOneApply, err := digger.RunCommandsPerProject(commandsToRunPerProject, &dependencyGraph, diggerProjectNamespace, parsedAzureContext.BaseUrl, parsedAzureContext.EventType, prNumber, azureService, lock, reporter, planStorage, policyChecker, currentDir)
if err != nil {
@@ -338,6 +342,21 @@ func main() {
} else {
policyChecker = policy.NoOpPolicyChecker{}
}
+
+ var reportStrategy reporting.ReportStrategy
+
+ if os.Getenv("REPORTING_STRATEGY") == "comments_per_run" || os.Getenv("ACCUMULATE_PLANS") == "true" {
+ reportStrategy = &reporting.CommentPerRunStrategy{
+ TimeOfRun: time.Now(),
+ }
+ } else if os.Getenv("REPORTING_STRATEGY") == "latest_run_comment" {
+ reportStrategy = &reporting.LatestRunCommentStrategy{
+ TimeOfRun: time.Now(),
+ }
+ } else {
+ reportStrategy = &reporting.MultipleCommentsStrategy{}
+ }
+
lock, err := locking.GetLock()
if err != nil {
fmt.Printf("Failed to create lock provider. %s\n", err)
@@ -348,11 +367,11 @@ func main() {
ci := digger.DetectCI()
switch ci {
case digger.GitHub:
- gitHubCI(lock, policyChecker)
+ gitHubCI(lock, policyChecker, reportStrategy)
case digger.GitLab:
- gitLabCI(lock, policyChecker)
+ gitLabCI(lock, policyChecker, reportStrategy)
case digger.Azure:
- azureCI(lock, policyChecker)
+ azureCI(lock, policyChecker, reportStrategy)
case digger.BitBucket:
case digger.None:
print("No CI detected.")
diff --git a/pkg/azure/azure.go b/pkg/azure/azure.go
index c5b2a329c..faa393e60 100644
--- a/pkg/azure/azure.go
+++ b/pkg/azure/azure.go
@@ -313,6 +313,44 @@ func (a *AzureReposService) IsMerged(prNumber int) (bool, error) {
return *pullRequest.Status == git.PullRequestStatusValues.Completed, nil
}
+func (a *AzureReposService) EditComment(id interface{}, comment string) error {
+ threadId := id.(int)
+ comments := []git.Comment{
+ {
+ Content: &comment,
+ },
+ }
+ _, err := a.Client.UpdateThread(context.Background(), git.UpdateThreadArgs{
+ Project: &a.ProjectName,
+ RepositoryId: &a.RepositoryId,
+ ThreadId: &threadId,
+ CommentThread: &git.GitPullRequestCommentThread{
+ Comments: &comments,
+ },
+ })
+ return err
+}
+
+func (a *AzureReposService) GetComments(prNumber int) ([]ci.Comment, error) {
+ comments, err := a.Client.GetComments(context.Background(), git.GetCommentsArgs{
+ Project: &a.ProjectName,
+ RepositoryId: &a.RepositoryId,
+ PullRequestId: &prNumber,
+ })
+ if err != nil {
+ return nil, err
+ }
+ var result []ci.Comment
+ for _, comment := range *comments {
+ result = append(result, ci.Comment{
+ Id: *comment.Id,
+ Body: comment.Content,
+ })
+ }
+ return result, nil
+
+}
+
func ProcessAzureReposEvent(azureEvent interface{}, diggerConfig *configuration.DiggerConfig, ciService ci.CIService) ([]configuration.Project, *configuration.Project, int, error) {
var impactedProjects []configuration.Project
var prNumber int
diff --git a/pkg/ci/ci.go b/pkg/ci/ci.go
index 591a9c3d5..3caaf5121 100644
--- a/pkg/ci/ci.go
+++ b/pkg/ci/ci.go
@@ -3,6 +3,8 @@ package ci
type CIService interface {
GetChangedFiles(prNumber int) ([]string, error)
PublishComment(prNumber int, comment string) error
+ EditComment(id interface{}, comment string) error
+ GetComments(prNumber int) ([]Comment, 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)
@@ -15,3 +17,8 @@ type CIService interface {
IsClosed(prNumber int) (bool, error)
GetUserTeams(organisation string, user string) ([]string, error)
}
+
+type Comment struct {
+ Id interface{}
+ Body *string
+}
diff --git a/pkg/core/execution/execution.go b/pkg/core/execution/execution.go
index 6cdc4cb66..2f0c6c063 100644
--- a/pkg/core/execution/execution.go
+++ b/pkg/core/execution/execution.go
@@ -162,13 +162,14 @@ func (d DiggerExecutor) Apply() (bool, error) {
if step.Action == "apply" {
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)
- commentErr := d.Reporter.Report(comment)
+ formatter := utils.GetTerraformOutputAsCollapsibleComment("Apply for " + d.ProjectLock.LockId() + "")
+
+ commentErr := d.Reporter.Report(applyOutput, formatter)
if commentErr != nil {
fmt.Printf("error publishing comment: %v", err)
}
if err != nil {
- commentErr = d.Reporter.Report("Error during applying.")
+ commentErr = d.Reporter.Report(err.Error(), utils.AsCollapsibleComment("Error during applying."))
if commentErr != nil {
fmt.Printf("error publishing comment: %v", err)
}
diff --git a/pkg/core/reporting/reporting.go b/pkg/core/reporting/reporting.go
index 622a533eb..99b532185 100644
--- a/pkg/core/reporting/reporting.go
+++ b/pkg/core/reporting/reporting.go
@@ -1,5 +1,5 @@
package reporting
type Reporter interface {
- Report(report string) error
+ Report(report string, reportFormatting func(report string) string) error
}
diff --git a/pkg/core/utils/comments.go b/pkg/core/utils/comments.go
index 10dca1540..e8413fd95 100644
--- a/pkg/core/utils/comments.go
+++ b/pkg/core/utils/comments.go
@@ -1,13 +1,22 @@
package utils
-func GetTerraformOutputAsCollapsibleComment(summary string, collapsedComment string) string {
- str := `
- ` + summary + `
+func GetTerraformOutputAsCollapsibleComment(summary string) func(string) string {
- ` + "```terraform" + `
-` + collapsedComment + `
+ return func(comment string) string {
+ return `` + summary + `
+
+` + "```terraform" + `
+` + comment + `
` + "```" + `
`
+ }
+}
+
+func AsCollapsibleComment(summary string) func(string) string {
- return str
+ return func(comment string) string {
+ return `` + summary + `
+ ` + comment + `
+ `
+ }
}
diff --git a/pkg/digger/digger.go b/pkg/digger/digger.go
index 23adee0a4..f97ac849b 100644
--- a/pkg/digger/digger.go
+++ b/pkg/digger/digger.go
@@ -73,9 +73,7 @@ func RunCommandsPerProject(
policyChecker policy.Checker,
workingDir string,
) (bool, bool, error) {
- accumulatePlans := os.Getenv("ACCUMULATE_PLANS") == "true"
appliesPerProject := make(map[string]bool)
- plansToPublish := make([]string, 0)
organisation := strings.Split(projectNamespace, "/")[0]
@@ -93,7 +91,7 @@ func RunCommandsPerProject(
if !allowedToPerformCommand {
msg := fmt.Sprintf("User %s is not allowed to perform action: %s. Check your policies", requestedBy, command)
- err := ciService.PublishComment(prNumber, msg)
+ err := reporter.Report(msg, utils.AsCollapsibleComment("Policy violation"))
if err != nil {
log.Printf("Error publishing comment: %v", err)
}
@@ -103,6 +101,7 @@ func RunCommandsPerProject(
projectLock := &locking.PullRequestLock{
InternalLock: lock,
+ Reporter: reporter,
CIService: ciService,
ProjectName: projectCommands.ProjectName,
ProjectNamespace: projectNamespace,
@@ -153,14 +152,10 @@ func RunCommandsPerProject(
return false, false, fmt.Errorf("failed to run digger plan command. %v", err)
} else if planPerformed {
if plan != "" {
- comment := utils.GetTerraformOutputAsCollapsibleComment("Plan for **"+projectLock.LockId()+"**", plan)
- if accumulatePlans {
- plansToPublish = append(plansToPublish, comment)
- } else {
- err = reporter.Report(comment)
- if err != nil {
- log.Printf("Failed to report plan. %v", err)
- }
+ formatter := utils.GetTerraformOutputAsCollapsibleComment("Plan for " + projectLock.LockId() + "")
+ err = reporter.Report(plan, formatter)
+ if err != nil {
+ log.Printf("Failed to report plan. %v", err)
}
}
err := ciService.SetStatus(prNumber, "success", projectCommands.ProjectName+"/plan")
@@ -193,7 +188,7 @@ func RunCommandsPerProject(
if !isMergeable && !isMerged {
comment := "Cannot perform Apply since the PR is not currently mergeable."
fmt.Println(comment)
- err = ciService.PublishComment(prNumber, comment)
+ err = reporter.Report(comment, utils.AsCollapsibleComment("Apply error"))
if err != nil {
fmt.Printf("error publishing comment: %v\n", err)
}
@@ -237,13 +232,6 @@ func RunCommandsPerProject(
}
}
- if len(plansToPublish) > 0 {
- err := reporter.Report(strings.Join(plansToPublish, "\n"))
- if err != nil {
- log.Printf("Failed to report plans. %v", err)
- }
- }
-
allAppliesSuccess := true
for _, success := range appliesPerProject {
if !success {
diff --git a/pkg/digger/digger_test.go b/pkg/digger/digger_test.go
index b04f958ba..e34658336 100644
--- a/pkg/digger/digger_test.go
+++ b/pkg/digger/digger_test.go
@@ -1,6 +1,7 @@
package digger
import (
+ "digger/pkg/ci"
"digger/pkg/core/execution"
"digger/pkg/core/models"
"digger/pkg/reporting"
@@ -105,6 +106,16 @@ func (m *MockPRManager) IsClosed(prNumber int) (bool, error) {
return false, nil
}
+func (m *MockPRManager) GetComments(prNumber int) ([]ci.Comment, error) {
+ m.Commands = append(m.Commands, RunInfo{"GetComments", strconv.Itoa(prNumber), time.Now()})
+ return []ci.Comment{}, nil
+}
+
+func (m *MockPRManager) EditComment(id interface{}, comment string) error {
+ m.Commands = append(m.Commands, RunInfo{"EditComment", strconv.Itoa(id.(int)) + " " + comment, time.Now()})
+ return nil
+}
+
type MockProjectLock struct {
Commands []RunInfo
}
@@ -170,8 +181,9 @@ func TestCorrectCommandExecutionWhenApplying(t *testing.T) {
lock := &MockProjectLock{}
planStorage := &MockPlanStorage{}
reporter := &reporting.CiReporter{
- CiService: prManager,
- PrNumber: 1,
+ CiService: prManager,
+ PrNumber: 1,
+ ReportStrategy: &reporting.MultipleCommentsStrategy{},
}
executor := execution.DiggerExecutor{
ApplyStage: &models.Stage{
@@ -205,7 +217,7 @@ func TestCorrectCommandExecutionWhenApplying(t *testing.T) {
commandStrings := allCommandsInOrderWithParams(terraformExecutor, commandRunner, prManager, lock, planStorage)
- assert.Equal(t, []string{"RetrievePlan #.tfplan", "Lock ", "Init ", "Apply ", "LockId ", "PublishComment 1 \n Apply for ****
\n\n ```terraform\n\n ```\n ", "LockId ", "Run echo"}, commandStrings)
+ assert.Equal(t, []string{"RetrievePlan #.tfplan", "Lock ", "Init ", "Apply ", "LockId ", "PublishComment 1 Apply for
\n \n```terraform\n\n ```\n ", "LockId ", "Run echo"}, commandStrings)
}
func TestCorrectCommandExecutionWhenPlanning(t *testing.T) {
diff --git a/pkg/github/github.go b/pkg/github/github.go
index a72e39481..3f94874a3 100644
--- a/pkg/github/github.go
+++ b/pkg/github/github.go
@@ -68,6 +68,24 @@ func (svc *GithubService) PublishComment(prNumber int, comment string) error {
return err
}
+func (svc *GithubService) GetComments(prNumber int) ([]ci.Comment, error) {
+ comments, _, err := svc.Client.Issues.ListComments(context.Background(), svc.Owner, svc.RepoName, prNumber, &github.IssueListCommentsOptions{ListOptions: github.ListOptions{PerPage: 100}})
+ commentBodies := make([]ci.Comment, len(comments))
+ for i, comment := range comments {
+ commentBodies[i] = ci.Comment{
+ Id: *comment.ID,
+ Body: comment.Body,
+ }
+ }
+ return commentBodies, err
+}
+
+func (svc *GithubService) EditComment(id interface{}, comment string) error {
+ commentId := id.(int64)
+ _, _, err := svc.Client.Issues.EditComment(context.Background(), svc.Owner, svc.RepoName, commentId, &github.IssueComment{Body: &comment})
+ return err
+}
+
func (svc *GithubService) SetStatus(prNumber int, status string, statusContext string) error {
pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber)
if err != nil {
diff --git a/pkg/gitlab/gitlab.go b/pkg/gitlab/gitlab.go
index d92547def..4ceaa61ed 100644
--- a/pkg/gitlab/gitlab.go
+++ b/pkg/gitlab/gitlab.go
@@ -1,6 +1,7 @@
package gitlab
import (
+ "digger/pkg/ci"
"digger/pkg/configuration"
"digger/pkg/core/models"
"digger/pkg/utils"
@@ -228,6 +229,16 @@ func (gitlabService GitLabService) IsMerged(mergeRequestID int) (bool, error) {
return false, nil
}
+func (gitlabService GitLabService) EditComment(id interface{}, comment string) error {
+ //TODO implement me
+ return nil
+}
+
+func (gitlabService GitLabService) GetComments(prNumber int) ([]ci.Comment, error) {
+ //TODO implement me
+ return nil, nil
+}
+
func getMergeRequest(gitlabService GitLabService) *go_gitlab.MergeRequest {
projectId := *gitlabService.Context.ProjectId
mergeRequestIID := *gitlabService.Context.MergeRequestIId
diff --git a/pkg/integration/integration_test.go b/pkg/integration/integration_test.go
index 622ddd1cf..2f9ac5222 100644
--- a/pkg/integration/integration_test.go
+++ b/pkg/integration/integration_test.go
@@ -44,9 +44,15 @@ func getProjectLockForTests() (error, *locking.PullRequestLock) {
repositoryName := "test_dynamodb_lock"
ghToken := "token"
githubPrService := dg_github.NewGitHubService(ghToken, repositoryName, repoOwner)
+ reporter := reporting.CiReporter{
+ CiService: githubPrService,
+ PrNumber: 1,
+ ReportStrategy: &reporting.MultipleCommentsStrategy{},
+ }
projectLock := &locking.PullRequestLock{
InternalLock: &dynamoDbLock,
+ Reporter: &reporter,
CIService: githubPrService,
ProjectName: "test_dynamodb_lock",
ProjectNamespace: repoOwner + "/" + repositoryName,
@@ -413,6 +419,7 @@ func TestHappyPath(t *testing.T) {
projectLock := &locking.PullRequestLock{
InternalLock: lock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "dev",
ProjectNamespace: diggerProjectNamespace,
@@ -453,6 +460,7 @@ func TestHappyPath(t *testing.T) {
projectLock = &locking.PullRequestLock{
InternalLock: lock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "dev",
ProjectNamespace: diggerProjectNamespace,
@@ -476,6 +484,7 @@ func TestHappyPath(t *testing.T) {
projectLock = &locking.PullRequestLock{
InternalLock: lock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "dev",
ProjectNamespace: diggerProjectNamespace,
@@ -568,6 +577,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
projectLock := &locking.PullRequestLock{
InternalLock: &dynamoDbLock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "digger_demo",
ProjectNamespace: diggerProjectNamespace,
@@ -607,6 +617,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
projectLock = &locking.PullRequestLock{
InternalLock: &dynamoDbLock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "digger_demo",
ProjectNamespace: diggerProjectNamespace,
@@ -630,6 +641,7 @@ func TestMultiEnvHappyPath(t *testing.T) {
projectLock = &locking.PullRequestLock{
InternalLock: &dynamoDbLock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "digger_demo",
ProjectNamespace: diggerProjectNamespace,
@@ -827,6 +839,7 @@ workflows:
projectLock = &locking.PullRequestLock{
InternalLock: lock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "dev",
ProjectNamespace: diggerProjectNamespace,
@@ -849,6 +862,7 @@ workflows:
projectLock = &locking.PullRequestLock{
InternalLock: lock,
+ Reporter: reporter,
CIService: githubPrService,
ProjectName: "dev",
ProjectNamespace: diggerProjectNamespace,
diff --git a/pkg/locking/locking.go b/pkg/locking/locking.go
index dc29a12fb..db66b30a1 100644
--- a/pkg/locking/locking.go
+++ b/pkg/locking/locking.go
@@ -6,6 +6,8 @@ import (
"digger/pkg/azure"
"digger/pkg/ci"
"digger/pkg/core/locking"
+ "digger/pkg/core/reporting"
+ "digger/pkg/core/utils"
"digger/pkg/gcp"
"errors"
"fmt"
@@ -26,6 +28,7 @@ import (
type PullRequestLock struct {
InternalLock locking.Lock
CIService ci.CIService
+ Reporter reporting.Reporter
ProjectName string
ProjectNamespace string
PrNumber int
@@ -71,7 +74,8 @@ func (projectLock *PullRequestLock) Lock() (bool, error) {
} else {
transactionIdStr := strconv.Itoa(*existingLockTransactionId)
comment := "Project " + projectLock.projectId() + " locked by another PR #" + transactionIdStr + " (failed to acquire lock " + projectLock.ProjectNamespace + "). The locking plan must be applied or discarded before future plans can execute"
- projectLock.CIService.PublishComment(projectLock.PrNumber, comment)
+
+ err = projectLock.Reporter.Report(comment, utils.AsCollapsibleComment("Locking failed"))
return false, nil
}
}
@@ -84,7 +88,10 @@ func (projectLock *PullRequestLock) Lock() (bool, error) {
if lockAcquired && !isNoOpLock {
comment := "Project " + projectLock.projectId() + " has been locked by PR #" + strconv.Itoa(projectLock.PrNumber)
- projectLock.CIService.PublishComment(projectLock.PrNumber, comment)
+ err = projectLock.Reporter.Report(comment, utils.AsCollapsibleComment("Locking successful"))
+ if err != nil {
+ println("failed to publish comment: " + err.Error())
+ }
println("project " + projectLock.projectId() + " locked successfully. PR # " + strconv.Itoa(projectLock.PrNumber))
}
@@ -114,7 +121,7 @@ func (projectLock *PullRequestLock) verifyNoHangingLocks() (bool, error) {
}
transactionIdStr := strconv.Itoa(*transactionId)
comment := "Project " + projectLock.projectId() + " locked by another PR #" + transactionIdStr + "(failed to acquire lock " + projectLock.ProjectName + "). The locking plan must be applied or discarded before future plans can execute"
- projectLock.CIService.PublishComment(projectLock.PrNumber, comment)
+ err = projectLock.Reporter.Report(comment, utils.AsCollapsibleComment("Locking failed"))
return false, nil
}
return true, nil
@@ -139,7 +146,8 @@ func (projectLock *PullRequestLock) Unlock() (bool, error) {
}
if lockReleased {
comment := "Project unlocked (" + projectLock.projectId() + ")."
- projectLock.CIService.PublishComment(projectLock.PrNumber, comment)
+ projectLock.Reporter.Report(comment, utils.AsCollapsibleComment("Unlocking successful"))
+
println("Project unlocked")
return true, nil
}
@@ -163,7 +171,7 @@ func (projectLock *PullRequestLock) ForceUnlock() error {
if lockReleased {
comment := "Project unlocked (" + projectLock.projectId() + ")."
- projectLock.CIService.PublishComment(projectLock.PrNumber, comment)
+ projectLock.Reporter.Report(comment, utils.AsCollapsibleComment("Unlocking successful"))
println("Project unlocked")
}
return nil
diff --git a/pkg/locking/locking_test.go b/pkg/locking/locking_test.go
index 11c040ddc..5f575a61f 100644
--- a/pkg/locking/locking_test.go
+++ b/pkg/locking/locking_test.go
@@ -9,9 +9,11 @@ import (
func TestLockingTwiceThrowsError(t *testing.T) {
mockDynamoDB := utils.MockLock{make(map[string]int)}
mockPrManager := utils.MockGithubPullrequestManager{}
+ reporter := utils.MockReporter{}
pl := PullRequestLock{
InternalLock: &mockDynamoDB,
CIService: &mockPrManager,
+ Reporter: &reporter,
ProjectName: "a",
ProjectNamespace: "",
PrNumber: 1,
@@ -23,6 +25,7 @@ func TestLockingTwiceThrowsError(t *testing.T) {
pl2 := PullRequestLock{
InternalLock: &mockDynamoDB,
CIService: &mockPrManager,
+ Reporter: &reporter,
ProjectName: "a",
ProjectNamespace: "",
PrNumber: 2,
diff --git a/pkg/reporting/reporting.go b/pkg/reporting/reporting.go
index 4788ab015..3e11fa2c9 100644
--- a/pkg/reporting/reporting.go
+++ b/pkg/reporting/reporting.go
@@ -1,12 +1,95 @@
package reporting
-import "digger/pkg/ci"
+import (
+ "digger/pkg/ci"
+ "digger/pkg/core/utils"
+ "fmt"
+ "strings"
+ "time"
+)
type CiReporter struct {
- CiService ci.CIService
- PrNumber int
+ CiService ci.CIService
+ PrNumber int
+ ReportStrategy ReportStrategy
}
-func (ciReporter *CiReporter) Report(report string) error {
- return ciReporter.CiService.PublishComment(ciReporter.PrNumber, report)
+type ReportStrategy interface {
+ Report(ciService ci.CIService, PrNumber int, report string, reportFormatter func(report string) string) error
+}
+
+type CommentPerRunStrategy struct {
+ TimeOfRun time.Time
+}
+
+func (strategy *CommentPerRunStrategy) Report(ciService ci.CIService, PrNumber int, report string, reportFormatter func(report string) string) error {
+ comments, err := ciService.GetComments(PrNumber)
+ if err != nil {
+ return fmt.Errorf("error getting comments: %v", err)
+ }
+
+ reportTitle := "Digger run report at " + strategy.TimeOfRun.Format("2006-01-02 15:04:05")
+ return upsertComment(ciService, PrNumber, report, reportFormatter, comments, reportTitle, err)
+}
+
+func upsertComment(ciService ci.CIService, PrNumber int, report string, reportFormatter func(report string) string, comments []ci.Comment, reportTitle string, err error) error {
+ report = reportFormatter(report)
+ var commentIdForThisRun interface{}
+ var commentBody string
+ for _, comment := range comments {
+ if strings.Contains(*comment.Body, reportTitle) {
+ commentIdForThisRun = comment.Id
+ commentBody = *comment.Body
+ break
+ }
+ }
+
+ if commentIdForThisRun == nil {
+ collapsibleComment := utils.AsCollapsibleComment(reportTitle)(report)
+ err := ciService.PublishComment(PrNumber, collapsibleComment)
+ if err != nil {
+ return fmt.Errorf("error publishing comment: %v", err)
+ }
+ return nil
+ }
+
+ // strip first and last lines
+ lines := strings.Split(commentBody, "\n")
+ lines = lines[1 : len(lines)-1]
+ commentBody = strings.Join(lines, "\n")
+
+ commentBody = commentBody + "\n\n" + report + "\n"
+
+ completeComment := utils.AsCollapsibleComment(reportTitle)(commentBody)
+
+ err = ciService.EditComment(commentIdForThisRun, completeComment)
+
+ if err != nil {
+ return fmt.Errorf("error editing comment: %v", err)
+ }
+ return nil
+}
+
+type LatestRunCommentStrategy struct {
+ TimeOfRun time.Time
+}
+
+func (strategy *LatestRunCommentStrategy) Report(ciService ci.CIService, prNumber int, comment string, commentFormatting func(comment string) string) error {
+ comments, err := ciService.GetComments(prNumber)
+ if err != nil {
+ return fmt.Errorf("error getting comments: %v", err)
+ }
+
+ reportTitle := "Digger latest run report"
+ return upsertComment(ciService, prNumber, comment, commentFormatting, comments, reportTitle, err)
+}
+
+type MultipleCommentsStrategy struct{}
+
+func (strategy *MultipleCommentsStrategy) Report(ciService ci.CIService, PrNumber int, report string, formatter func(string) string) error {
+ return ciService.PublishComment(PrNumber, formatter(report))
+}
+
+func (ciReporter *CiReporter) Report(report string, reportFormatter func(report string) string) error {
+ return ciReporter.ReportStrategy.Report(ciReporter.CiService, ciReporter.PrNumber, report, reportFormatter)
}
diff --git a/pkg/reporting/reporting_test.go b/pkg/reporting/reporting_test.go
new file mode 100644
index 000000000..e815fe55b
--- /dev/null
+++ b/pkg/reporting/reporting_test.go
@@ -0,0 +1,229 @@
+package reporting
+
+import (
+ "digger/pkg/ci"
+ "digger/pkg/core/utils"
+ "github.com/stretchr/testify/assert"
+ "testing"
+ "time"
+)
+
+func TestCommentPerRunStrategyReport(t *testing.T) {
+ timeOfRun := time.Now()
+ strategy := CommentPerRunStrategy{
+ TimeOfRun: timeOfRun,
+ }
+ existingCommentForOtherRun := utils.AsCollapsibleComment("Digger run report at some other time")("")
+
+ prNumber := 1
+ ciService := &MockCiService{
+ CommentsPerPr: map[int][]*ci.Comment{
+ prNumber: {
+ {
+ Id: 1,
+ Body: &existingCommentForOtherRun,
+ },
+ },
+ },
+ }
+
+ report := "resource \"null_resource\" \"test\" {}"
+ reportFormatter := utils.GetTerraformOutputAsCollapsibleComment("run1")
+ err := strategy.Report(ciService, prNumber, report, reportFormatter)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report2 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter2 := utils.GetTerraformOutputAsCollapsibleComment("run2")
+ err = strategy.Report(ciService, prNumber, report2, reportFormatter2)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report3 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter3 := utils.GetTerraformOutputAsCollapsibleComment("run3")
+ err = strategy.Report(ciService, prNumber, report3, reportFormatter3)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report4 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter4 := utils.GetTerraformOutputAsCollapsibleComment("run4")
+ err = strategy.Report(ciService, prNumber, report4, reportFormatter4)
+
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+
+ assert.Equal(t, 2, len(ciService.CommentsPerPr[prNumber]))
+ assert.Equal(t, "Digger run report at "+timeOfRun.Format("2006-01-02 15:04:05")+"
\n run1
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\nrun2
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\n\nrun3
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\n\nrun4
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\n ", *ciService.CommentsPerPr[prNumber][1].Body)
+}
+
+func TestLatestCommentStrategyReport(t *testing.T) {
+ timeOfRun := time.Now()
+ strategy := LatestRunCommentStrategy{
+ TimeOfRun: timeOfRun,
+ }
+ existingCommentForOtherRun := utils.AsCollapsibleComment("Digger run report at some other time")("")
+
+ prNumber := 1
+ ciService := &MockCiService{
+ CommentsPerPr: map[int][]*ci.Comment{
+ prNumber: {
+ {
+ Id: 1,
+ Body: &existingCommentForOtherRun,
+ },
+ },
+ },
+ }
+
+ report := "resource \"null_resource\" \"test\" {}"
+ reportFormatter := utils.GetTerraformOutputAsCollapsibleComment("run1")
+ err := strategy.Report(ciService, prNumber, report, reportFormatter)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report2 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter2 := utils.GetTerraformOutputAsCollapsibleComment("run2")
+ err = strategy.Report(ciService, prNumber, report2, reportFormatter2)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report3 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter3 := utils.GetTerraformOutputAsCollapsibleComment("run3")
+ err = strategy.Report(ciService, prNumber, report3, reportFormatter3)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report4 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter4 := utils.GetTerraformOutputAsCollapsibleComment("run4")
+ err = strategy.Report(ciService, prNumber, report4, reportFormatter4)
+
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+
+ assert.Equal(t, 2, len(ciService.CommentsPerPr[prNumber]))
+ assert.Equal(t, "Digger latest run report
\n run1
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\nrun2
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\n\nrun3
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\n\nrun4
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n \n\n ", *ciService.CommentsPerPr[prNumber][1].Body)
+}
+
+func TestMultipleCommentStrategyReport(t *testing.T) {
+ strategy := MultipleCommentsStrategy{}
+ existingCommentForOtherRun := utils.AsCollapsibleComment("Digger run report at some other time")("")
+
+ prNumber := 1
+ ciService := &MockCiService{
+ CommentsPerPr: map[int][]*ci.Comment{
+ prNumber: {
+ {
+ Id: 1,
+ Body: &existingCommentForOtherRun,
+ },
+ },
+ },
+ }
+
+ report := "resource \"null_resource\" \"test\" {}"
+ reportFormatter := utils.GetTerraformOutputAsCollapsibleComment("run1")
+ err := strategy.Report(ciService, prNumber, report, reportFormatter)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report2 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter2 := utils.GetTerraformOutputAsCollapsibleComment("run2")
+ err = strategy.Report(ciService, prNumber, report2, reportFormatter2)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report3 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter3 := utils.GetTerraformOutputAsCollapsibleComment("run3")
+ err = strategy.Report(ciService, prNumber, report3, reportFormatter3)
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+ report4 := "resource \"null_resource\" \"test\" {}"
+ reportFormatter4 := utils.GetTerraformOutputAsCollapsibleComment("run4")
+ err = strategy.Report(ciService, prNumber, report4, reportFormatter4)
+
+ if err != nil {
+ t.Errorf("Error: %v", err)
+ }
+
+ assert.Equal(t, 5, len(ciService.CommentsPerPr[prNumber]))
+ assert.Equal(t, "run4
\n \n```terraform\nresource \"null_resource\" \"test\" {}\n ```\n ", *ciService.CommentsPerPr[prNumber][4].Body)
+}
+
+type MockCiService struct {
+ CommentsPerPr map[int][]*ci.Comment
+}
+
+func (t MockCiService) GetUserTeams(organisation string, user string) ([]string, error) {
+ return nil, nil
+}
+
+func (t MockCiService) GetChangedFiles(prNumber int) ([]string, error) {
+ return nil, nil
+}
+func (t MockCiService) PublishComment(prNumber int, comment string) error {
+
+ latestId := 0
+
+ for _, comments := range t.CommentsPerPr {
+ for _, c := range comments {
+ if c.Id.(int) > latestId {
+ latestId = c.Id.(int)
+ }
+ }
+ }
+
+ t.CommentsPerPr[prNumber] = append(t.CommentsPerPr[prNumber], &ci.Comment{Id: latestId + 1, Body: &comment})
+
+ return nil
+}
+
+func (t MockCiService) SetStatus(prNumber int, status string, statusContext string) error {
+ return nil
+}
+
+func (t MockCiService) GetCombinedPullRequestStatus(prNumber int) (string, error) {
+ return "", nil
+}
+
+func (t MockCiService) MergePullRequest(prNumber int) error {
+ return nil
+}
+
+func (t MockCiService) IsMergeable(prNumber int) (bool, error) {
+ return true, nil
+}
+
+func (t MockCiService) IsMerged(prNumber int) (bool, error) {
+ return false, nil
+}
+
+func (t MockCiService) DownloadLatestPlans(prNumber int) (string, error) {
+ return "", nil
+}
+
+func (t MockCiService) IsClosed(prNumber int) (bool, error) {
+ return false, nil
+}
+
+func (t MockCiService) GetComments(prNumber int) ([]ci.Comment, error) {
+ comments := []ci.Comment{}
+ for _, c := range t.CommentsPerPr[prNumber] {
+ comments = append(comments, *c)
+ }
+ return comments, nil
+}
+
+func (t MockCiService) EditComment(commentId interface{}, comment string) error {
+ for _, comments := range t.CommentsPerPr {
+ for _, c := range comments {
+ if c.Id == commentId {
+ c.Body = &comment
+ return nil
+ }
+ }
+ }
+ return nil
+}
diff --git a/pkg/utils/mocks.go b/pkg/utils/mocks.go
index 50366fd70..bc05bd222 100644
--- a/pkg/utils/mocks.go
+++ b/pkg/utils/mocks.go
@@ -1,5 +1,7 @@
package utils
+import "digger/pkg/ci"
+
type MockTerraform struct {
commands []string
}
@@ -90,6 +92,14 @@ func (t MockPullRequestManager) IsClosed(prNumber int) (bool, error) {
return false, nil
}
+func (t MockPullRequestManager) GetComments(prNumber int) ([]ci.Comment, error) {
+ return []ci.Comment{}, nil
+}
+
+func (t MockPullRequestManager) EditComment(commentId interface{}, comment string) error {
+ return nil
+}
+
type MockPlanStorage struct {
}
diff --git a/pkg/utils/pullrequestmanager_mock.go b/pkg/utils/pullrequestmanager_mock.go
index 895c9e3c3..66e09cfb6 100644
--- a/pkg/utils/pullrequestmanager_mock.go
+++ b/pkg/utils/pullrequestmanager_mock.go
@@ -1,5 +1,16 @@
package utils
+import "digger/pkg/ci"
+
+type MockReporter struct {
+ commands []string
+}
+
+func (mockReporter *MockReporter) Report(report string, formatter func(string) string) error {
+ mockReporter.commands = append(mockReporter.commands, "Report")
+ return nil
+}
+
type MockGithubPullrequestManager struct {
commands []string
}
@@ -52,3 +63,13 @@ func (mockGithubPullrequestManager *MockGithubPullrequestManager) IsMerged(prNum
mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "IsClosed")
return false, nil
}
+
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) GetComments(prNumber int) ([]ci.Comment, error) {
+ mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "GetComments")
+ return []ci.Comment{}, nil
+}
+
+func (mockGithubPullrequestManager *MockGithubPullrequestManager) EditComment(commentId interface{}, comment string) error {
+ mockGithubPullrequestManager.commands = append(mockGithubPullrequestManager.commands, "EditComment")
+ return nil
+}