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
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy