From 0aa84b18a10382f70bda7ee4b1c3f4b6bdcd22f9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 2 Oct 2024 15:38:22 +0200 Subject: [PATCH] feat: expose Markdown fields in webhook payload (#14931) Fixes: https://github.com/coder/coder/issues/14930 --- coderd/notifications/dispatch/webhook.go | 34 +++++++++++-------- coderd/notifications/dispatch/webhook_test.go | 17 ++++++---- coderd/notifications/notifications_test.go | 2 +- docs/admin/notifications/slack.md | 12 ++++--- docs/admin/notifications/teams.md | 8 ++--- 5 files changed, 41 insertions(+), 32 deletions(-) diff --git a/coderd/notifications/dispatch/webhook.go b/coderd/notifications/dispatch/webhook.go index 4a548b40e4..fcad3a7b0e 100644 --- a/coderd/notifications/dispatch/webhook.go +++ b/coderd/notifications/dispatch/webhook.go @@ -28,43 +28,47 @@ type WebhookHandler struct { // WebhookPayload describes the JSON payload to be delivered to the configured webhook endpoint. type WebhookPayload struct { - Version string `json:"_version"` - MsgID uuid.UUID `json:"msg_id"` - Payload types.MessagePayload `json:"payload"` - Title string `json:"title"` - Body string `json:"body"` + Version string `json:"_version"` + MsgID uuid.UUID `json:"msg_id"` + Payload types.MessagePayload `json:"payload"` + Title string `json:"title"` + TitleMarkdown string `json:"title_markdown"` + Body string `json:"body"` + BodyMarkdown string `json:"body_markdown"` } func NewWebhookHandler(cfg codersdk.NotificationsWebhookConfig, log slog.Logger) *WebhookHandler { return &WebhookHandler{cfg: cfg, log: log, cl: &http.Client{}} } -func (w *WebhookHandler) Dispatcher(payload types.MessagePayload, titleTmpl, bodyTmpl string) (DeliveryFunc, error) { +func (w *WebhookHandler) Dispatcher(payload types.MessagePayload, titleMarkdown, bodyMarkdown string) (DeliveryFunc, error) { if w.cfg.Endpoint.String() == "" { return nil, xerrors.New("webhook endpoint not defined") } - title, err := markdown.PlaintextFromMarkdown(titleTmpl) + titlePlaintext, err := markdown.PlaintextFromMarkdown(titleMarkdown) if err != nil { return nil, xerrors.Errorf("render title: %w", err) } - body, err := markdown.PlaintextFromMarkdown(bodyTmpl) + bodyPlaintext, err := markdown.PlaintextFromMarkdown(bodyMarkdown) if err != nil { return nil, xerrors.Errorf("render body: %w", err) } - return w.dispatch(payload, title, body, w.cfg.Endpoint.String()), nil + return w.dispatch(payload, titlePlaintext, titleMarkdown, bodyPlaintext, bodyMarkdown, w.cfg.Endpoint.String()), nil } -func (w *WebhookHandler) dispatch(msgPayload types.MessagePayload, title, body, endpoint string) DeliveryFunc { +func (w *WebhookHandler) dispatch(msgPayload types.MessagePayload, titlePlaintext, titleMarkdown, bodyPlaintext, bodyMarkdown, endpoint string) DeliveryFunc { return func(ctx context.Context, msgID uuid.UUID) (retryable bool, err error) { // Prepare payload. payload := WebhookPayload{ - Version: "1.0", - MsgID: msgID, - Title: title, - Body: body, - Payload: msgPayload, + Version: "1.1", + MsgID: msgID, + Title: titlePlaintext, + TitleMarkdown: titleMarkdown, + Body: bodyPlaintext, + BodyMarkdown: bodyMarkdown, + Payload: msgPayload, } m, err := json.Marshal(payload) if err != nil { diff --git a/coderd/notifications/dispatch/webhook_test.go b/coderd/notifications/dispatch/webhook_test.go index 3bfcfd8a2e..26a78752cf 100644 --- a/coderd/notifications/dispatch/webhook_test.go +++ b/coderd/notifications/dispatch/webhook_test.go @@ -28,17 +28,15 @@ func TestWebhook(t *testing.T) { t.Parallel() const ( - titleTemplate = "this is the title ({{.Labels.foo}})" - bodyTemplate = "this is the body ({{.Labels.baz}})" + titlePlaintext = "this is the title" + titleMarkdown = "this *is* _the_ title" + bodyPlaintext = "this is the body" + bodyMarkdown = "~this~ is the `body`" ) msgPayload := types.MessagePayload{ Version: "1.0", NotificationName: "test", - Labels: map[string]string{ - "foo": "bar", - "baz": "quux", - }, } tests := []struct { @@ -61,6 +59,11 @@ func TestWebhook(t *testing.T) { assert.Equal(t, msgID, payload.MsgID) assert.Equal(t, msgID.String(), r.Header.Get("X-Message-Id")) + assert.Equal(t, titlePlaintext, payload.Title) + assert.Equal(t, titleMarkdown, payload.TitleMarkdown) + assert.Equal(t, bodyPlaintext, payload.Body) + assert.Equal(t, bodyMarkdown, payload.BodyMarkdown) + w.WriteHeader(http.StatusOK) _, err = w.Write([]byte(fmt.Sprintf("received %s", payload.MsgID))) assert.NoError(t, err) @@ -138,7 +141,7 @@ func TestWebhook(t *testing.T) { Endpoint: *serpent.URLOf(endpoint), } handler := dispatch.NewWebhookHandler(cfg, logger.With(slog.F("test", tc.name))) - deliveryFn, err := handler.Dispatcher(msgPayload, titleTemplate, bodyTemplate) + deliveryFn, err := handler.Dispatcher(msgPayload, titleMarkdown, bodyMarkdown) require.NoError(t, err) retryable, err := deliveryFn(ctx, msgID) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index ca1f4f78aa..e52610c9c5 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -249,7 +249,7 @@ func TestWebhookDispatch(t *testing.T) { // THEN: the webhook is received by the mock server and has the expected contents payload := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, sent) - require.EqualValues(t, "1.0", payload.Version) + require.EqualValues(t, "1.1", payload.Version) require.Equal(t, *msgID, payload.MsgID) require.Equal(t, payload.Payload.Labels, input) require.Equal(t, payload.Payload.UserEmail, email) diff --git a/docs/admin/notifications/slack.md b/docs/admin/notifications/slack.md index aa6a4dcdb5..554e5c986a 100644 --- a/docs/admin/notifications/slack.md +++ b/docs/admin/notifications/slack.md @@ -90,9 +90,11 @@ receiver.router.post("/v1/webhook", async (req, res) => { return res.status(400).send("Error: request body is missing"); } - const { title, body } = req.body; - if (!title || !body) { - return res.status(400).send('Error: missing fields: "title", or "body"'); + const { title_markdown, body_markdown } = req.body; + if (!title_markdown || !body_markdown) { + return res + .status(400) + .send('Error: missing fields: "title_markdown", or "body_markdown"'); } const payload = req.body.payload; @@ -118,11 +120,11 @@ receiver.router.post("/v1/webhook", async (req, res) => { blocks: [ { type: "header", - text: { type: "plain_text", text: title }, + text: { type: "mrkdwn", text: title_markdown }, }, { type: "section", - text: { type: "mrkdwn", text: body }, + text: { type: "mrkdwn", text: body_markdown }, }, ], }; diff --git a/docs/admin/notifications/teams.md b/docs/admin/notifications/teams.md index 92957dd464..7accfbe956 100644 --- a/docs/admin/notifications/teams.md +++ b/docs/admin/notifications/teams.md @@ -67,10 +67,10 @@ The process of setting up a Teams workflow consists of three key steps: } } }, - "title": { + "title_markdown": { "type": "string" }, - "body": { + "body_markdown": { "type": "string" } } @@ -108,11 +108,11 @@ The process of setting up a Teams workflow consists of three key steps: }, { "type": "TextBlock", - "text": "**@{replace(body('Parse_JSON')?['title'], '"', '\"')}**" + "text": "**@{replace(body('Parse_JSON')?['title_markdown'], '"', '\"')}**" }, { "type": "TextBlock", - "text": "@{replace(body('Parse_JSON')?['body'], '"', '\"')}", + "text": "@{replace(body('Parse_JSON')?['body_markdown'], '"', '\"')}", "wrap": true }, {