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"</tit= +le> </head> <body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-= ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel= @@ -83,73 +62,35 @@ er Logo" style=3D"height: 40px;" /> </div> <h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m= argin: 8px 0 32px; line-height: 1.5;"> - Failed workspace builds report + Workspace builds failed for template "Bobby First Template" </h1> <div style=3D"line-height: 1.5;"> <p>Hi Bobby,</p> - <p>The following templates have had build failures over the last we= -ek:</p> - -<ul> -<li><p><strong>Bobby First Template</strong> failed to build <sup>4</sup>&f= -rasl;<sub>55</sub> times</p></li> - -<li><p><strong>Bobby Second Template</strong> failed to build <sup>5</sup>&= -frasl;<sub>50</sub> times</p></li> -</ul> + <p>Template <strong>Bobby First Template</strong> has failed to bui= +ld <sup>4</sup>⁄<sub>55</sub> times over the last week.</p> <p><strong>Report:</strong></p> -<p><strong>Bobby First Template</strong></p> - -<ul> -<li><p><strong>bobby-template-version-1</strong> failed 3 times:</p> +<p><strong>bobby-template-version-1</strong> failed 3 times:</p> <ul> -<li><p><a href=3D"http://test.com/@mtojek/workspace-1/builds/1234">mtojek /= - workspace-1 / #1234</a></p></li> - -<li><p><a href=3D"http://test.com/@johndoe/my-workspace-3/builds/5678">john= -doe / my-workspace-3 / #5678</a></p></li> - -<li><p><a href=3D"http://test.com/@jack/workwork/builds/774">jack / workwor= -k / #774</a></p></li> -</ul></li> +<li><a href=3D"http://test.com/@mtojek/workspace-1/builds/1234">mtojek / wo= +rkspace-1 / #1234</a><br> +</li> +<li><a href=3D"http://test.com/@johndoe/my-workspace-3/builds/5678">johndoe= + / my-workspace-3 / #5678</a><br> +</li> +<li><a href=3D"http://test.com/@jack/workwork/builds/774">jack / workwork /= + #774</a><br> +</li> +</ul> -<li><p><strong>bobby-template-version-2</strong> failed 1 time:</p> +<p><strong>bobby-template-version-2</strong> failed 1 time:</p> <ul> <li><a href=3D"http://test.com/@ben/cool-workspace/builds/8888">ben / cool-= workspace / #8888</a><br> </li> -</ul></li> -</ul> - -<p><strong>Bobby Second Template</strong></p> - -<ul> -<li><p><strong>bobby-template-version-1</strong> failed 3 times:</p> - -<ul> -<li><p><a href=3D"http://test.com/@daniellemaywood/workspace-9/builds/9234"= ->daniellemaywood / workspace-9 / #9234</a></p></li> - -<li><p><a href=3D"http://test.com/@johndoe/my-workspace-7/builds/8678">john= -doe / my-workspace-7 / #8678</a></p></li> - -<li><p><a href=3D"http://test.com/@jack/workworkwork/builds/374">jack / wor= -kworkwork / #374</a></p></li> -</ul></li> - -<li><p><strong>bobby-template-version-2</strong> failed 2 times:</p> - -<ul> -<li><p><a href=3D"http://test.com/@ben/more-cool-workspace/builds/8878">ben= - / more-cool-workspace / #8878</a></p></li> - -<li><p><a href=3D"http://test.com/@ben/less-cool-workspace/builds/8848">ben= - / less-cool-workspace / #8848</a></p></li> -</ul></li> </ul> <p>We recommend reviewing these issues to ensure future builds are successf= @@ -157,14 +98,10 @@ ul.</p> </div> <div style=3D"text-align: center; margin-top: 32px;"> =20 - <a href=3D"http://test.com/workspaces?filter=3Did%3A24f5bd8f-1566-4= -374-9734-c3efa0454dc7+id%3A372a194b-dcde-43f1-b7cf-8a2f3d3114a0+id%3A1386d2= -94-19c1-4351-89e2-6cae1afb9bfe+id%3A86fd99b1-1b6e-4b7e-b58e-0aee6e35c159+id= -%3Acd469690-b6eb-4123-b759-980be7a7b278+id%3Ac447d472-0800-4529-a836-788754= -d5e27d+id%3A919db6df-48f0-4dc1-b357-9036a2c40f86+id%3Ac8fb0652-9290-4bf2-a7= -11-71b910243ac2+id%3A703d718d-2234-4990-9a02-5b1df6cf462a" style=3D"display= -: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fa= -fc; text-decoration: none; border-radius: 8px; margin: 0 4px;"> + <a href=3D"http://test.com/workspaces?filter=3Dtemplate%3Abobby-fir= +st-template" style=3D"display: inline-block; padding: 13px 24px; background= +-color: #020617; color: #f8fafc; text-decoration: none; border-radius: 8px;= + margin: 0 4px;"> View workspaces </a> =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