feat: support custom notifications (#19751)

## Description

Adds support for sending an ad‑hoc custom notification to the
authenticated user via API and CLI. This is useful for surfacing the
result of scripts or long‑running tasks. Notifications are delivered
through the configured method and the dashboard Inbox, respecting
existing preferences and delivery settings.

## Changes

* New notification template: “Custom Notification” with a label for a
custom title and a custom message.
* New API endpoint: `POST /api/v2/notifications/custom` to send a custom
notification to the requesting user.
* New API endpoint: `GET /notifications/templates/custom` to get custom
notification template.
* New CLI subcommand: `coder notifications custom <title> <message>` to
send a custom notification to the requesting user.
* Documentation updates: Add a “Custom notifications” section under
Administration > Monitoring > Notifications, including instructions on
sending custom notifications and examples of when to use them.

Closes: https://github.com/coder/coder/issues/19611
This commit is contained in:
Susana Ferreira
2025-09-11 15:08:57 +02:00
committed by GitHub
parent 4c98decfb7
commit eec6c8c120
26 changed files with 1056 additions and 30 deletions
+34 -2
View File
@@ -16,7 +16,7 @@ func (r *RootCmd) notifications() *serpent.Command {
Short: "Manage Coder notifications",
Long: "Administrators can use these commands to change notification settings.\n" + FormatExamples(
Example{
Description: "Pause Coder notifications. Administrators can temporarily stop notifiers from dispatching messages in case of the target outage (for example: unavailable SMTP server or Webhook not responding).",
Description: "Pause Coder notifications. Administrators can temporarily stop notifiers from dispatching messages in case of the target outage (for example: unavailable SMTP server or Webhook not responding)",
Command: "coder notifications pause",
},
Example{
@@ -24,9 +24,13 @@ func (r *RootCmd) notifications() *serpent.Command {
Command: "coder notifications resume",
},
Example{
Description: "Send a test notification. Administrators can use this to verify the notification target settings.",
Description: "Send a test notification. Administrators can use this to verify the notification target settings",
Command: "coder notifications test",
},
Example{
Description: "Send a custom notification to the requesting user. Sending notifications targeting other users or groups is currently not supported",
Command: "coder notifications custom \"Custom Title\" \"Custom Message\"",
},
),
Aliases: []string{"notification"},
Handler: func(inv *serpent.Invocation) error {
@@ -36,6 +40,7 @@ func (r *RootCmd) notifications() *serpent.Command {
r.pauseNotifications(),
r.resumeNotifications(),
r.testNotifications(),
r.customNotifications(),
},
}
return cmd
@@ -109,3 +114,30 @@ func (r *RootCmd) testNotifications() *serpent.Command {
}
return cmd
}
func (r *RootCmd) customNotifications() *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "custom <title> <message>",
Short: "Send a custom notification",
Middleware: serpent.Chain(
serpent.RequireNArgs(2),
r.InitClient(client),
),
Handler: func(inv *serpent.Invocation) error {
err := client.PostCustomNotification(inv.Context(), codersdk.CustomNotificationRequest{
Content: &codersdk.CustomNotificationContent{
Title: inv.Args[0],
Message: inv.Args[1],
},
})
if err != nil {
return xerrors.Errorf("unable to post custom notification: %w", err)
}
_, _ = fmt.Fprintln(inv.Stderr, "A custom notification has been sent.")
return nil
},
}
return cmd
}
+101
View File
@@ -12,6 +12,8 @@ import (
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
"github.com/coder/coder/v2/codersdk"
@@ -166,3 +168,102 @@ func TestNotificationsTest(t *testing.T) {
require.Len(t, sent, 0)
})
}
func TestCustomNotifications(t *testing.T) {
t.Parallel()
t.Run("BadRequest", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
ownerClient := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t),
NotificationsEnqueuer: notifyEnq,
})
// Given: A member user
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
// When: The member user attempts to send a custom notification with empty title and message
inv, root := clitest.New(t, "notifications", "custom", "", "")
clitest.SetupConfig(t, memberClient, root)
// Then: an error is expected with no notifications sent
err := inv.Run()
var sdkError *codersdk.Error
require.Error(t, err)
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
require.Equal(t, http.StatusBadRequest, sdkError.StatusCode())
require.Equal(t, "Invalid request body", sdkError.Message)
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
require.Len(t, sent, 0)
})
t.Run("SystemUserNotAllowed", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
ownerClient, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t),
NotificationsEnqueuer: notifyEnq,
})
// Given: A system user (prebuilds system user)
_, token := dbgen.APIKey(t, db, database.APIKey{
UserID: database.PrebuildsSystemUserID,
LoginType: database.LoginTypeNone,
})
systemUserClient := codersdk.New(ownerClient.URL)
systemUserClient.SetSessionToken(token)
// When: The system user attempts to send a custom notification
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
clitest.SetupConfig(t, systemUserClient, root)
// Then: an error is expected with no notifications sent
err := inv.Run()
var sdkError *codersdk.Error
require.Error(t, err)
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
require.Equal(t, "Forbidden", sdkError.Message)
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateTestNotification))
require.Len(t, sent, 0)
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
notifyEnq := &notificationstest.FakeEnqueuer{}
ownerClient := coderdtest.New(t, &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t),
NotificationsEnqueuer: notifyEnq,
})
// Given: A member user
ownerUser := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, memberUser := coderdtest.CreateAnotherUser(t, ownerClient, ownerUser.OrganizationID)
// When: The member user attempts to send a custom notification
inv, root := clitest.New(t, "notifications", "custom", "Custom Title", "Custom Message")
clitest.SetupConfig(t, memberClient, root)
// Then: we expect a custom notification to be sent to the member user
err := inv.Run()
require.NoError(t, err)
sent := notifyEnq.Sent(notificationstest.WithTemplateID(notifications.TemplateCustomNotification))
require.Len(t, sent, 1)
require.Equal(t, memberUser.ID, sent[0].UserID)
require.Len(t, sent[0].Labels, 2)
require.Equal(t, "Custom Title", sent[0].Labels["custom_title"])
require.Equal(t, "Custom Message", sent[0].Labels["custom_message"])
require.Equal(t, memberUser.ID.String(), sent[0].CreatedBy)
})
}
+8 -2
View File
@@ -12,7 +12,7 @@ USAGE:
from
dispatching messages in case of the target outage (for example: unavailable
SMTP
server or Webhook not responding).:
server or Webhook not responding):
$ coder notifications pause
@@ -22,11 +22,17 @@ USAGE:
- Send a test notification. Administrators can use this to verify the
notification
target settings.:
target settings:
$ coder notifications test
- Send a custom notification to the requesting user. Sending notifications
targeting other users or groups is currently not supported:
$ coder notifications custom "Custom Title" "Custom Message"
SUBCOMMANDS:
custom Send a custom notification
pause Pause notifications
resume Resume notifications
test Send a test notification
+9
View File
@@ -0,0 +1,9 @@
coder v0.0.0-devel
USAGE:
coder notifications custom <title> <message>
Send a custom notification
———
Run `coder --help` for a list of global options.