diff --git a/cmd/server.go b/cmd/server.go index 5cf1cc096b..4c974ce2bf 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -71,6 +71,7 @@ const ( EnableRegExpCmdFlag = "enable-regexp-cmd" EnableDiffMarkdownFormat = "enable-diff-markdown-format" ExecutableName = "executable-name" + HideUnchangedPlanComments = "hide-unchanged-plan-comments" GHHostnameFlag = "gh-hostname" GHTeamAllowlistFlag = "gh-team-allowlist" GHTokenFlag = "gh-token" @@ -531,6 +532,10 @@ var boolFlags = map[string]boolFlag{ description: "Enable websocket origin check", defaultValue: false, }, + HideUnchangedPlanComments: { + description: "Remove no-changes plan comments from the pull request.", + defaultValue: false, + }, } var intFlags = map[string]intFlag{ CheckoutDepthFlag: { diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index b060a50473..6375cdfeb7 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -447,6 +447,16 @@ and set `--autoplan-modules` to `false`. This is useful when running multiple Atlantis servers against a single repository. +### `--hide-unchanged-plan-comments` + ```bash + atlantis server --hide-unchanged-plan-comments + # or + ATLANTIS_HIDE_UNCHANGED_PLAN_COMMENTS=true + ``` +Remove no-changes plan comments from the pull request. + +This is useful when you have many projects and want to keep the pull request clean from useless comments. + ### `--gh-hostname` ```bash atlantis server --gh-hostname="my.github.enterprise.com" diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 151cf24347..7fd5527bea 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1241,7 +1241,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers pullUpdater := &events.PullUpdater{ HidePrevPlanComments: false, VCSClient: e2eVCSClient, - MarkdownRenderer: events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis"), + MarkdownRenderer: events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false), } autoMerger := &events.AutoMerger{ diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 0e33276832..227dca01cc 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -132,7 +132,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock pullUpdater = &events.PullUpdater{ HidePrevPlanComments: false, VCSClient: vcsClient, - MarkdownRenderer: events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis"), + MarkdownRenderer: events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false), } autoMerger = &events.AutoMerger{ diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go index 5d8b6441dc..09542297a6 100644 --- a/server/events/markdown_renderer.go +++ b/server/events/markdown_renderer.go @@ -48,28 +48,30 @@ type MarkdownRenderer struct { // gitlabSupportsCommonMark is true if the version of GitLab we're // using supports the CommonMark markdown format. // If we're not configured with a GitLab client, this will be false. - gitlabSupportsCommonMark bool - disableApplyAll bool - disableApply bool - disableMarkdownFolding bool - disableRepoLocking bool - enableDiffMarkdownFormat bool - markdownTemplates *template.Template - executableName string + gitlabSupportsCommonMark bool + disableApplyAll bool + disableApply bool + disableMarkdownFolding bool + disableRepoLocking bool + enableDiffMarkdownFormat bool + markdownTemplates *template.Template + executableName string + hideUnchangedPlanComments bool } // commonData is data that all responses have. type commonData struct { - Command string - SubCommand string - Verbose bool - Log string - PlansDeleted bool - DisableApplyAll bool - DisableApply bool - DisableRepoLocking bool - EnableDiffMarkdownFormat bool - ExecutableName string + Command string + SubCommand string + Verbose bool + Log string + PlansDeleted bool + DisableApplyAll bool + DisableApply bool + DisableRepoLocking bool + EnableDiffMarkdownFormat bool + ExecutableName string + HideUnchangedPlanComments bool } // errData is data about an error response. @@ -109,6 +111,7 @@ type projectResultTmplData struct { RepoRelDir string ProjectName string Rendered string + NoChanges bool } // Initialize templates @@ -121,6 +124,7 @@ func NewMarkdownRenderer( enableDiffMarkdownFormat bool, markdownTemplateOverridesDir string, executableName string, + hideUnchangedPlanComments bool, ) *MarkdownRenderer { var templates *template.Template templates, _ = template.New("").Funcs(sprig.TxtFuncMap()).ParseFS(templatesFS, "templates/*.tmpl") @@ -129,14 +133,15 @@ func NewMarkdownRenderer( templates = overrides } return &MarkdownRenderer{ - gitlabSupportsCommonMark: gitlabSupportsCommonMark, - disableApplyAll: disableApplyAll, - disableMarkdownFolding: disableMarkdownFolding, - disableApply: disableApply, - disableRepoLocking: disableRepoLocking, - enableDiffMarkdownFormat: enableDiffMarkdownFormat, - markdownTemplates: templates, - executableName: executableName, + gitlabSupportsCommonMark: gitlabSupportsCommonMark, + disableApplyAll: disableApplyAll, + disableMarkdownFolding: disableMarkdownFolding, + disableApply: disableApply, + disableRepoLocking: disableRepoLocking, + enableDiffMarkdownFormat: enableDiffMarkdownFormat, + markdownTemplates: templates, + executableName: executableName, + hideUnchangedPlanComments: hideUnchangedPlanComments, } } @@ -145,16 +150,17 @@ func NewMarkdownRenderer( func (m *MarkdownRenderer) Render(res command.Result, cmdName command.Name, subCmd, log string, verbose bool, vcsHost models.VCSHostType) string { commandStr := cases.Title(language.English).String(strings.Replace(cmdName.String(), "_", " ", -1)) common := commonData{ - Command: commandStr, - SubCommand: subCmd, - Verbose: verbose, - Log: log, - PlansDeleted: res.PlansDeleted, - DisableApplyAll: m.disableApplyAll || m.disableApply, - DisableApply: m.disableApply, - DisableRepoLocking: m.disableRepoLocking, - EnableDiffMarkdownFormat: m.enableDiffMarkdownFormat, - ExecutableName: m.executableName, + Command: commandStr, + SubCommand: subCmd, + Verbose: verbose, + Log: log, + PlansDeleted: res.PlansDeleted, + DisableApplyAll: m.disableApplyAll || m.disableApply, + DisableApply: m.disableApply, + DisableRepoLocking: m.disableRepoLocking, + EnableDiffMarkdownFormat: m.enableDiffMarkdownFormat, + ExecutableName: m.executableName, + HideUnchangedPlanComments: m.hideUnchangedPlanComments, } templates := m.markdownTemplates @@ -197,6 +203,7 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult, } else { resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("planSuccessUnwrapped"), planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply, DisableRepoLocking: common.DisableRepoLocking, EnableDiffMarkdownFormat: common.EnableDiffMarkdownFormat}) } + resultData.NoChanges = result.PlanSuccess.NoChanges() numPlanSuccesses++ } else if result.PolicyCheckSuccess != nil { result.PolicyCheckSuccess.PolicyCheckOutput = strings.TrimSpace(result.PolicyCheckSuccess.PolicyCheckOutput) diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index c6fad8d4eb..9c0548ff91 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -57,7 +57,7 @@ func TestRenderErr(t *testing.T) { }, } - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis") + r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) for _, c := range cases { res := command.Result{ Error: c.Error, @@ -102,7 +102,7 @@ func TestRenderFailure(t *testing.T) { }, } - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis") + r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) for _, c := range cases { res := command.Result{ Failure: c.Failure, @@ -121,7 +121,7 @@ func TestRenderFailure(t *testing.T) { } func TestRenderErrAndFailure(t *testing.T) { - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis") + r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) res := command.Result{ Error: errors.New("error"), Failure: "failure", @@ -824,7 +824,7 @@ $$$ }, } - r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis") + r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", false) for _, c := range cases { t.Run(c.Description, func(t *testing.T) { res := command.Result{ @@ -979,6 +979,7 @@ $$$ false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) for _, c := range cases { t.Run(c.Description, func(t *testing.T) { @@ -1127,6 +1128,7 @@ $$$ false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) for _, c := range cases { t.Run(c.Description, func(t *testing.T) { @@ -1165,6 +1167,7 @@ func TestRenderCustomPolicyCheckTemplate_DisableApplyAll(t *testing.T) { false, // enableDiffMarkdownFormat tmpDir, // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) rendered := r.Render(command.Result{ @@ -1196,6 +1199,7 @@ func TestRenderProjectResults_DisableFolding(t *testing.T) { false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) rendered := mr.Render(command.Result{ @@ -1287,6 +1291,7 @@ func TestRenderProjectResults_WrappedErr(t *testing.T) { false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) rendered := mr.Render(command.Result{ @@ -1402,6 +1407,7 @@ func TestRenderProjectResults_WrapSingleProject(t *testing.T) { false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) var pr command.ProjectResult switch cmd { @@ -1509,6 +1515,7 @@ func TestRenderProjectResults_MultiProjectApplyWrapped(t *testing.T) { false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) tfOut := strings.Repeat("line\n", 13) rendered := mr.Render(command.Result{ @@ -1564,6 +1571,7 @@ func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) { false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) tfOut := strings.Repeat("line\n", 13) + "Plan: 1 to add, 0 to change, 0 to destroy." rendered := mr.Render(command.Result{ @@ -1739,6 +1747,7 @@ This plan was not saved because one or more projects failed and automerge requir false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) rendered := mr.Render(c.cr, command.Plan, "", "log", false, models.Github) expWithBackticks := strings.Replace(c.exp, "$", "`", -1) @@ -2197,6 +2206,7 @@ $$$ false, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) for _, c := range cases { t.Run(c.Description, func(t *testing.T) { @@ -2633,6 +2643,7 @@ func TestRenderProjectResultsWithEnableDiffMarkdownFormat(t *testing.T) { true, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) for _, c := range cases { @@ -2669,6 +2680,7 @@ func BenchmarkRenderProjectResultsWithEnableDiffMarkdownFormat(b *testing.B) { true, // enableDiffMarkdownFormat "", // MarkdownTemplateOverridesDir "atlantis", // executableName + false, // hideUnchangedPlanComments ) for _, c := range cases { @@ -2688,3 +2700,161 @@ func BenchmarkRenderProjectResultsWithEnableDiffMarkdownFormat(b *testing.B) { }) } } + +func TestRenderProjectResultsHideUnchangedPlans(t *testing.T) { + cases := []struct { + Description string + Command command.Name + SubCommand string + ProjectResults []command.ProjectResult + VCSHost models.VCSHostType + Expected string + }{ + { + "multiple successful plans, hide unchanged plans", + command.Plan, + "", + []command.ProjectResult{ + { + Workspace: "workspace", + RepoRelDir: "path", + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "terraform-output", + LockURL: "lock-url", + ApplyCmd: "atlantis apply -d path -w workspace", + RePlanCmd: "atlantis plan -d path -w workspace", + }, + }, + { + Workspace: "workspace", + RepoRelDir: "path2", + ProjectName: "projectname", + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "No changes. Infrastructure is up-to-date.", + LockURL: "lock-url2", + ApplyCmd: "atlantis apply -d path2 -w workspace", + RePlanCmd: "atlantis plan -d path2 -w workspace", + }, + }, + { + Workspace: "workspace", + RepoRelDir: "path3", + ProjectName: "projectname2", + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "terraform-output3", + LockURL: "lock-url3", + ApplyCmd: "atlantis apply -d path3 -w workspace", + RePlanCmd: "atlantis plan -d path3 -w workspace", + }, + }, + }, + models.Github, + `Ran Plan for 3 projects: + +1. dir: $path$ workspace: $workspace$ +1. project: $projectname$ dir: $path2$ workspace: $workspace$ +1. project: $projectname2$ dir: $path3$ workspace: $workspace$ + +### 1. dir: $path$ workspace: $workspace$ +$$$diff +terraform-output +$$$ + +* :arrow_forward: To **apply** this plan, comment: + * $atlantis apply -d path -w workspace$ +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :repeat: To **plan** this project again, comment: + * $atlantis plan -d path -w workspace$ + +--- +### 3. project: $projectname2$ dir: $path3$ workspace: $workspace$ +$$$diff +terraform-output3 +$$$ + +* :arrow_forward: To **apply** this plan, comment: + * $atlantis apply -d path3 -w workspace$ +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url3) +* :repeat: To **plan** this project again, comment: + * $atlantis plan -d path3 -w workspace$ + +--- +* :fast_forward: To **apply** all unapplied plans from this pull request, comment: + * $atlantis apply$ +* :put_litter_in_its_place: To delete all plans and locks for the PR, comment: + * $atlantis unlock$ +`, + }, + { + "multiple successful plans, hide unchanged plans, all plans are unchanged", + command.Plan, + "", + []command.ProjectResult{ + { + Workspace: "workspace", + RepoRelDir: "path", + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "No changes. Infrastructure is up-to-date.", + LockURL: "lock-url", + ApplyCmd: "atlantis apply -d path -w workspace", + RePlanCmd: "atlantis plan -d path -w workspace", + }, + }, + { + Workspace: "workspace", + RepoRelDir: "path2", + ProjectName: "projectname", + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "No changes. Infrastructure is up-to-date.", + LockURL: "lock-url2", + ApplyCmd: "atlantis apply -d path2 -w workspace", + RePlanCmd: "atlantis plan -d path2 -w workspace", + }, + }, + { + Workspace: "workspace", + RepoRelDir: "path3", + ProjectName: "projectname2", + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "No changes. Infrastructure is up-to-date.", + LockURL: "lock-url3", + ApplyCmd: "atlantis apply -d path3 -w workspace", + RePlanCmd: "atlantis plan -d path3 -w workspace", + }, + }, + }, + models.Github, + `Ran Plan for 3 projects: + +1. dir: $path$ workspace: $workspace$ +1. project: $projectname$ dir: $path2$ workspace: $workspace$ +1. project: $projectname2$ dir: $path3$ workspace: $workspace$ + +* :fast_forward: To **apply** all unapplied plans from this pull request, comment: + * $atlantis apply$ +* :put_litter_in_its_place: To delete all plans and locks for the PR, comment: + * $atlantis unlock$ +`, + }, + } + + r := events.NewMarkdownRenderer(false, false, false, false, false, false, "", "atlantis", true) + for _, c := range cases { + t.Run(c.Description, func(t *testing.T) { + res := command.Result{ + ProjectResults: c.ProjectResults, + } + for _, verbose := range []bool{true, false} { + t.Run(c.Description, func(t *testing.T) { + s := r.Render(res, c.Command, c.SubCommand, "log", verbose, c.VCSHost) + expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) + if !verbose { + Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s)) + } else { + Equals(t, expWithBackticks+"\n
Log\n

\n\n```\nlog```\n

", s) + } + }) + } + }) + } +} diff --git a/server/events/models/models.go b/server/events/models/models.go index 6c45795be8..66ecb47d0b 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -390,6 +390,11 @@ func (p *PlanSuccess) DiffSummary() string { return reNoChanges.FindString(p.TerraformOutput) } +// NoChanges returns true if the plan has no changes. +func (p *PlanSuccess) NoChanges() bool { + return reNoChanges.MatchString(p.TerraformOutput) +} + // Diff Markdown regexes var ( diffKeywordRegex = regexp.MustCompile(`(?m)^( +)([-+~]\s)(.*)(\s=\s|\s->\s|<<|\{|\(known after apply\)| {2,}[^ ]+:.*)(.*)`) diff --git a/server/events/templates/multi_project_plan.tmpl b/server/events/templates/multi_project_plan.tmpl index 5a72db37c5..2693b8a518 100644 --- a/server/events/templates/multi_project_plan.tmpl +++ b/server/events/templates/multi_project_plan.tmpl @@ -1,7 +1,9 @@ {{ define "multiProjectPlan" -}} {{ template "multiProjectHeader" . }} {{ $disableApplyAll := .DisableApplyAll -}} +{{ $hideUnchangedPlans := .HideUnchangedPlanComments -}} {{ range $i, $result := .Results -}} +{{ if (and $hideUnchangedPlans $result.NoChanges) }}{{continue}}{{end -}} ### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}` {{ $result.Rendered }} diff --git a/server/server.go b/server/server.go index 54e40e0d42..27825b6776 100644 --- a/server/server.go +++ b/server/server.go @@ -422,6 +422,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.EnableDiffMarkdownFormat, userConfig.MarkdownTemplateOverridesDir, userConfig.ExecutableName, + userConfig.HideUnchangedPlanComments, ) var lockingClient locking.Locker diff --git a/server/user_config.go b/server/user_config.go index e75f2bd5a7..8f7b1deed2 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -41,6 +41,7 @@ type UserConfig struct { EnableRegExpCmd bool `mapstructure:"enable-regexp-cmd"` EnableDiffMarkdownFormat bool `mapstructure:"enable-diff-markdown-format"` ExecutableName string `mapstructure:"executable-name"` + HideUnchangedPlanComments bool `mapstructure:"hide-unchanged-plan-comments"` GithubAllowMergeableBypassApply bool `mapstructure:"gh-allow-mergeable-bypass-apply"` GithubHostname string `mapstructure:"gh-hostname"` GithubToken string `mapstructure:"gh-token"`