Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Spartakovic I think that the default behaviour should be comments_per_run since multiple_comments is too noisy for all people

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't mind that, only left it so people aren't surprised by the new behaviour

This comment was marked as outdated.

required: false
default: 'comments_per_run'
outputs:
output:
value: ${{ steps.digger.outputs.output }}
Expand Down Expand Up @@ -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
Expand All @@ -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: |
Expand Down
43 changes: 31 additions & 12 deletions cmd/digger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 {
Expand All @@ -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")
Expand Down Expand Up @@ -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)

Expand All @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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.")
Expand Down
38 changes: 38 additions & 0 deletions pkg/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions pkg/ci/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
7 changes: 4 additions & 3 deletions pkg/core/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <b>" + d.ProjectLock.LockId() + "</b>")

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)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/core/reporting/reporting.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package reporting

type Reporter interface {
Report(report string) error
Report(report string, reportFormatting func(report string) string) error
}
21 changes: 15 additions & 6 deletions pkg/core/utils/comments.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package utils

func GetTerraformOutputAsCollapsibleComment(summary string, collapsedComment string) string {
str := `<details>
<summary>` + summary + `</summary>
func GetTerraformOutputAsCollapsibleComment(summary string) func(string) string {

` + "```terraform" + `
` + collapsedComment + `
return func(comment string) string {
return `<details><summary>` + summary + `</summary>

` + "```terraform" + `
` + comment + `
` + "```" + `
</details>`
}
}

func AsCollapsibleComment(summary string) func(string) string {

return str
return func(comment string) string {
return `<details><summary>` + summary + `</summary>
` + comment + `
</details>`
}
}
26 changes: 7 additions & 19 deletions pkg/digger/digger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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)
}
Expand All @@ -103,6 +101,7 @@ func RunCommandsPerProject(

projectLock := &locking.PullRequestLock{
InternalLock: lock,
Reporter: reporter,
CIService: ciService,
ProjectName: projectCommands.ProjectName,
ProjectNamespace: projectNamespace,
Expand Down Expand Up @@ -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 <b>" + projectLock.LockId() + "</b>")
err = reporter.Report(plan, formatter)
if err != nil {
log.Printf("Failed to report plan. %v", err)
}
}
err := ciService.SetStatus(prNumber, "success", projectCommands.ProjectName+"/plan")
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 15 additions & 3 deletions pkg/digger/digger_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package digger

import (
"digger/pkg/ci"
"digger/pkg/core/execution"
"digger/pkg/core/models"
"digger/pkg/reporting"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 <details>\n <summary>Apply for ****</summary>\n\n ```terraform\n\n ```\n</details>", "LockId ", "Run echo"}, commandStrings)
assert.Equal(t, []string{"RetrievePlan #.tfplan", "Lock ", "Init ", "Apply ", "LockId ", "PublishComment 1 <details><summary>Apply for <b></b></summary>\n \n```terraform\n\n ```\n</details>", "LockId ", "Run echo"}, commandStrings)
}

func TestCorrectCommandExecutionWhenPlanning(t *testing.T) {
Expand Down
18 changes: 18 additions & 0 deletions pkg/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading