Repository: runatlantis/atlantis (language: go)

Issue:
Add Summaries to Plan And Apply PR Comments
<!--- Please keep this note for the community --->

### Community Note

- Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
- Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
- If you are interested in working on this issue or have submitted a pull request, please leave a comment.

<!--- Thank you for keeping this note for the community --->

---

- [x] I'd be willing to implement this feature ([contributing guide](https://github.com/runatlantis/atlantis/blob/main/CONTRIBUTING.md))

**Describe the user story**

As an Atlantis user, I would like to see the following:

1) If there is more than 1 project in a plan, when the plan is finished, I would like to be able to quickly see how many projects have changes, have no changes and have failed the plan.
2) If there is more than 1 project in an apply, when the apply is finished, I would like to be able to quickly see how many projects applied successfully, how many failed and how many errored.

**Describe the solution you'd like**

Add summary lines to the Atlantis plan and apply PR comments detailing the required information.

**Describe the drawbacks of your solution**

None

**Describe alternatives you've considered**

None



Discussion / hints:

Fix patch:
diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go
index 17328852d3..74a72c6719 100644
--- a/server/events/markdown_renderer.go
+++ b/server/events/markdown_renderer.go
@@ -93,6 +93,22 @@ type resultData struct {
 	commonData
 }
 
+type planResultData struct {
+	Results []projectResultTmplData
+	commonData
+	NumPlansWithChanges   int
+	NumPlansWithNoChanges int
+	NumPlanFailures       int
+}
+
+type applyResultData struct {
+	Results []projectResultTmplData
+	commonData
+	NumApplySuccesses int
+	NumApplyFailures  int
+	NumApplyErrors    int
+}
+
 type planSuccessData struct {
 	models.PlanSuccess
 	PlanSummary              string
@@ -187,6 +203,11 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
 	numPolicyCheckSuccesses := 0
 	numPolicyApprovalSuccesses := 0
 	numVersionSuccesses := 0
+	numPlansWithChanges := 0
+	numPlansWithNoChanges := 0
+	numApplySuccesses := 0
+	numApplyFailures := 0
+	numApplyErrors := 0
 
 	templates := m.markdownTemplates
 
@@ -213,6 +234,11 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
 				resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("planSuccessUnwrapped"), data)
 			}
 			resultData.NoChanges = result.PlanSuccess.NoChanges()
+			if result.PlanSuccess.NoChanges() {
+				numPlansWithNoChanges++
+			} else {
+				numPlansWithChanges++
+			}
 			numPlanSuccesses++
 		} else if result.PolicyCheckResults != nil && common.Command == policyCheckCommandTitle {
 			policyCheckResults := policyCheckResultsData{
@@ -255,6 +281,7 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
 			} else {
 				resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("applyUnwrappedSuccess"), struct{ Output string }{output})
 			}
+			numApplySuccesses++
 		} else if result.VersionSuccess != "" {
 			output := strings.TrimSpace(result.VersionSuccess)
 			if m.shouldUseWrappedTmpl(vcsHost, output) {
@@ -289,8 +316,14 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
 				tmpl = templates.Lookup("wrappedErr")
 			}
 			resultData.Rendered = m.renderTemplateTrimSpace(tmpl, errData{result.Error.Error(), resultData.Rendered, common})
+			if common.Command == applyCommandTitle {
+				numApplyErrors++
+			}
 		} else if result.Failure != "" {
 			resultData.Rendered = m.renderTemplateTrimSpace(templates.Lookup("failure"), failureData{result.Failure, resultData.Rendered, common})
+			if common.Command == applyCommandTitle {
+				numApplyFailures++
+			}
 		}
 		resultsTmplData = append(resultsTmplData, resultData)
 	}
@@ -324,7 +357,7 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
 		tmpl = templates.Lookup("multiProjectPlan")
 	case common.Command == policyCheckCommandTitle:
 		if numPolicyCheckSuccesses == len(results) {
-			tmpl = templates.Lookup("multiProjectPlan")
+			tmpl = templates.Lookup("multiProjectPolicy")
 		} else {
 			tmpl = templates.Lookup("multiProjectPolicyUnsuccessful")
 		}
@@ -350,6 +383,14 @@ func (m *MarkdownRenderer) renderProjectResults(results []command.ProjectResult,
 	default:
 		return fmt.Sprintf("no template matched–this is a bug: command=%s", common.Command)
 	}
+
+	switch {
+	case common.Command == planCommandTitle:
+		numPlanFailures := len(results) - numPlanSuccesses
+		return m.renderTemplateTrimSpace(tmpl, planResultData{resultsTmplData, common, numPlansWithChanges, numPlansWithNoChanges, numPlanFailures})
+	case common.Command == applyCommandTitle:
+		return m.renderTemplateTrimSpace(tmpl, applyResultData{resultsTmplData, common, numApplySuccesses, numApplyFailures, numApplyErrors})
+	}
 	return m.renderTemplateTrimSpace(tmpl, resultData{resultsTmplData, common})
 }
 
diff --git a/server/events/templates/multi_project_apply.tmpl b/server/events/templates/multi_project_apply.tmpl
index 701d766971..50038555b3 100644
--- a/server/events/templates/multi_project_apply.tmpl
+++ b/server/events/templates/multi_project_apply.tmpl
@@ -6,5 +6,6 @@
 
 ---
 {{ end -}}
-{{- template "log" . -}}
+{{ template "multiProjectApplyFooter" . -}}
+{{ template "log" . -}}
 {{ end -}}
diff --git a/server/events/templates/multi_project_apply_footer.tmpl b/server/events/templates/multi_project_apply_footer.tmpl
new file mode 100644
index 0000000000..58299868ce
--- /dev/null
+++ b/server/events/templates/multi_project_apply_footer.tmpl
@@ -0,0 +1,7 @@
+{{ define "multiProjectApplyFooter" -}}
+{{ if (gt (len .Results) 1) -}}
+### Apply Summary
+
+{{ len .Results }} projects, {{ .NumApplySuccesses }} successful, {{ .NumApplyFailures }} failed, {{ .NumApplyErrors }} errored
+{{ end -}}
+{{ end -}}
diff --git a/server/events/templates/multi_project_plan.tmpl b/server/events/templates/multi_project_plan.tmpl
index 456b0ecf61..9c3898ad48 100644
--- a/server/events/templates/multi_project_plan.tmpl
+++ b/server/events/templates/multi_project_plan.tmpl
@@ -7,17 +7,8 @@
 ### {{ add $i 1 }}. {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`
 {{ $result.Rendered }}
 
-{{ if ne $disableApplyAll true -}}
 ---
 {{ end -}}
-{{ end -}}
-{{ if ne .DisableApplyAll true -}}
-{{ if and (gt (len .Results) 0) (not .PlansDeleted) -}}
-* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
-    * `{{ .ExecutableName }} apply`
-* :put_litter_in_its_place: To **delete** all plans and locks for the PR, comment:
-    * `{{ .ExecutableName }} unlock`
-{{ end -}}
-{{ end -}}
-{{- template "log" . -}}
+{{ template "multiProjectPlanFooter" . -}}
+{{ template "log" . -}}
 {{ end -}}
diff --git a/server/events/templates/multi_project_plan_footer.tmpl b/server/events/templates/multi_project_plan_footer.tmpl
new file mode 100644
index 0000000000..41683ab018
--- /dev/null
+++ b/server/events/templates/multi_project_plan_footer.tmpl
@@ -0,0 +1,13 @@
+{{ define "multiProjectPlanFooter" -}}
+{{ if and (gt (len .Results) 0) -}}
+### Plan Summary
+
+{{ len .Results }} projects, {{ .NumPlansWithChanges }} with changes, {{ .NumPlansWithNoChanges }} with no changes, {{ .NumPlanFailures }} failed
+{{ if and (not .PlansDeleted) (ne .DisableApplyAll true) }}
+* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
+    * `{{ .ExecutableName }} apply`
+* :put_litter_in_its_place: To **delete** all plans and locks for the PR, comment:
+    * `{{ .ExecutableName }} unlock`
+{{ end -}}
+{{ end -}}
+{{ end -}}
diff --git a/server/events/templates/multi_project_policy.tmpl b/server/events/templates/multi_project_policy.tmpl
new file mode 100644
index 0000000000..c34c59f896
--- /dev/null
+++ b/server/events/templates/multi_project_policy.tmpl
@@ -0,0 +1,23 @@
+{{ define "multiProjectPolicy" -}}
+{{ 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 }}
+
+{{ if ne $disableApplyAll true -}}
+---
+{{ end -}}
+{{ end -}}
+{{ if ne .DisableApplyAll true -}}
+{{ if and (gt (len .Results) 0) (not .PlansDeleted) -}}
+* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
+    * `{{ .ExecutableName }} apply`
+* :put_litter_in_its_place: To **delete** all plans and locks for the PR, comment:
+    * `{{ .ExecutableName }} unlock`
+{{ end -}}
+{{ end -}}
+{{ template "log" . -}}
+{{ end -}}
diff --git a/server/events/templates/single_project_apply.tmpl b/server/events/templates/single_project_apply.tmpl
index a963641b81..173bb46847 100644
--- a/server/events/templates/single_project_apply.tmpl
+++ b/server/events/templates/single_project_apply.tmpl
@@ -3,5 +3,5 @@
 Ran {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`
 
 {{ $result.Rendered }}
-{{- template "log" . -}}
+{{ template "log" . -}}
 {{ end -}}
diff --git a/server/events/templates/single_project_import_success.tmpl b/server/events/templates/single_project_import_success.tmpl
index 7e0fbd65c1..e03b53de5e 100644
--- a/server/events/templates/single_project_import_success.tmpl
+++ b/server/events/templates/single_project_import_success.tmpl
@@ -3,5 +3,5 @@
 Ran {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectName }}` {{ end }}dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`
 
 {{ $result.Rendered }}
-{{- template "log" . -}}
+{{ template "log" . -}}
 {{ end -}}
diff --git a/server/events/templates/single_project_plan_success.tmpl b/server/events/templates/single_project_plan_success.tmpl
index 46c1b6640a..afbe3d5701 100644
--- a/server/events/templates/single_project_plan_success.tmpl
+++ b/server/events/templates/single_project_plan_success.tmpl
@@ -10,5 +10,5 @@ Ran {{ .Command }} for {{ if $result.ProjectName }}project: `{{ $result.ProjectN
 * :put_litter_in_its_place: To **delete** all plans and locks for the PR, comment:
     * `{{ .ExecutableName }} unlock`
 {{ end -}}
-{{- template "log" . -}}
+{{ template "log" . -}}
 {{ end -}}
diff --git a/server/events/templates/single_project_plan_unsuccessful.tmpl b/server/events/templates/single_project_plan_unsuccessful.tmpl
index 45c5c54926..1dd0cd9643 100644
--- a/server/events/templates/single_project_plan_unsuccessful.tmpl
+++ b/server/events/templates/single_project_plan_unsuccessful.tmpl
@@ -3,5 +3,5 @@
 Ran {{ .Command }} for dir: `{{ $result.RepoRelDir }}` workspace: `{{ $result.Workspace }}`
 
 {{ $result.Rendered }}
-{{- template "log" . -}}
+{{ template "log" . -}}
 {{ end -}}
diff --git a/server/events/templates/unwrapped_err.tmpl b/server/events/templates/unwrapped_err.tmpl
index 1be0a7ef29..37b201ab93 100644
--- a/server/events/templates/unwrapped_err.tmpl
+++ b/server/events/templates/unwrapped_err.tmpl
@@ -1,9 +1,9 @@
 {{ define "unwrappedErr" -}}
-**{{.Command}} Error**
+**{{ .Command }} Error**
 ```
-{{.Error}}
+{{ .Error }}
 ```
-{{- if ne .RenderedContext ""}}
+{{ if ne .RenderedContext "" -}}
 {{ .RenderedContext }}
-{{- end }}
-{{- end }}
+{{ end -}}
+{{ end -}}


Test patch:
diff --git a/server/controllers/events/testdata/test-repos/automerge/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/automerge/exp-output-autoplan.txt
index 4f8bbe1fa0..c32ed6dfdc 100644
--- a/server/controllers/events/testdata/test-repos/automerge/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/automerge/exp-output-autoplan.txt
@@ -49,6 +49,10 @@ Plan: 1 to add, 0 to change, 0 to destroy.
     * `atlantis plan -d dir2`
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-autoplan.txt
index 6d3aa862ab..7f0f5f45a8 100644
--- a/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-autoplan.txt
@@ -69,6 +69,10 @@ Plan: 1 to add, 0 to change, 0 to destroy.
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-plan-again.txt b/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-plan-again.txt
index ce5547f08a..c9a7d87124 100644
--- a/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-plan-again.txt
+++ b/server/controllers/events/testdata/test-repos/import-multiple-project/exp-output-plan-again.txt
@@ -53,6 +53,10 @@ Plan: 1 to add, 0 to change, 0 to destroy.
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 1 with changes, 1 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/modules-yaml/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/modules-yaml/exp-output-autoplan.txt
index 25fe0e6121..1e55d623b5 100644
--- a/server/controllers/events/testdata/test-repos/modules-yaml/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/modules-yaml/exp-output-autoplan.txt
@@ -63,6 +63,10 @@ Changes to Outputs:
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-apply.txt b/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-apply.txt
index 34121591c1..eb6bda8987 100644
--- a/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-apply.txt
+++ b/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-apply.txt
@@ -19,4 +19,7 @@ workspace = "default"
 ### 2. dir: `dir2` workspace: `default`
 **Apply Failed**: All policies must pass for project before running apply.
 
----
\ No newline at end of file
+---
+### Apply Summary
+
+2 projects, 1 successful, 1 failed, 0 errored
\ No newline at end of file
diff --git a/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt
index 55d2149939..098c4eba93 100644
--- a/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt
@@ -63,6 +63,10 @@ Changes to Outputs:
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-apply.txt b/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-apply.txt
index 8490f306d7..a136ff9691 100644
--- a/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-apply.txt
+++ b/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-apply.txt
@@ -20,4 +20,7 @@ null_resource.staging[0]: Creation complete after *s [id=*******************]
 Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
 ```
 
----
\ No newline at end of file
+---
+### Apply Summary
+
+2 projects, 2 successful, 0 failed, 0 errored
\ No newline at end of file
diff --git a/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-autoplan.txt
index 4f48c365d6..29f5f76dae 100644
--- a/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/repo-config-file/exp-output-autoplan.txt
@@ -49,6 +49,10 @@ Plan: 1 to add, 0 to change, 0 to destroy.
     * `atlantis plan -d infrastructure/production`
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-autoplan.txt
index 615a3f4c61..ad9591b8ae 100644
--- a/server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/server-side-cfg/exp-output-autoplan.txt
@@ -71,6 +71,10 @@ Changes to Outputs:
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-all.txt b/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-all.txt
index 711bf571ce..61eac2271a 100644
--- a/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-all.txt
+++ b/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-apply-all.txt
@@ -38,4 +38,7 @@ postapply
 
 </details>
 
----
\ No newline at end of file
+---
+### Apply Summary
+
+2 projects, 2 successful, 0 failed, 0 errored
\ No newline at end of file
diff --git a/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-autoplan.txt
index 88eda9d431..dcbb45bf78 100644
--- a/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/simple-yaml/exp-output-autoplan.txt
@@ -70,6 +70,10 @@ Changes to Outputs:
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/simple/exp-output-apply-var-all.txt b/server/controllers/events/testdata/test-repos/simple/exp-output-apply-var-all.txt
index bd6526fa8c..2977099b55 100644
--- a/server/controllers/events/testdata/test-repos/simple/exp-output-apply-var-all.txt
+++ b/server/controllers/events/testdata/test-repos/simple/exp-output-apply-var-all.txt
@@ -46,4 +46,7 @@ workspace = "new_workspace"
 
 </details>
 
----
\ No newline at end of file
+---
+### Apply Summary
+
+2 projects, 2 successful, 0 failed, 0 errored
\ No newline at end of file
diff --git a/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-autoplan.txt
index df46d869e6..49c4dc2673 100644
--- a/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-autoplan.txt
@@ -69,6 +69,10 @@ Plan: 1 to add, 0 to change, 0 to destroy.
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan-again.txt b/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan-again.txt
index df46d869e6..49c4dc2673 100644
--- a/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan-again.txt
+++ b/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan-again.txt
@@ -69,6 +69,10 @@ Plan: 1 to add, 0 to change, 0 to destroy.
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan.txt b/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan.txt
index 2766ea44de..fb3cfdbbd7 100644
--- a/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan.txt
+++ b/server/controllers/events/testdata/test-repos/state-rm-multiple-project/exp-output-plan.txt
@@ -37,6 +37,10 @@ and found no differences, so no changes are needed.
     * `atlantis plan -d dir2`
 
 ---
+### Plan Summary
+
+2 projects, 0 with changes, 2 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-autoplan.txt b/server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-autoplan.txt
index d8a084bd85..82ce193d9f 100644
--- a/server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-autoplan.txt
+++ b/server/controllers/events/testdata/test-repos/tfvars-yaml/exp-output-autoplan.txt
@@ -67,6 +67,10 @@ Changes to Outputs:
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt b/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt
index 73d66792e4..cd4e8e0b95 100644
--- a/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt
+++ b/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt
@@ -63,6 +63,10 @@ Changes to Outputs:
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt b/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt
index 73d66792e4..cd4e8e0b95 100644
--- a/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt
+++ b/server/controllers/events/testdata/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt
@@ -63,6 +63,10 @@ Changes to Outputs:
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
diff --git a/server/events/apply_command_runner_test.go b/server/events/apply_command_runner_test.go
index 27b1efa4fe..838ca3e299 100644
--- a/server/events/apply_command_runner_test.go
+++ b/server/events/apply_command_runner_test.go
@@ -258,7 +258,7 @@ func TestApplyCommandRunner_ExecutionOrder(t *testing.T) {
 			},
 			ExpComment: "Ran Apply for 2 projects:\n\n" +
 				"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
-				"2. dir: `` workspace: ``\n**Apply Error**\n```\nshabang\n```\n\n---",
+				"2. dir: `` workspace: ``\n**Apply Error**\n```\nshabang\n```\n\n---\n### Apply Summary\n\n2 projects, 1 successful, 0 failed, 1 errored",
 		},
 		{
 			Description: "When first apply fails, the second not will run",
@@ -343,7 +343,7 @@ func TestApplyCommandRunner_ExecutionOrder(t *testing.T) {
 			},
 			ExpComment: "Ran Apply for 2 projects:\n\n" +
 				"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
-				"2. dir: `` workspace: ``\n**Apply Error**\n```\nshabang\n```\n\n---",
+				"2. dir: `` workspace: ``\n**Apply Error**\n```\nshabang\n```\n\n---\n### Apply Summary\n\n2 projects, 1 successful, 0 failed, 1 errored",
 		},
 		{
 			Description: "When one out of two fails, the following two will not run",
@@ -398,7 +398,7 @@ func TestApplyCommandRunner_ExecutionOrder(t *testing.T) {
 				"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
 				"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
 				"3. dir: `` workspace: ``\n**Apply Error**\n```\nshabang\n```\n\n---\n### " +
-				"4. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---",
+				"4. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### Apply Summary\n\n4 projects, 3 successful, 0 failed, 1 errored",
 		},
 		{
 			Description: "Don't block when parallel is not set",
@@ -430,7 +430,7 @@ func TestApplyCommandRunner_ExecutionOrder(t *testing.T) {
 			},
 			ExpComment: "Ran Apply for 2 projects:\n\n" +
 				"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n**Apply Error**\n```\nshabang\n```\n\n---\n### " +
-				"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---",
+				"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### Apply Summary\n\n2 projects, 1 successful, 0 failed, 1 errored",
 		},
 		{
 			Description: "Don't block when abortOnExcecutionOrderFail is not set",
@@ -460,7 +460,7 @@ func TestApplyCommandRunner_ExecutionOrder(t *testing.T) {
 			},
 			ExpComment: "Ran Apply for 2 projects:\n\n" +
 				"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n**Apply Error**\n```\nshabang\n```\n\n---\n### " +
-				"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---",
+				"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### Apply Summary\n\n2 projects, 1 successful, 0 failed, 1 errored",
 		},
 	}
 
diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go
index 1b048b044d..eebd1a8b87 100644
--- a/server/events/markdown_renderer_test.go
+++ b/server/events/markdown_renderer_test.go
@@ -26,6 +26,11 @@ import (
 	. "github.com/runatlantis/atlantis/testing"
 )
 
+// Strip Carriage Returns, leading and trailing spaces and replace 'dollar' with 'backtick' in the string
+func normalize(s string) string {
+	return strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(s, "$", "`"), "\r", ""))
+}
+
 func TestRenderErr(t *testing.T) {
 	err := errors.New("err")
 	cases := []struct {
@@ -63,9 +68,9 @@ func TestRenderErr(t *testing.T) {
 			t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) {
 				s := r.Render(res, c.Command, "", "log", verbose, models.Github)
 				if !verbose {
-					Equals(t, strings.TrimSpace(c.Expected), strings.TrimSpace(s))
+					Equals(t, normalize(c.Expected), normalize(s))
 				} else {
-					Equals(t, c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+					Equals(t, normalize(c.Expected)+"\n\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", normalize(s))
 				}
 			})
 		}
@@ -108,9 +113,9 @@ func TestRenderFailure(t *testing.T) {
 			t.Run(fmt.Sprintf("%s_%t", c.Description, verbose), func(t *testing.T) {
 				s := r.Render(res, c.Command, "", "log", verbose, models.Github)
 				if !verbose {
-					Equals(t, strings.TrimSpace(c.Expected), strings.TrimSpace(s))
+					Equals(t, normalize(c.Expected), normalize(s))
 				} else {
-					Equals(t, c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+					Equals(t, normalize(c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>"), normalize(s))
 				}
 			})
 		}
@@ -124,7 +129,7 @@ func TestRenderErrAndFailure(t *testing.T) {
 		Failure: "failure",
 	}
 	s := r.Render(res, command.Plan, "", "", false, models.Github)
-	Equals(t, "**Plan Error**\n```\nerror\n```", s)
+	Equals(t, "**Plan Error**\n```\nerror\n```", normalize(s))
 }
 
 func TestRenderProjectResults(t *testing.T) {
@@ -424,7 +429,8 @@ $$$
 :put_litter_in_its_place: A plan file was discarded. Re-plan would be required before applying.
 
 * :repeat: To **plan** this project again, comment:
-  * $atlantis plan -d path -w workspace$`,
+  * $atlantis plan -d path -w workspace$
+`,
 		},
 		{
 			"single successful state rm",
@@ -470,7 +476,8 @@ $$$
 
 $$$diff
 success
-$$$`,
+$$$
+`,
 		},
 		{
 			"single successful apply with project name",
@@ -489,7 +496,8 @@ $$$`,
 
 $$$diff
 success
-$$$`,
+$$$
+`,
 		},
 		{
 			"multiple successful plans",
@@ -548,6 +556,10 @@ $$$
     * $atlantis plan -d path2 -w workspace$
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
@@ -667,6 +679,9 @@ success2
 $$$
 
 ---
+### Apply Summary
+
+2 projects, 2 successful, 0 failed, 0 errored
 `,
 		},
 		{
@@ -686,7 +701,8 @@ $$$
 **Plan Error**
 $$$
 error
-$$$`,
+$$$
+`,
 		},
 		{
 			"single failed plan",
@@ -702,7 +718,8 @@ $$$`,
 			models.Github,
 			`Ran Plan for dir: $path$ workspace: $workspace$
 
-**Plan Failed**: failure`,
+**Plan Failed**: failure
+`,
 		},
 		{
 			"successful, failed, and errored plan",
@@ -761,6 +778,10 @@ error
 $$$
 
 ---
+### Plan Summary
+
+3 projects, 1 with changes, 0 with no changes, 2 failed
+
 * :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:
@@ -911,6 +932,9 @@ error
 $$$
 
 ---
+### Apply Summary
+
+3 projects, 1 successful, 1 failed, 1 errored
 `,
 		},
 		{
@@ -958,6 +982,9 @@ error
 $$$
 
 ---
+### Apply Summary
+
+3 projects, 1 successful, 1 failed, 1 errored
 `,
 		},
 	}
@@ -971,11 +998,10 @@ $$$
 			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))
+						Equals(t, normalize(c.Expected), normalize(s))
 					} else {
-						Equals(t, expWithBackticks+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+						Equals(t, normalize(c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>"), normalize(s))
 					}
 				})
 			}
@@ -1094,6 +1120,7 @@ $$$
 * :repeat: To **plan** this project again, comment:
     * $atlantis plan -d path -w workspace$
 
+---
 ### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
 $$$diff
 terraform-output2
@@ -1105,6 +1132,10 @@ $$$
 * :repeat: To **plan** this project again, comment:
     * $atlantis plan -d path2 -w workspace$
 
+---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
 `,
 		},
 	}
@@ -1127,11 +1158,10 @@ $$$
 			for _, verbose := range []bool{true, false} {
 				t.Run(c.Description, func(t *testing.T) {
 					s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
-					expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
 					if !verbose {
-						Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
+						Equals(t, normalize(c.Expected), normalize(s))
 					} else {
-						Equals(t, expWithBackticks+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+						Equals(t, normalize(c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>"), normalize(s))
 					}
 				})
 			}
@@ -1244,6 +1274,7 @@ $$$
 * :repeat: To **plan** this project again, comment:
     * $atlantis plan -d path -w workspace$
 
+---
 ### 2. project: $projectname$ dir: $path2$ workspace: $workspace$
 $$$diff
 terraform-output2
@@ -1253,6 +1284,10 @@ $$$
 * :repeat: To **plan** this project again, comment:
     * $atlantis plan -d path2 -w workspace$
 
+---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
 `,
 		},
 	}
@@ -1276,11 +1311,10 @@ $$$
 			for _, verbose := range []bool{true, false} {
 				t.Run(c.Description, func(t *testing.T) {
 					s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
-					expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
 					if !verbose {
-						Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
+						Equals(t, normalize(c.Expected), normalize(s))
 					} else {
-						Equals(t, expWithBackticks+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+						Equals(t, normalize(c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>"), normalize(s))
 					}
 				})
 			}
@@ -1342,8 +1376,7 @@ $$$
 * :repeat: To re-run policies **plan** this project again by commenting:
     * $atlantis plan -d path -w workspace$`
 
-	expWithBackticks := strings.Replace(exp, "$", "`", -1)
-	Equals(t, expWithBackticks, rendered)
+	Equals(t, normalize(exp), normalize(rendered))
 }
 
 // Test that if folding is disabled that it's not used.
@@ -1480,9 +1513,7 @@ $$$
 ` + c.Output + `
 $$$`
 				}
-
-				expWithBackticks := strings.Replace(exp, "$", "`", -1)
-				Equals(t, expWithBackticks, rendered)
+				Equals(t, normalize(exp), normalize(rendered))
 			})
 	}
 }
@@ -1656,8 +1687,7 @@ $$$`
 						}
 					}
 
-					expWithBackticks := strings.Replace(exp, "$", "`", -1)
-					Equals(t, expWithBackticks, rendered)
+					Equals(t, normalize(exp), normalize(rendered))
 				})
 		}
 	}
@@ -1714,9 +1744,12 @@ $$$
 
 </details>
 
----`
-	expWithBackticks := strings.Replace(exp, "$", "`", -1)
-	Equals(t, expWithBackticks, rendered)
+---
+### Apply Summary
+
+2 projects, 2 successful, 0 failed, 0 errored
+`
+	Equals(t, normalize(exp), normalize(rendered))
 }
 
 func TestRenderProjectResults_MultiProjectPlanWrapped(t *testing.T) {
@@ -1793,12 +1826,16 @@ $$$
 Plan: 1 to add, 0 to change, 0 to destroy.
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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$`
-	expWithBackticks := strings.Replace(exp, "$", "`", -1)
-	Equals(t, expWithBackticks, rendered)
+    * $atlantis unlock$
+`
+	Equals(t, normalize(exp), normalize(rendered))
 }
 
 // Test rendering when there was an error in one of the plans and we deleted
@@ -1851,7 +1888,11 @@ func TestRenderProjectResults_PlansDeleted(t *testing.T) {
 ### 2. dir: $.$ workspace: $production$
 **Plan Failed**: failure
 
----`,
+---
+### Plan Summary
+
+2 projects, 0 with changes, 0 with no changes, 2 failed
+`,
 		},
 		"one failure, one success": {
 			cr: command.Result{
@@ -1890,7 +1931,11 @@ $$$
 
 This plan was not saved because one or more projects failed and automerge requires all plans pass.
 
----`,
+---
+### Plan Summary
+
+2 projects, 1 with changes, 0 with no changes, 1 failed
+`,
 		},
 	}
 
@@ -1908,8 +1953,7 @@ This plan was not saved because one or more projects failed and automerge requir
 				false,      // hideUnchangedPlanComments
 			)
 			rendered := mr.Render(c.cr, command.Plan, "", "log", false, models.Github)
-			expWithBackticks := strings.Replace(c.exp, "$", "`", -1)
-			Equals(t, expWithBackticks, rendered)
+			Equals(t, normalize(c.exp), normalize(rendered))
 		})
 	}
 }
@@ -2051,7 +2095,8 @@ $$$
 
 $$$diff
 success
-$$$`,
+$$$
+`,
 		},
 		{
 			"single successful apply with project name",
@@ -2069,7 +2114,8 @@ $$$`,
 
 $$$diff
 success
-$$$`,
+$$$
+`,
 		},
 		{
 			"multiple successful plans",
@@ -2125,6 +2171,10 @@ $$$
     * $atlantis plan -d path2 -w workspace$
 
 ---
+### Plan Summary
+
+2 projects, 2 with changes, 0 with no changes, 0 failed
+
 * :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:
@@ -2165,6 +2215,9 @@ success2
 $$$
 
 ---
+### Apply Summary
+
+2 projects, 2 successful, 0 failed, 0 errored
 `,
 		},
 		{
@@ -2183,7 +2236,8 @@ $$$
 **Plan Error**
 $$$
 error
-$$$`,
+$$$
+`,
 		},
 		{
 			"single failed plan",
@@ -2198,7 +2252,8 @@ $$$`,
 			models.Github,
 			`Ran Plan for dir: $path$ workspace: $workspace$
 
-**Plan Failed**: failure`,
+**Plan Failed**: failure
+`,
 		},
 		{
 			"successful, failed, and errored plan",
@@ -2255,6 +2310,10 @@ error
 $$$
 
 ---
+### Plan Summary
+
+3 projects, 1 with changes, 0 with no changes, 2 failed
+
 * :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:
@@ -2305,6 +2364,9 @@ error
 $$$
 
 ---
+### Apply Summary
+
+3 projects, 1 successful, 1 failed, 1 errored
 `,
 		},
 		{
@@ -2351,6 +2413,9 @@ error
 $$$
 
 ---
+### Apply Summary
+
+3 projects, 1 successful, 1 failed, 1 errored
 `,
 		},
 	}
@@ -2374,11 +2439,10 @@ $$$
 			for _, verbose := range []bool{true, false} {
 				t.Run(c.Description, func(t *testing.T) {
 					s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
-					expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
 					if !verbose {
-						Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
+						Equals(t, normalize(c.Expected), normalize(s))
 					} else {
-						Equals(t, expWithBackticks+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+						Equals(t, normalize(c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>"), normalize(s))
 					}
 				})
 			}
@@ -2812,11 +2876,10 @@ func TestRenderProjectResultsWithEnableDiffMarkdownFormat(t *testing.T) {
 			for _, verbose := range []bool{true, false} {
 				t.Run(c.Description, func(t *testing.T) {
 					s := r.Render(res, c.Command, "", "log", verbose, c.VCSHost)
-					expWithBackticks := strings.Replace(c.Expected, "$", "`", -1)
 					if !verbose {
-						Equals(t, strings.TrimSpace(expWithBackticks), strings.TrimSpace(s))
+						Equals(t, normalize(c.Expected), normalize(s))
 					} else {
-						Equals(t, expWithBackticks+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+						Equals(t, normalize(c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>"), normalize(s))
 					}
 				})
 			}
@@ -2937,6 +3000,10 @@ $$$
     * $atlantis plan -d path3 -w workspace$
 
 ---
+### Plan Summary
+
+3 projects, 2 with changes, 1 with no changes, 0 failed
+
 * :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:
@@ -2988,6 +3055,10 @@ $$$
 1. project: $projectname$ dir: $path2$ workspace: $workspace$
 1. project: $projectname2$ dir: $path3$ workspace: $workspace$
 
+### Plan Summary
+
+3 projects, 0 with changes, 3 with no changes, 0 failed
+
 * :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:
@@ -3005,11 +3076,10 @@ $$$
 			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))
+						Equals(t, normalize(c.Expected), normalize(s))
 					} else {
-						Equals(t, expWithBackticks+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>", s)
+						Equals(t, normalize(c.Expected+"\n<details><summary>Log</summary>\n  <p>\n\n```\nlog```\n</p></details>"), normalize(s))
 					}
 				})
 			}