diff --git a/coderd/aitasks_test.go b/coderd/aitasks_test.go index 4172cccda6..646e8ab33f 100644 --- a/coderd/aitasks_test.go +++ b/coderd/aitasks_test.go @@ -1061,6 +1061,64 @@ func TestTasksNotification(t *testing.T) { notificationTemplate: notifications.TemplateTaskIdle, taskPrompt: "This is a very long task prompt that should be truncated to 160 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", }, + // Should send TemplateTaskCompleted when the AI task transitions to 'Complete'. + { + name: "TemplateTaskCompleted", + latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateWorking}, + newAppStatus: codersdk.WorkspaceAppStatusStateComplete, + isAITask: true, + isNotificationSent: true, + notificationTemplate: notifications.TemplateTaskCompleted, + taskPrompt: "TemplateTaskCompleted", + }, + // Should send TemplateTaskFailed when the AI task transitions to 'Failure'. + { + name: "TemplateTaskFailed", + latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateWorking}, + newAppStatus: codersdk.WorkspaceAppStatusStateFailure, + isAITask: true, + isNotificationSent: true, + notificationTemplate: notifications.TemplateTaskFailed, + taskPrompt: "TemplateTaskFailed", + }, + // Should send TemplateTaskCompleted when the AI task transitions from 'Idle' to 'Complete'. + { + name: "TemplateTaskCompletedFromIdle", + latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateIdle}, + newAppStatus: codersdk.WorkspaceAppStatusStateComplete, + isAITask: true, + isNotificationSent: true, + notificationTemplate: notifications.TemplateTaskCompleted, + taskPrompt: "TemplateTaskCompletedFromIdle", + }, + // Should send TemplateTaskFailed when the AI task transitions from 'Idle' to 'Failure'. + { + name: "TemplateTaskFailedFromIdle", + latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateIdle}, + newAppStatus: codersdk.WorkspaceAppStatusStateFailure, + isAITask: true, + isNotificationSent: true, + notificationTemplate: notifications.TemplateTaskFailed, + taskPrompt: "TemplateTaskFailedFromIdle", + }, + // Should NOT send notification when transitioning from 'Complete' to 'Complete' (no change). + { + name: "NoNotificationCompleteToComplete", + latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateComplete}, + newAppStatus: codersdk.WorkspaceAppStatusStateComplete, + isAITask: true, + isNotificationSent: false, + taskPrompt: "NoNotificationCompleteToComplete", + }, + // Should NOT send notification when transitioning from 'Failure' to 'Failure' (no change). + { + name: "NoNotificationFailureToFailure", + latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateFailure}, + newAppStatus: codersdk.WorkspaceAppStatusStateFailure, + isAITask: true, + isNotificationSent: false, + taskPrompt: "NoNotificationFailureToFailure", + }, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/coderd/database/migrations/000383_add_task_completed_failed_notification_templates.down.sql b/coderd/database/migrations/000383_add_task_completed_failed_notification_templates.down.sql new file mode 100644 index 0000000000..9a87362653 --- /dev/null +++ b/coderd/database/migrations/000383_add_task_completed_failed_notification_templates.down.sql @@ -0,0 +1,5 @@ +-- Remove Task 'completed' transition template notification +DELETE FROM notification_templates WHERE id = '8c5a4d12-9f7e-4b3a-a1c8-6e4f2d9b5a7c'; + +-- Remove Task 'failed' transition template notification +DELETE FROM notification_templates WHERE id = '3b7e8f1a-4c2d-49a6-b5e9-7f3a1c8d6b4e'; diff --git a/coderd/database/migrations/000383_add_task_completed_failed_notification_templates.up.sql b/coderd/database/migrations/000383_add_task_completed_failed_notification_templates.up.sql new file mode 100644 index 0000000000..a9d6b01103 --- /dev/null +++ b/coderd/database/migrations/000383_add_task_completed_failed_notification_templates.up.sql @@ -0,0 +1,63 @@ +-- Task transition to 'complete' status +INSERT INTO notification_templates ( + id, + name, + title_template, + body_template, + actions, + "group", + method, + kind, + enabled_by_default +) VALUES ( + '8c5a4d12-9f7e-4b3a-a1c8-6e4f2d9b5a7c', + 'Task Completed', + E'Task ''{{.Labels.workspace}}'' completed', + E'The task ''{{.Labels.task}}'' has completed successfully.', + '[ + { + "label": "View task", + "url": "{{base_url}}/tasks/{{.UserUsername}}/{{.Labels.workspace}}" + }, + { + "label": "View workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + } + ]'::jsonb, + 'Task Events', + NULL, + 'system'::notification_template_kind, + true + ); + +-- Task transition to 'failed' status +INSERT INTO notification_templates ( + id, + name, + title_template, + body_template, + actions, + "group", + method, + kind, + enabled_by_default +) VALUES ( + '3b7e8f1a-4c2d-49a6-b5e9-7f3a1c8d6b4e', + 'Task Failed', + E'Task ''{{.Labels.workspace}}'' failed', + E'The task ''{{.Labels.task}}'' has failed. Check the logs for more details.', + '[ + { + "label": "View task", + "url": "{{base_url}}/tasks/{{.UserUsername}}/{{.Labels.workspace}}" + }, + { + "label": "View workspace", + "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}" + } + ]'::jsonb, + 'Task Events', + NULL, + 'system'::notification_template_kind, + true + ); diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 12adcfbb08..83e8e990a3 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -55,6 +55,8 @@ var ( // Task-related events. var ( - TemplateTaskWorking = uuid.MustParse("bd4b7168-d05e-4e19-ad0f-3593b77aa90f") - TemplateTaskIdle = uuid.MustParse("d4a6271c-cced-4ed0-84ad-afd02a9c7799") + TemplateTaskWorking = uuid.MustParse("bd4b7168-d05e-4e19-ad0f-3593b77aa90f") + TemplateTaskIdle = uuid.MustParse("d4a6271c-cced-4ed0-84ad-afd02a9c7799") + TemplateTaskCompleted = uuid.MustParse("8c5a4d12-9f7e-4b3a-a1c8-6e4f2d9b5a7c") + TemplateTaskFailed = uuid.MustParse("3b7e8f1a-4c2d-49a6-b5e9-7f3a1c8d6b4e") ) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 9689e6467d..d3d9ff2b85 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -1301,6 +1301,34 @@ func TestNotificationTemplates_Golden(t *testing.T) { Data: map[string]any{}, }, }, + { + name: "TemplateTaskCompleted", + id: notifications.TemplateTaskCompleted, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "task": "my-task", + "workspace": "my-workspace", + }, + Data: map[string]any{}, + }, + }, + { + name: "TemplateTaskFailed", + id: notifications.TemplateTaskFailed, + payload: types.MessagePayload{ + UserName: "Bobby", + UserEmail: "bobby@coder.com", + UserUsername: "bobby", + Labels: map[string]string{ + "task": "my-task", + "workspace": "my-workspace", + }, + Data: map[string]any{}, + }, + }, } // We must have a test case for every notification_template. This is enforced below: diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateTaskCompleted.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTaskCompleted.html.golden new file mode 100644 index 0000000000..769d5595db --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTaskCompleted.html.golden @@ -0,0 +1,84 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Task 'my-workspace' completed +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 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +The task 'my-task' has completed successfully. + + +View task: http://test.com/tasks/bobby/my-workspace + +View workspace: http://test.com/@bobby/my-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Task 'my-workspace' completed + + +
+
+ 3D"Cod= +
+

+ Task 'my-workspace' completed +

+
+

Hi Bobby,

+

The task ‘my-task’ has completed successfully.

+
+
+ =20 + + View task + + =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateTaskFailed.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTaskFailed.html.golden new file mode 100644 index 0000000000..5d0879bc82 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateTaskFailed.html.golden @@ -0,0 +1,85 @@ +From: system@coder.com +To: bobby@coder.com +Subject: Task 'my-workspace' failed +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 +MIME-Version: 1.0 + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Hi Bobby, + +The task 'my-task' has failed. Check the logs for more details. + + +View task: http://test.com/tasks/bobby/my-workspace + +View workspace: http://test.com/@bobby/my-workspace + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + + + + + + + Task 'my-workspace' failed + + +
+
+ 3D"Cod= +
+

+ Task 'my-workspace' failed +

+
+

Hi Bobby,

+

The task ‘my-task’ has failed. Check the logs for mo= +re details.

+
+
+ =20 + + View task + + =20 + + View workspace + + =20 +
+
+

© 2024 Coder. All rights reserved - h= +ttp://test.com

+

Click here to manage your notification = +settings

+

Stop receiving emails like this

+
+
+ + + +--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4-- diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTaskCompleted.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTaskCompleted.json.golden new file mode 100644 index 0000000000..2336bf3162 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTaskCompleted.json.golden @@ -0,0 +1,33 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.2", + "notification_name": "Task Completed", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View task", + "url": "http://test.com/tasks/bobby/my-workspace" + }, + { + "label": "View workspace", + "url": "http://test.com/@bobby/my-workspace" + } + ], + "labels": { + "task": "my-task", + "workspace": "my-workspace" + }, + "data": {}, + "targets": null + }, + "title": "Task 'my-workspace' completed", + "title_markdown": "Task 'my-workspace' completed", + "body": "The task 'my-task' has completed successfully.", + "body_markdown": "The task 'my-task' has completed successfully." +} \ No newline at end of file diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateTaskFailed.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTaskFailed.json.golden new file mode 100644 index 0000000000..44788581a0 --- /dev/null +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateTaskFailed.json.golden @@ -0,0 +1,33 @@ +{ + "_version": "1.1", + "msg_id": "00000000-0000-0000-0000-000000000000", + "payload": { + "_version": "1.2", + "notification_name": "Task Failed", + "notification_template_id": "00000000-0000-0000-0000-000000000000", + "user_id": "00000000-0000-0000-0000-000000000000", + "user_email": "bobby@coder.com", + "user_name": "Bobby", + "user_username": "bobby", + "actions": [ + { + "label": "View task", + "url": "http://test.com/tasks/bobby/my-workspace" + }, + { + "label": "View workspace", + "url": "http://test.com/@bobby/my-workspace" + } + ], + "labels": { + "task": "my-task", + "workspace": "my-workspace" + }, + "data": {}, + "targets": null + }, + "title": "Task 'my-workspace' failed", + "title_markdown": "Task 'my-workspace' failed", + "body": "The task 'my-task' has failed. Check the logs for more details.", + "body_markdown": "The task 'my-task' has failed. Check the logs for more details." +} \ No newline at end of file diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 51654380e5..23046dab28 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -452,6 +452,10 @@ func (api *API) enqueueAITaskStateNotification( notificationTemplate = notifications.TemplateTaskWorking case codersdk.WorkspaceAppStatusStateIdle: notificationTemplate = notifications.TemplateTaskIdle + case codersdk.WorkspaceAppStatusStateComplete: + notificationTemplate = notifications.TemplateTaskCompleted + case codersdk.WorkspaceAppStatusStateFailure: + notificationTemplate = notifications.TemplateTaskFailed default: // Not a notifiable state, do nothing return