From eb06a763766b6371cb38b0030b5e8e599d36178f Mon Sep 17 00:00:00 2001
From: M Atif Ali
Date: Wed, 23 Apr 2025 18:15:16 +0500
Subject: [PATCH] =?UTF-8?q?Revert=20"feat(coderd/notifications):=20group?=
=?UTF-8?q?=20workspace=20build=20failure=20report=20(che=E2=80=A6"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit 4ca425deccc6abfab7b0d6a8b928f3e63cba3a68.
---
coderd/database/dbmem/dbmem.go | 1 -
...group_build_failure_notifications.down.sql | 21 --
...6_group_build_failure_notifications.up.sql | 29 ---
coderd/database/queries.sql.go | 11 +-
coderd/database/queries/workspacebuilds.sql | 1 -
coderd/notifications/notifications_test.go | 111 +++-------
coderd/notifications/reports/generator.go | 160 ++++++--------
.../reports/generator_internal_test.go | 202 +++++++-----------
...ateWorkspaceBuildsFailedReport.html.golden | 131 +++---------
...ateWorkspaceBuildsFailedReport.json.golden | 129 ++++-------
10 files changed, 245 insertions(+), 551 deletions(-)
delete mode 100644 coderd/database/migrations/000316_group_build_failure_notifications.down.sql
delete mode 100644 coderd/database/migrations/000316_group_build_failure_notifications.up.sql
diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go
index 19cbc16e63d0b..87275b1051efe 100644
--- a/coderd/database/dbmem/dbmem.go
+++ b/coderd/database/dbmem/dbmem.go
@@ -3283,7 +3283,6 @@ func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context,
}
workspaceBuildStats = append(workspaceBuildStats, database.GetFailedWorkspaceBuildsByTemplateIDRow{
- WorkspaceID: w.ID,
WorkspaceName: w.Name,
WorkspaceOwnerUsername: workspaceOwner.Username,
TemplateVersionName: templateVersion.Name,
diff --git a/coderd/database/migrations/000316_group_build_failure_notifications.down.sql b/coderd/database/migrations/000316_group_build_failure_notifications.down.sql
deleted file mode 100644
index 3ea2e98ff19e1..0000000000000
--- a/coderd/database/migrations/000316_group_build_failure_notifications.down.sql
+++ /dev/null
@@ -1,21 +0,0 @@
-UPDATE notification_templates
-SET
- name = 'Report: Workspace Builds Failed For Template',
- title_template = E'Workspace builds failed for template "{{.Labels.template_display_name}}"',
- body_template = E'Template **{{.Labels.template_display_name}}** has failed to build {{.Data.failed_builds}}/{{.Data.total_builds}} times over the last {{.Data.report_frequency}}.
-
-**Report:**
-{{range $version := .Data.template_versions}}
-**{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1.0}}s{{end}}:
-{{range $build := $version.failed_builds}}
-* [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}})
-{{- end}}
-{{end}}
-We recommend reviewing these issues to ensure future builds are successful.',
- actions = '[
- {
- "label": "View workspaces",
- "url": "{{ base_url }}/workspaces?filter=template%3A{{.Labels.template_name}}"
- }
- ]'::jsonb
-WHERE id = '34a20db2-e9cc-4a93-b0e4-8569699d7a00';
diff --git a/coderd/database/migrations/000316_group_build_failure_notifications.up.sql b/coderd/database/migrations/000316_group_build_failure_notifications.up.sql
deleted file mode 100644
index e3c4e79fc6d35..0000000000000
--- a/coderd/database/migrations/000316_group_build_failure_notifications.up.sql
+++ /dev/null
@@ -1,29 +0,0 @@
-UPDATE notification_templates
-SET
- name = 'Report: Workspace Builds Failed',
- title_template = 'Failed workspace builds report',
- body_template =
-E'The following templates have had build failures over the last {{.Data.report_frequency}}:
-{{range $template := .Data.templates}}
-- **{{$template.display_name}}** failed to build {{$template.failed_builds}}/{{$template.total_builds}} times
-{{end}}
-
-**Report:**
-{{range $template := .Data.templates}}
-**{{$template.display_name}}**
-{{range $version := $template.versions}}
-- **{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1.0}}s{{end}}:
-{{range $build := $version.failed_builds}}
- - [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}})
-{{end}}
-{{end}}
-{{end}}
-
-We recommend reviewing these issues to ensure future builds are successful.',
- actions = '[
- {
- "label": "View workspaces",
- "url": "{{ base_url }}/workspaces?filter={{$first := true}}{{range $template := .Data.templates}}{{range $version := $template.versions}}{{range $build := $version.failed_builds}}{{if not $first}}+{{else}}{{$first = false}}{{end}}id%3A{{$build.workspace_id}}{{end}}{{end}}{{end}}"
- }
- ]'::jsonb
-WHERE id = '34a20db2-e9cc-4a93-b0e4-8569699d7a00';
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 4268e802fe4a2..81004abcd8a50 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -15779,7 +15779,6 @@ SELECT
tv.name AS template_version_name,
u.username AS workspace_owner_username,
w.name AS workspace_name,
- w.id AS workspace_id,
wb.build_number AS workspace_build_number
FROM
workspace_build_with_user AS wb
@@ -15818,11 +15817,10 @@ type GetFailedWorkspaceBuildsByTemplateIDParams struct {
}
type GetFailedWorkspaceBuildsByTemplateIDRow struct {
- TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
- WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"`
- WorkspaceName string `db:"workspace_name" json:"workspace_name"`
- WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
- WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"`
+ TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
+ WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"`
+ WorkspaceName string `db:"workspace_name" json:"workspace_name"`
+ WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"`
}
func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) {
@@ -15838,7 +15836,6 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a
&i.TemplateVersionName,
&i.WorkspaceOwnerUsername,
&i.WorkspaceName,
- &i.WorkspaceID,
&i.WorkspaceBuildNumber,
); err != nil {
return nil, err
diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql
index 34ef639a1694b..da349fa1441b3 100644
--- a/coderd/database/queries/workspacebuilds.sql
+++ b/coderd/database/queries/workspacebuilds.sql
@@ -213,7 +213,6 @@ SELECT
tv.name AS template_version_name,
u.username AS workspace_owner_username,
w.name AS workspace_name,
- w.id AS workspace_id,
wb.build_number AS workspace_build_number
FROM
workspace_build_with_user AS wb
diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go
index e9cb3e413aee5..9bf31384234ed 100644
--- a/coderd/notifications/notifications_test.go
+++ b/coderd/notifications/notifications_test.go
@@ -978,102 +978,45 @@ func TestNotificationTemplates_Golden(t *testing.T) {
UserName: "Bobby",
UserEmail: "bobby@coder.com",
UserUsername: "bobby",
- Labels: map[string]string{},
+ Labels: map[string]string{
+ "template_name": "bobby-first-template",
+ "template_display_name": "Bobby First Template",
+ },
// We need to use floats as `json.Unmarshal` unmarshal numbers in `map[string]any` to floats.
Data: map[string]any{
+ "failed_builds": 4.0,
+ "total_builds": 55.0,
"report_frequency": "week",
- "templates": []map[string]any{
+ "template_versions": []map[string]any{
{
- "name": "bobby-first-template",
- "display_name": "Bobby First Template",
- "failed_builds": 4.0,
- "total_builds": 55.0,
- "versions": []map[string]any{
+ "template_version_name": "bobby-template-version-1",
+ "failed_count": 3.0,
+ "failed_builds": []map[string]any{
{
- "template_version_name": "bobby-template-version-1",
- "failed_count": 3.0,
- "failed_builds": []map[string]any{
- {
- "workspace_owner_username": "mtojek",
- "workspace_name": "workspace-1",
- "workspace_id": "24f5bd8f-1566-4374-9734-c3efa0454dc7",
- "build_number": 1234.0,
- },
- {
- "workspace_owner_username": "johndoe",
- "workspace_name": "my-workspace-3",
- "workspace_id": "372a194b-dcde-43f1-b7cf-8a2f3d3114a0",
- "build_number": 5678.0,
- },
- {
- "workspace_owner_username": "jack",
- "workspace_name": "workwork",
- "workspace_id": "1386d294-19c1-4351-89e2-6cae1afb9bfe",
- "build_number": 774.0,
- },
- },
+ "workspace_owner_username": "mtojek",
+ "workspace_name": "workspace-1",
+ "build_number": 1234.0,
},
{
- "template_version_name": "bobby-template-version-2",
- "failed_count": 1.0,
- "failed_builds": []map[string]any{
- {
- "workspace_owner_username": "ben",
- "workspace_name": "cool-workspace",
- "workspace_id": "86fd99b1-1b6e-4b7e-b58e-0aee6e35c159",
- "build_number": 8888.0,
- },
- },
+ "workspace_owner_username": "johndoe",
+ "workspace_name": "my-workspace-3",
+ "build_number": 5678.0,
+ },
+ {
+ "workspace_owner_username": "jack",
+ "workspace_name": "workwork",
+ "build_number": 774.0,
},
},
},
{
- "name": "bobby-second-template",
- "display_name": "Bobby Second Template",
- "failed_builds": 5.0,
- "total_builds": 50.0,
- "versions": []map[string]any{
- {
- "template_version_name": "bobby-template-version-1",
- "failed_count": 3.0,
- "failed_builds": []map[string]any{
- {
- "workspace_owner_username": "daniellemaywood",
- "workspace_name": "workspace-9",
- "workspace_id": "cd469690-b6eb-4123-b759-980be7a7b278",
- "build_number": 9234.0,
- },
- {
- "workspace_owner_username": "johndoe",
- "workspace_name": "my-workspace-7",
- "workspace_id": "c447d472-0800-4529-a836-788754d5e27d",
- "build_number": 8678.0,
- },
- {
- "workspace_owner_username": "jack",
- "workspace_name": "workworkwork",
- "workspace_id": "919db6df-48f0-4dc1-b357-9036a2c40f86",
- "build_number": 374.0,
- },
- },
- },
+ "template_version_name": "bobby-template-version-2",
+ "failed_count": 1.0,
+ "failed_builds": []map[string]any{
{
- "template_version_name": "bobby-template-version-2",
- "failed_count": 2.0,
- "failed_builds": []map[string]any{
- {
- "workspace_owner_username": "ben",
- "workspace_name": "more-cool-workspace",
- "workspace_id": "c8fb0652-9290-4bf2-a711-71b910243ac2",
- "build_number": 8878.0,
- },
- {
- "workspace_owner_username": "ben",
- "workspace_name": "less-cool-workspace",
- "workspace_id": "703d718d-2234-4990-9a02-5b1df6cf462a",
- "build_number": 8848.0,
- },
- },
+ "workspace_owner_username": "ben",
+ "workspace_name": "cool-workspace",
+ "build_number": 8888.0,
},
},
},
diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go
index 6b7dbd0c5b7b9..2424498146c60 100644
--- a/coderd/notifications/reports/generator.go
+++ b/coderd/notifications/reports/generator.go
@@ -18,7 +18,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/notifications"
- "github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
)
@@ -103,11 +102,6 @@ const (
failedWorkspaceBuildsReportFrequencyLabel = "week"
)
-type adminReport struct {
- stats database.GetWorkspaceBuildStatsByTemplatesRow
- failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow
-}
-
func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error {
now := clk.Now()
since := now.Add(-failedWorkspaceBuildsReportFrequency)
@@ -142,8 +136,6 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
return xerrors.Errorf("unable to fetch failed workspace builds: %w", err)
}
- reports := make(map[uuid.UUID][]adminReport)
-
for _, stats := range templateStatsRows {
select {
case <-ctx.Done():
@@ -173,40 +165,33 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", stats.TemplateID), slog.Error(err))
continue
}
+ reportData := buildDataForReportFailedWorkspaceBuilds(stats, failedBuilds)
- for _, templateAdmin := range templateAdmins {
- adminReports := reports[templateAdmin.ID]
- adminReports = append(adminReports, adminReport{
- failedBuilds: failedBuilds,
- stats: stats,
- })
-
- reports[templateAdmin.ID] = adminReports
- }
- }
-
- for templateAdmin, reports := range reports {
- select {
- case <-ctx.Done():
- logger.Debug(ctx, "context is canceled, quitting", slog.Error(ctx.Err()))
- break
- default:
+ // Send reports to template admins
+ templateDisplayName := stats.TemplateDisplayName
+ if templateDisplayName == "" {
+ templateDisplayName = stats.TemplateName
}
- reportData := buildDataForReportFailedWorkspaceBuilds(reports)
-
- targets := []uuid.UUID{}
- for _, report := range reports {
- targets = append(targets, report.stats.TemplateID, report.stats.TemplateOrganizationID)
- }
+ for _, templateAdmin := range templateAdmins {
+ select {
+ case <-ctx.Done():
+ logger.Debug(ctx, "context is canceled, quitting", slog.Error(ctx.Err()))
+ break
+ default:
+ }
- if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin, notifications.TemplateWorkspaceBuildsFailedReport,
- map[string]string{},
- reportData,
- "report_generator",
- slice.Unique(targets)...,
- ); err != nil {
- logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err))
+ if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport,
+ map[string]string{
+ "template_name": stats.TemplateName,
+ "template_display_name": templateDisplayName,
+ },
+ reportData,
+ "report_generator",
+ stats.TemplateID, stats.TemplateOrganizationID,
+ ); err != nil {
+ logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err))
+ }
}
}
@@ -228,71 +213,54 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat
const workspaceBuildsLimitPerTemplateVersion = 10
-func buildDataForReportFailedWorkspaceBuilds(reports []adminReport) map[string]any {
- templates := []map[string]any{}
-
- for _, report := range reports {
- // Build notification model for template versions and failed workspace builds.
- //
- // Failed builds are sorted by template version ascending, workspace build number descending.
- // Review builds, group them by template versions, and assign to builds to template versions.
- // The map requires `[]map[string]any{}` to be compatible with data passed to `NotificationEnqueuer`.
- templateVersions := []map[string]any{}
- for _, failedBuild := range report.failedBuilds {
- c := len(templateVersions)
-
- if c == 0 || templateVersions[c-1]["template_version_name"] != failedBuild.TemplateVersionName {
- templateVersions = append(templateVersions, map[string]any{
- "template_version_name": failedBuild.TemplateVersionName,
- "failed_count": 1,
- "failed_builds": []map[string]any{
- {
- "workspace_owner_username": failedBuild.WorkspaceOwnerUsername,
- "workspace_name": failedBuild.WorkspaceName,
- "workspace_id": failedBuild.WorkspaceID,
- "build_number": failedBuild.WorkspaceBuildNumber,
- },
+func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any {
+ // Build notification model for template versions and failed workspace builds.
+ //
+ // Failed builds are sorted by template version ascending, workspace build number descending.
+ // Review builds, group them by template versions, and assign to builds to template versions.
+ // The map requires `[]map[string]any{}` to be compatible with data passed to `NotificationEnqueuer`.
+ templateVersions := []map[string]any{}
+ for _, failedBuild := range failedBuilds {
+ c := len(templateVersions)
+
+ if c == 0 || templateVersions[c-1]["template_version_name"] != failedBuild.TemplateVersionName {
+ templateVersions = append(templateVersions, map[string]any{
+ "template_version_name": failedBuild.TemplateVersionName,
+ "failed_count": 1,
+ "failed_builds": []map[string]any{
+ {
+ "workspace_owner_username": failedBuild.WorkspaceOwnerUsername,
+ "workspace_name": failedBuild.WorkspaceName,
+ "build_number": failedBuild.WorkspaceBuildNumber,
},
- })
- continue
- }
-
- tv := templateVersions[c-1]
- //nolint:errorlint,forcetypeassert // only this function prepares the notification model
- tv["failed_count"] = tv["failed_count"].(int) + 1
-
- //nolint:errorlint,forcetypeassert // only this function prepares the notification model
- builds := tv["failed_builds"].([]map[string]any)
- if len(builds) < workspaceBuildsLimitPerTemplateVersion {
- // return N last builds to prevent long email reports
- builds = append(builds, map[string]any{
- "workspace_owner_username": failedBuild.WorkspaceOwnerUsername,
- "workspace_name": failedBuild.WorkspaceName,
- "workspace_id": failedBuild.WorkspaceID,
- "build_number": failedBuild.WorkspaceBuildNumber,
- })
- tv["failed_builds"] = builds
- }
- templateVersions[c-1] = tv
+ },
+ })
+ continue
}
- templateDisplayName := report.stats.TemplateDisplayName
- if templateDisplayName == "" {
- templateDisplayName = report.stats.TemplateName
+ tv := templateVersions[c-1]
+ //nolint:errorlint,forcetypeassert // only this function prepares the notification model
+ tv["failed_count"] = tv["failed_count"].(int) + 1
+
+ //nolint:errorlint,forcetypeassert // only this function prepares the notification model
+ builds := tv["failed_builds"].([]map[string]any)
+ if len(builds) < workspaceBuildsLimitPerTemplateVersion {
+ // return N last builds to prevent long email reports
+ builds = append(builds, map[string]any{
+ "workspace_owner_username": failedBuild.WorkspaceOwnerUsername,
+ "workspace_name": failedBuild.WorkspaceName,
+ "build_number": failedBuild.WorkspaceBuildNumber,
+ })
+ tv["failed_builds"] = builds
}
-
- templates = append(templates, map[string]any{
- "failed_builds": report.stats.FailedBuilds,
- "total_builds": report.stats.TotalBuilds,
- "versions": templateVersions,
- "name": report.stats.TemplateName,
- "display_name": templateDisplayName,
- })
+ templateVersions[c-1] = tv
}
return map[string]any{
- "report_frequency": failedWorkspaceBuildsReportFrequencyLabel,
- "templates": templates,
+ "failed_builds": stats.FailedBuilds,
+ "total_builds": stats.TotalBuilds,
+ "report_frequency": failedWorkspaceBuildsReportFrequencyLabel,
+ "template_versions": templateVersions,
}
}
diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go
index c0215e4854e08..a4330493f0aed 100644
--- a/coderd/notifications/reports/generator_internal_test.go
+++ b/coderd/notifications/reports/generator_internal_test.go
@@ -3,7 +3,6 @@ package reports
import (
"context"
"database/sql"
- "sort"
"testing"
"time"
@@ -119,13 +118,17 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) {
t.Run("FailedBuilds_SecondRun_Report_ThirdRunTooEarly_NoReport_FourthRun_Report", func(t *testing.T) {
t.Parallel()
- verifyNotification := func(t *testing.T, recipientID uuid.UUID, notif *notificationstest.FakeNotification, templates []map[string]any) {
+ verifyNotification := func(t *testing.T, recipient database.User, notif *notificationstest.FakeNotification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) {
t.Helper()
- require.Equal(t, recipientID, notif.UserID)
+ require.Equal(t, recipient.ID, notif.UserID)
require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notif.TemplateID)
+ require.Equal(t, tmpl.Name, notif.Labels["template_name"])
+ require.Equal(t, tmpl.DisplayName, notif.Labels["template_display_name"])
+ require.Equal(t, failedBuilds, notif.Data["failed_builds"])
+ require.Equal(t, totalBuilds, notif.Data["total_builds"])
require.Equal(t, "week", notif.Data["report_frequency"])
- require.Equal(t, templates, notif.Data["templates"])
+ require.Equal(t, templateVersions, notif.Data["template_versions"])
}
// Setup
@@ -209,65 +212,43 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) {
require.NoError(t, err)
sent := notifEnq.Sent()
- require.Len(t, sent, 2) // 2 templates, 2 template admins
-
- templateAdmins := []uuid.UUID{templateAdmin1.ID, templateAdmin2.ID}
-
- // Ensure consistent order for tests
- sort.Slice(templateAdmins, func(i, j int) bool {
- return templateAdmins[i].String() < templateAdmins[j].String()
- })
- sort.Slice(sent, func(i, j int) bool {
- return sent[i].UserID.String() < sent[j].UserID.String()
- })
+ require.Len(t, sent, 4) // 2 templates, 2 template admins
+ for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} {
+ verifyNotification(t, templateAdmin, sent[i], t1, 3, 4, []map[string]interface{}{
+ {
+ "failed_builds": []map[string]interface{}{
+ {"build_number": int32(7), "workspace_name": w3.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(1), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ },
+ "failed_count": 2,
+ "template_version_name": t1v1.Name,
+ },
+ {
+ "failed_builds": []map[string]interface{}{
+ {"build_number": int32(3), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ },
+ "failed_count": 1,
+ "template_version_name": t1v2.Name,
+ },
+ })
+ }
- for i, templateAdmin := range templateAdmins {
- verifyNotification(t, templateAdmin, sent[i], []map[string]any{
+ for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} {
+ verifyNotification(t, templateAdmin, sent[i+2], t2, 3, 5, []map[string]interface{}{
{
- "name": t1.Name,
- "display_name": t1.DisplayName,
- "failed_builds": int64(3),
- "total_builds": int64(4),
- "versions": []map[string]any{
- {
- "failed_builds": []map[string]any{
- {"build_number": int32(7), "workspace_name": w3.Name, "workspace_id": w3.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(1), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- },
- "failed_count": 2,
- "template_version_name": t1v1.Name,
- },
- {
- "failed_builds": []map[string]any{
- {"build_number": int32(3), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- },
- "failed_count": 1,
- "template_version_name": t1v2.Name,
- },
+ "failed_builds": []map[string]interface{}{
+ {"build_number": int32(8), "workspace_name": w4.Name, "workspace_owner_username": user2.Username},
},
+ "failed_count": 1,
+ "template_version_name": t2v1.Name,
},
{
- "name": t2.Name,
- "display_name": t2.DisplayName,
- "failed_builds": int64(3),
- "total_builds": int64(5),
- "versions": []map[string]any{
- {
- "failed_builds": []map[string]any{
- {"build_number": int32(8), "workspace_name": w4.Name, "workspace_id": w4.ID, "workspace_owner_username": user2.Username},
- },
- "failed_count": 1,
- "template_version_name": t2v1.Name,
- },
- {
- "failed_builds": []map[string]any{
- {"build_number": int32(6), "workspace_name": w2.Name, "workspace_id": w2.ID, "workspace_owner_username": user2.Username},
- {"build_number": int32(5), "workspace_name": w2.Name, "workspace_id": w2.ID, "workspace_owner_username": user2.Username},
- },
- "failed_count": 2,
- "template_version_name": t2v2.Name,
- },
+ "failed_builds": []map[string]interface{}{
+ {"build_number": int32(6), "workspace_name": w2.Name, "workspace_owner_username": user2.Username},
+ {"build_number": int32(5), "workspace_name": w2.Name, "workspace_owner_username": user2.Username},
},
+ "failed_count": 2,
+ "template_version_name": t2v2.Name,
},
})
}
@@ -298,33 +279,14 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) {
// Then: we should see the failed job in the report
sent = notifEnq.Sent()
require.Len(t, sent, 2) // a new failed job should be reported
-
- templateAdmins = []uuid.UUID{templateAdmin1.ID, templateAdmin2.ID}
-
- // Ensure consistent order for tests
- sort.Slice(templateAdmins, func(i, j int) bool {
- return templateAdmins[i].String() < templateAdmins[j].String()
- })
- sort.Slice(sent, func(i, j int) bool {
- return sent[i].UserID.String() < sent[j].UserID.String()
- })
-
- for i, templateAdmin := range templateAdmins {
- verifyNotification(t, templateAdmin, sent[i], []map[string]any{
+ for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} {
+ verifyNotification(t, templateAdmin, sent[i], t1, 1, 1, []map[string]interface{}{
{
- "name": t1.Name,
- "display_name": t1.DisplayName,
- "failed_builds": int64(1),
- "total_builds": int64(1),
- "versions": []map[string]any{
- {
- "failed_builds": []map[string]any{
- {"build_number": int32(77), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- },
- "failed_count": 1,
- "template_version_name": t1v2.Name,
- },
+ "failed_builds": []map[string]interface{}{
+ {"build_number": int32(77), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
},
+ "failed_count": 1,
+ "template_version_name": t1v2.Name,
},
})
}
@@ -333,13 +295,17 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) {
t.Run("TooManyFailedBuilds_SecondRun_Report", func(t *testing.T) {
t.Parallel()
- verifyNotification := func(t *testing.T, recipient database.User, notif *notificationstest.FakeNotification, templates []map[string]any) {
+ verifyNotification := func(t *testing.T, recipient database.User, notif *notificationstest.FakeNotification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) {
t.Helper()
require.Equal(t, recipient.ID, notif.UserID)
require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notif.TemplateID)
+ require.Equal(t, tmpl.Name, notif.Labels["template_name"])
+ require.Equal(t, tmpl.DisplayName, notif.Labels["template_display_name"])
+ require.Equal(t, failedBuilds, notif.Data["failed_builds"])
+ require.Equal(t, totalBuilds, notif.Data["total_builds"])
require.Equal(t, "week", notif.Data["report_frequency"])
- require.Equal(t, templates, notif.Data["templates"])
+ require.Equal(t, templateVersions, notif.Data["template_versions"])
}
// Setup
@@ -403,46 +369,38 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) {
sent := notifEnq.Sent()
require.Len(t, sent, 1) // 1 template, 1 template admin
- verifyNotification(t, templateAdmin1, sent[0], []map[string]any{
+ verifyNotification(t, templateAdmin1, sent[0], t1, 46, 47, []map[string]interface{}{
{
- "name": t1.Name,
- "display_name": t1.DisplayName,
- "failed_builds": int64(46),
- "total_builds": int64(47),
- "versions": []map[string]any{
- {
- "failed_builds": []map[string]any{
- {"build_number": int32(23), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(22), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(21), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(20), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(19), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(18), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(17), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(16), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(15), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(14), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- },
- "failed_count": 23,
- "template_version_name": t1v1.Name,
- },
- {
- "failed_builds": []map[string]any{
- {"build_number": int32(123), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(122), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(121), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(120), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(119), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(118), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(117), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(116), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(115), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- {"build_number": int32(114), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username},
- },
- "failed_count": 23,
- "template_version_name": t1v2.Name,
- },
+ "failed_builds": []map[string]interface{}{
+ {"build_number": int32(23), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(22), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(21), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(20), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(19), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(18), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(17), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(16), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(15), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(14), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ },
+ "failed_count": 23,
+ "template_version_name": t1v1.Name,
+ },
+ {
+ "failed_builds": []map[string]interface{}{
+ {"build_number": int32(123), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(122), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(121), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(120), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(119), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(118), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(117), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(116), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(115), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
+ {"build_number": int32(114), "workspace_name": w1.Name, "workspace_owner_username": user1.Username},
},
+ "failed_count": 23,
+ "template_version_name": t1v2.Name,
},
})
})
diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden
index 9699486bf9cc8..f3edc6ac05d02 100644
--- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden
+++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden
@@ -1,6 +1,6 @@
From: system@coder.com
To: bobby@coder.com
-Subject: Failed workspace builds report
+Subject: Workspace builds failed for template "Bobby First Template"
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
Date: Fri, 11 Oct 2024 09:03:06 +0000
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
@@ -12,51 +12,29 @@ Content-Type: text/plain; charset=UTF-8
Hi Bobby,
-The following templates have had build failures over the last week:
-
-Bobby First Template failed to build 4/55 times
-Bobby Second Template failed to build 5/50 times
+Template Bobby First Template has failed to build 4/55 times over the last =
+week.
Report:
-Bobby First Template
-
bobby-template-version-1 failed 3 times:
- mtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/build=
-s/1234)
- johndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace=
--3/builds/5678)
- jack / workwork / #774 (http://test.com/@jack/workwork/builds/774)
-bobby-template-version-2 failed 1 time:
- ben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/build=
-s/8888)
-
-Bobby Second Template
+mtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/builds/12=
+34)
+johndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace-3/b=
+uilds/5678)
+jack / workwork / #774 (http://test.com/@jack/workwork/builds/774)
-bobby-template-version-1 failed 3 times:
- daniellemaywood / workspace-9 / #9234 (http://test.com/@daniellemaywood=
-/workspace-9/builds/9234)
- johndoe / my-workspace-7 / #8678 (http://test.com/@johndoe/my-workspace=
--7/builds/8678)
- jack / workworkwork / #374 (http://test.com/@jack/workworkwork/builds/3=
-74)
-bobby-template-version-2 failed 2 times:
- ben / more-cool-workspace / #8878 (http://test.com/@ben/more-cool-works=
-pace/builds/8878)
- ben / less-cool-workspace / #8848 (http://test.com/@ben/less-cool-works=
-pace/builds/8848)
+bobby-template-version-2 failed 1 time:
+ben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/builds/88=
+88)
We recommend reviewing these issues to ensure future builds are successful.
-View workspaces: http://test.com/workspaces?filter=3Did%3A24f5bd8f-1566-437=
-4-9734-c3efa0454dc7+id%3A372a194b-dcde-43f1-b7cf-8a2f3d3114a0+id%3A1386d294=
--19c1-4351-89e2-6cae1afb9bfe+id%3A86fd99b1-1b6e-4b7e-b58e-0aee6e35c159+id%3=
-Acd469690-b6eb-4123-b759-980be7a7b278+id%3Ac447d472-0800-4529-a836-788754d5=
-e27d+id%3A919db6df-48f0-4dc1-b357-9036a2c40f86+id%3Ac8fb0652-9290-4bf2-a711=
--71b910243ac2+id%3A703d718d-2234-4990-9a02-5b1df6cf462a
+View workspaces: http://test.com/workspaces?filter=3Dtemplate%3Abobby-first=
+-template
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
Content-Transfer-Encoding: quoted-printable
@@ -68,7 +46,8 @@ Content-Type: text/html; charset=UTF-8
- Failed workspace builds report
+ Workspace builds failed for template "Bobby First Template"
- Failed workspace builds report
+ Workspace builds failed for template "Bobby First Template"
Hi Bobby,
-
The following templates have had build failures over the last we=
-ek:
-
-
+
Template Bobby First Template has failed to bui=
+ld 4⁄55 times over the last week.
Report:
-
Bobby First Template
-
-
-
bobby-template-version-2 failed 1 time:
+bobby-template-version-2 failed 1 time:
-
-
-
Bobby Second Template
-
-
We recommend reviewing these issues to ensure future builds are successf=
@@ -157,14 +98,10 @@ ul.
=20
-
+
View workspaces
=20
diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden
index 78c8ba2a3195c..987d97b91c029 100644
--- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden
+++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden
@@ -3,7 +3,7 @@
"msg_id": "00000000-0000-0000-0000-000000000000",
"payload": {
"_version": "1.2",
- "notification_name": "Report: Workspace Builds Failed",
+ "notification_name": "Report: Workspace Builds Failed For Template",
"notification_template_id": "00000000-0000-0000-0000-000000000000",
"user_id": "00000000-0000-0000-0000-000000000000",
"user_email": "bobby@coder.com",
@@ -12,113 +12,56 @@
"actions": [
{
"label": "View workspaces",
- "url": "http://test.com/workspaces?filter=id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000"
+ "url": "http://test.com/workspaces?filter=template%3Abobby-first-template"
}
],
- "labels": {},
+ "labels": {
+ "template_display_name": "Bobby First Template",
+ "template_name": "bobby-first-template"
+ },
"data": {
+ "failed_builds": 4,
"report_frequency": "week",
- "templates": [
+ "template_versions": [
{
- "display_name": "Bobby First Template",
- "failed_builds": 4,
- "name": "bobby-first-template",
- "total_builds": 55,
- "versions": [
+ "failed_builds": [
+ {
+ "build_number": 1234,
+ "workspace_name": "workspace-1",
+ "workspace_owner_username": "mtojek"
+ },
{
- "failed_builds": [
- {
- "build_number": 1234,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "workspace-1",
- "workspace_owner_username": "mtojek"
- },
- {
- "build_number": 5678,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "my-workspace-3",
- "workspace_owner_username": "johndoe"
- },
- {
- "build_number": 774,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "workwork",
- "workspace_owner_username": "jack"
- }
- ],
- "failed_count": 3,
- "template_version_name": "bobby-template-version-1"
+ "build_number": 5678,
+ "workspace_name": "my-workspace-3",
+ "workspace_owner_username": "johndoe"
},
{
- "failed_builds": [
- {
- "build_number": 8888,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "cool-workspace",
- "workspace_owner_username": "ben"
- }
- ],
- "failed_count": 1,
- "template_version_name": "bobby-template-version-2"
+ "build_number": 774,
+ "workspace_name": "workwork",
+ "workspace_owner_username": "jack"
}
- ]
+ ],
+ "failed_count": 3,
+ "template_version_name": "bobby-template-version-1"
},
{
- "display_name": "Bobby Second Template",
- "failed_builds": 5,
- "name": "bobby-second-template",
- "total_builds": 50,
- "versions": [
- {
- "failed_builds": [
- {
- "build_number": 9234,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "workspace-9",
- "workspace_owner_username": "daniellemaywood"
- },
- {
- "build_number": 8678,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "my-workspace-7",
- "workspace_owner_username": "johndoe"
- },
- {
- "build_number": 374,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "workworkwork",
- "workspace_owner_username": "jack"
- }
- ],
- "failed_count": 3,
- "template_version_name": "bobby-template-version-1"
- },
+ "failed_builds": [
{
- "failed_builds": [
- {
- "build_number": 8878,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "more-cool-workspace",
- "workspace_owner_username": "ben"
- },
- {
- "build_number": 8848,
- "workspace_id": "00000000-0000-0000-0000-000000000000",
- "workspace_name": "less-cool-workspace",
- "workspace_owner_username": "ben"
- }
- ],
- "failed_count": 2,
- "template_version_name": "bobby-template-version-2"
+ "build_number": 8888,
+ "workspace_name": "cool-workspace",
+ "workspace_owner_username": "ben"
}
- ]
+ ],
+ "failed_count": 1,
+ "template_version_name": "bobby-template-version-2"
}
- ]
+ ],
+ "total_builds": 55
},
"targets": null
},
- "title": "Failed workspace builds report",
- "title_markdown": "Failed workspace builds report",
- "body": "The following templates have had build failures over the last week:\n\nBobby First Template failed to build 4/55 times\nBobby Second Template failed to build 5/50 times\n\nReport:\n\nBobby First Template\n\nbobby-template-version-1 failed 3 times:\n mtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/builds/1234)\n johndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace-3/builds/5678)\n jack / workwork / #774 (http://test.com/@jack/workwork/builds/774)\nbobby-template-version-2 failed 1 time:\n ben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/builds/8888)\n\n\nBobby Second Template\n\nbobby-template-version-1 failed 3 times:\n daniellemaywood / workspace-9 / #9234 (http://test.com/@daniellemaywood/workspace-9/builds/9234)\n johndoe / my-workspace-7 / #8678 (http://test.com/@johndoe/my-workspace-7/builds/8678)\n jack / workworkwork / #374 (http://test.com/@jack/workworkwork/builds/374)\nbobby-template-version-2 failed 2 times:\n ben / more-cool-workspace / #8878 (http://test.com/@ben/more-cool-workspace/builds/8878)\n ben / less-cool-workspace / #8848 (http://test.com/@ben/less-cool-workspace/builds/8848)\n\n\nWe recommend reviewing these issues to ensure future builds are successful.",
- "body_markdown": "The following templates have had build failures over the last week:\n\n- **Bobby First Template** failed to build 4/55 times\n\n- **Bobby Second Template** failed to build 5/50 times\n\n\n**Report:**\n\n**Bobby First Template**\n\n- **bobby-template-version-1** failed 3 times:\n\n - [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234)\n\n - [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678)\n\n - [jack / workwork / #774](http://test.com/@jack/workwork/builds/774)\n\n\n- **bobby-template-version-2** failed 1 time:\n\n - [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888)\n\n\n\n**Bobby Second Template**\n\n- **bobby-template-version-1** failed 3 times:\n\n - [daniellemaywood / workspace-9 / #9234](http://test.com/@daniellemaywood/workspace-9/builds/9234)\n\n - [johndoe / my-workspace-7 / #8678](http://test.com/@johndoe/my-workspace-7/builds/8678)\n\n - [jack / workworkwork / #374](http://test.com/@jack/workworkwork/builds/374)\n\n\n- **bobby-template-version-2** failed 2 times:\n\n - [ben / more-cool-workspace / #8878](http://test.com/@ben/more-cool-workspace/builds/8878)\n\n - [ben / less-cool-workspace / #8848](http://test.com/@ben/less-cool-workspace/builds/8848)\n\n\n\n\nWe recommend reviewing these issues to ensure future builds are successful."
+ "title": "Workspace builds failed for template \"Bobby First Template\"",
+ "title_markdown": "Workspace builds failed for template \"Bobby First Template\"",
+ "body": "Template Bobby First Template has failed to build 4/55 times over the last week.\n\nReport:\n\nbobby-template-version-1 failed 3 times:\n\nmtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/builds/1234)\njohndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace-3/builds/5678)\njack / workwork / #774 (http://test.com/@jack/workwork/builds/774)\n\nbobby-template-version-2 failed 1 time:\n\nben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/builds/8888)\n\nWe recommend reviewing these issues to ensure future builds are successful.",
+ "body_markdown": "Template **Bobby First Template** has failed to build 4/55 times over the last week.\n\n**Report:**\n\n**bobby-template-version-1** failed 3 times:\n\n* [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234)\n* [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678)\n* [jack / workwork / #774](http://test.com/@jack/workwork/builds/774)\n\n**bobby-template-version-2** failed 1 time:\n\n* [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888)\n\nWe recommend reviewing these issues to ensure future builds are successful."
}
\ No newline at end of file