diff --git a/server/events/output_updater.go b/server/events/output_updater.go index eb972fcced..d6c638a0ac 100644 --- a/server/events/output_updater.go +++ b/server/events/output_updater.go @@ -77,18 +77,16 @@ func (c *ChecksOutputUpdater) UpdateOutput(ctx *command.Context, cmd PullCommand }) // Description is a required field - var description string + description := fmt.Sprintf("**Project**: `%s` **Dir**: `%s` **Workspace**: `%s`", projectResult.ProjectName, projectResult.RepoRelDir, projectResult.Workspace) + var state models.CommitStatus if projectResult.Error != nil || projectResult.Failure != "" { - description = fmt.Sprintf("%s failed for %s", strings.Title(projectResult.Command.String()), projectResult.ProjectName) state = models.FailedCommitStatus } else { - description = fmt.Sprintf("%s succeeded for %s", strings.Title(projectResult.Command.String()), projectResult.ProjectName) state = models.SuccessCommitStatus } - // TODO: Make the mark down rendered project specific - output := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo) + output := c.MarkdownRenderer.RenderProject(projectResult, cmd.CommandName(), ctx.Pull.BaseRepo) updateStatusReq := types.UpdateStatusRequest{ Repo: ctx.HeadRepo, Ref: ctx.Pull.HeadCommit, diff --git a/server/vcs/markdown/markdown_renderer.go b/server/vcs/markdown/markdown_renderer.go index 403d4c8204..4f181ec8ac 100644 --- a/server/vcs/markdown/markdown_renderer.go +++ b/server/vcs/markdown/markdown_renderer.go @@ -67,10 +67,10 @@ type projectResultTmplData struct { Rendered string } -// Render formats the data into a markdown string. +// Render formats the data into a markdown string for a command. // nolint: interfacer func (m *Renderer) Render(res command.Result, cmdName command.Name, baseRepo models.Repo) string { - commandStr := strings.Title(strings.Replace(cmdName.String(), "_", " ", -1)) + commandStr := strings.Title(strings.ReplaceAll(cmdName.String(), "_", " ")) common := commonData{ Command: commandStr, DisableApplyAll: m.DisableApplyAll || m.DisableApply, @@ -83,20 +83,31 @@ func (m *Renderer) Render(res command.Result, cmdName command.Name, baseRepo mod if res.Failure != "" { return m.renderTemplate(template.Must(template.New("").Parse(failureWithLogTmpl)), failureData{res.Failure, common}) } - return m.renderProjectResults(res.ProjectResults, common, baseRepo) + + return m.renderProjectResults(res.ProjectResults, common, cmdName, baseRepo) +} + +// RenderProject formats the data into a markdown string for a project +func (m *Renderer) RenderProject(prjRes command.ProjectResult, cmdName command.Name, baseRepo models.Repo) string { + commandStr := strings.Title(strings.ReplaceAll(cmdName.String(), "_", " ")) + common := commonData{ + Command: commandStr, + DisableApply: m.DisableApply, + EnableDiffMarkdownFormat: m.EnableDiffMarkdownFormat, + } + template, templateData := m.TemplateResolver.ResolveProject(prjRes, baseRepo, common) + return m.renderTemplate(template, templateData) } -func (m *Renderer) renderProjectResults(results []command.ProjectResult, common commonData, baseRepo models.Repo) string { +func (m *Renderer) renderProjectResults(results []command.ProjectResult, common commonData, cmdName command.Name, baseRepo models.Repo) string { // render project results var prjResultTmplData []projectResultTmplData for _, result := range results { - template, templateData := m.TemplateResolver.ResolveProject(result, baseRepo, common) - renderedOutput := m.renderTemplate(template, templateData) prjResultTmplData = append(prjResultTmplData, projectResultTmplData{ Workspace: result.Workspace, RepoRelDir: result.RepoRelDir, ProjectName: result.ProjectName, - Rendered: renderedOutput, + Rendered: m.RenderProject(result, cmdName, baseRepo), }) } diff --git a/server/vcs/markdown/markdown_renderer_test.go b/server/vcs/markdown/markdown_renderer_test.go index 37175a31d6..a437b0b271 100644 --- a/server/vcs/markdown/markdown_renderer_test.go +++ b/server/vcs/markdown/markdown_renderer_test.go @@ -1942,3 +1942,265 @@ Plan: 1 to add, 1 to change, 1 to destroy. }) } } + +func TestRenderProjectCustomTemplate(t *testing.T) { + cases := []struct { + Description string + Command command.Name + ProjectResult command.ProjectResult + VCSHost models.VCSHostType + Expected string + TemplateOverrides map[string]string + }{ + { + "Default Plan", + command.Plan, + command.ProjectResult{ + PlanSuccess: &models.PlanSuccess{}, + }, + models.Github, + `$$$diff + +$$$ + +* :arrow_forward: To **apply** this plan, comment: + * $$ +* :put_litter_in_its_place: To **delete** this plan click [here]() +* :repeat: To **plan** this project again, comment: + * $$ +`, + map[string]string{}, + }, + { + "Plan Override", + command.Plan, + command.ProjectResult{ + PlanSuccess: &models.PlanSuccess{}, + }, + models.Github, + "Custom Template", + map[string]string{"project_plan_success": "testdata/custom_template.tmpl"}, + }, + { + "Default Apply", + command.Plan, + command.ProjectResult{ + ApplySuccess: "Apply Output", + }, + models.Github, + `$$$diff +Apply Output +$$$`, + map[string]string{}, + }, + { + "Apply Override", + command.Apply, + command.ProjectResult{ + ApplySuccess: "Apply Output", + }, + models.Github, + "Custom Template", + map[string]string{"project_apply_success": "testdata/custom_template.tmpl"}, + }, + } + for _, c := range cases { + + templateResolver := TemplateResolver{ + GlobalCfg: valid.GlobalCfg{ + Repos: []valid.Repo{ + { + ID: testRepo.ID(), + TemplateOverrides: c.TemplateOverrides, + }, + }, + }, + } + r := Renderer{ + TemplateResolver: templateResolver, + } + expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) + t.Run(fmt.Sprintf("%s_%t", c.Description, false), func(t *testing.T) { + s := r.RenderProject(c.ProjectResult, c.Command, testRepo) + fmt.Println(s) + Equals(t, expWithBackticks, s) + }) + } + +} + +func TestRenderProjectRenderErrorf(t *testing.T) { + err := errors.New("err") + cases := []struct { + Description string + Command command.Name + Error error + Expected string + }{ + { + "plan error", + command.Plan, + err, + "**Plan Error**\n```\nerr\n```", + }, + { + "apply error", + command.Apply, + err, + "**Apply Error**\n```\nerr\n```", + }, + { + "policy check error", + command.PolicyCheck, + err, + "**Policy Check Error**\n```\nerr\n```", + }, + } + r := Renderer{} + for _, c := range cases { + res := command.ProjectResult{ + Error: c.Error, + } + t.Run(c.Description, func(t *testing.T) { + s := r.RenderProject(res, c.Command, testRepo) + Equals(t, c.Expected, s) + }) + } +} + +func TestRenderProjectFailure(t *testing.T) { + cases := []struct { + Description string + Command command.Name + Failure string + Expected string + }{ + { + "apply failure", + command.Apply, + "failure", + "**Apply Failed**: failure", + }, + { + "plan failure", + command.Plan, + "failure", + "**Plan Failed**: failure", + }, + { + "policy check failure", + command.PolicyCheck, + "failure", + "**Policy Check Failed**\n```\nfailure\n```" + + "\n* :heavy_check_mark: To **approve** failing policies either request an approval from approvers or address the failure by modifying the codebase.\n", + }, + } + r := Renderer{} + for _, c := range cases { + res := command.ProjectResult{ + Failure: c.Failure, + } + t.Run(c.Description, func(t *testing.T) { + s := r.RenderProject(res, c.Command, testRepo) + Equals(t, c.Expected, s) + }) + } +} + +func TestRenderProjectErrAndFailure(t *testing.T) { + r := Renderer{} + res := command.ProjectResult{ + Error: errors.New("error"), + Failure: "failure", + } + s := r.RenderProject(res, command.Plan, testRepo) + Equals(t, "**Plan Error**\n```\nerror\n```", s) +} + +func TestRenderProjectDisableApply(t *testing.T) { + cases := []struct { + Description string + Command command.Name + ProjectResult command.ProjectResult + VCSHost models.VCSHostType + Expected string + DisableApply bool + }{ + { + "successful plan with disable apply not set", + command.Plan, + command.ProjectResult{ + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "terraform-output", + LockURL: "lock-url", + RePlanCmd: "atlantis plan -d path -w workspace", + ApplyCmd: "atlantis apply -d path -w workspace", + }, + Workspace: "workspace", + RepoRelDir: "path", + }, + models.Github, + `$$$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$ +`, + false, + }, + { + "successful plan with disable apply set", + command.Plan, + command.ProjectResult{ + PlanSuccess: &models.PlanSuccess{ + TerraformOutput: "terraform-output", + LockURL: "lock-url", + RePlanCmd: "atlantis plan -d path -w workspace", + ApplyCmd: "atlantis apply -d path -w workspace", + }, + Workspace: "workspace", + RepoRelDir: "path", + }, + models.Github, + `$$$diff +terraform-output +$$$ + +* :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$ +`, + true, + }, + } + for _, c := range cases { + r := Renderer{ + DisableApply: c.DisableApply, + } + t.Run(c.Description, func(t *testing.T) { + s := r.RenderProject(c.ProjectResult, c.Command, testRepo) + fmt.Print(s) + expWithBackticks := strings.Replace(c.Expected, "$", "`", -1) + Equals(t, expWithBackticks, s) + }) + } +} + +func TestRenderProjectFolding(t *testing.T) { + mr := Renderer{ + TemplateResolver: TemplateResolver{ + DisableMarkdownFolding: true, + }, + } + + rendered := mr.RenderProject(command.ProjectResult{ + RepoRelDir: ".", + Workspace: "default", + Error: errors.New(strings.Repeat("line\n", 13)), + }, command.Plan, testRepo) + Equals(t, false, strings.Contains(rendered, "
")) +} diff --git a/server/vcs/markdown/template_resolver.go b/server/vcs/markdown/template_resolver.go index 48314e8b2a..c28449cd68 100644 --- a/server/vcs/markdown/template_resolver.go +++ b/server/vcs/markdown/template_resolver.go @@ -113,6 +113,15 @@ func (t *TemplateResolver) ResolveProject(result command.ProjectResult, baseRepo var templateData interface{} switch { + case result.Error != nil: + tmpl = t.buildTemplate(Error, baseRepo.VCSHost.Type, wrappedErrTmpl, unwrappedErrTmpl, result.Error.Error(), templateOverrides) + templateData = struct { + Command string + Error string + }{ + Command: common.Command, + Error: result.Error.Error(), + } case result.Failure != "": // use template override if specified if val, ok := templateOverrides["project_failure"]; ok { @@ -128,15 +137,6 @@ func (t *TemplateResolver) ResolveProject(result command.ProjectResult, baseRepo Command: common.Command, Failure: result.Failure, } - case result.Error != nil: - tmpl = t.buildTemplate(Error, baseRepo.VCSHost.Type, wrappedErrTmpl, unwrappedErrTmpl, result.Error.Error(), templateOverrides) - templateData = struct { - Command string - Error string - }{ - Command: common.Command, - Error: result.Error.Error(), - } case result.PlanSuccess != nil: tmpl = t.buildTemplate(PlanSuccess, baseRepo.VCSHost.Type, planSuccessWrappedTmpl, planSuccessUnwrappedTmpl, result.PlanSuccess.TerraformOutput, templateOverrides) templateData = planSuccessData{