feat(coderd): notify on task completion/failure (#20327)

Adds notifications on task transitions to completed or failure state.

Authored by Claude, I reviewed it and it appears to be legit.
This commit is contained in:
Cian Johnston
2025-10-16 10:21:08 +01:00
committed by GitHub
parent 3fa438f80e
commit 0faee8e913
10 changed files with 397 additions and 2 deletions
+58
View File
@@ -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()
@@ -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';
@@ -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
);
+4 -2
View File
@@ -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")
)
@@ -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:
@@ -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
<!doctype html>
<html lang=3D"en">
<head>
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-workspace' completed</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
; background: #f8fafc;">
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
n: left; font-size: 14px; line-height: 1.5;">
<div style=3D"text-align: center;">
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
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;">
Task 'my-workspace' completed
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
<p>The task &lsquo;my-task&rsquo; has completed successfully.</p>
</div>
<div style=3D"text-align: center; margin-top: 32px;">
=20
<a href=3D"http://test.com/tasks/bobby/my-workspace" style=3D"displ=
ay: inline-block; padding: 13px 24px; background-color: #020617; color: #f8=
fafc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
View task
</a>
=20
<a href=3D"http://test.com/@bobby/my-workspace" style=3D"display: i=
nline-block; padding: 13px 24px; background-color: #020617; color: #f8fafc;=
text-decoration: none; border-radius: 8px; margin: 0 4px;">
View workspace
</a>
=20
</div>
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
ttp://test.com</a></p>
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
r: #2563eb; text-decoration: none;">Click here to manage your notification =
settings</a></p>
<p><a href=3D"http://test.com/settings/notifications?disabled=3D8c5=
a4d12-9f7e-4b3a-a1c8-6e4f2d9b5a7c" style=3D"color: #2563eb; text-decoration=
: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
</html>
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
@@ -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
<!doctype html>
<html lang=3D"en">
<head>
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-workspace' failed</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
l', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617=
; background: #f8fafc;">
<div style=3D"max-width: 600px; margin: 20px auto; padding: 60px; borde=
r: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-alig=
n: left; font-size: 14px; line-height: 1.5;">
<div style=3D"text-align: center;">
<img src=3D"https://coder.com/coder-logo-horizontal.png" alt=3D"Cod=
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;">
Task 'my-workspace' failed
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
<p>The task &lsquo;my-task&rsquo; has failed. Check the logs for mo=
re details.</p>
</div>
<div style=3D"text-align: center; margin-top: 32px;">
=20
<a href=3D"http://test.com/tasks/bobby/my-workspace" style=3D"displ=
ay: inline-block; padding: 13px 24px; background-color: #020617; color: #f8=
fafc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
View task
</a>
=20
<a href=3D"http://test.com/@bobby/my-workspace" style=3D"display: i=
nline-block; padding: 13px 24px; background-color: #020617; color: #f8fafc;=
text-decoration: none; border-radius: 8px; margin: 0 4px;">
View workspace
</a>
=20
</div>
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
<p>&copy;&nbsp;2024&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a =
href=3D"http://test.com" style=3D"color: #2563eb; text-decoration: none;">h=
ttp://test.com</a></p>
<p><a href=3D"http://test.com/settings/notifications" style=3D"colo=
r: #2563eb; text-decoration: none;">Click here to manage your notification =
settings</a></p>
<p><a href=3D"http://test.com/settings/notifications?disabled=3D3b7=
e8f1a-4c2d-49a6-b5e9-7f3a1c8d6b4e" style=3D"color: #2563eb; text-decoration=
: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
</html>
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4--
@@ -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."
}
@@ -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."
}
+4
View File
@@ -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