mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
feat: expose Markdown fields in webhook payload (#14931)
Fixes: https://github.com/coder/coder/issues/14930
This commit is contained in:
@ -28,43 +28,47 @@ type WebhookHandler struct {
|
|||||||
|
|
||||||
// WebhookPayload describes the JSON payload to be delivered to the configured webhook endpoint.
|
// WebhookPayload describes the JSON payload to be delivered to the configured webhook endpoint.
|
||||||
type WebhookPayload struct {
|
type WebhookPayload struct {
|
||||||
Version string `json:"_version"`
|
Version string `json:"_version"`
|
||||||
MsgID uuid.UUID `json:"msg_id"`
|
MsgID uuid.UUID `json:"msg_id"`
|
||||||
Payload types.MessagePayload `json:"payload"`
|
Payload types.MessagePayload `json:"payload"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Body string `json:"body"`
|
TitleMarkdown string `json:"title_markdown"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
BodyMarkdown string `json:"body_markdown"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebhookHandler(cfg codersdk.NotificationsWebhookConfig, log slog.Logger) *WebhookHandler {
|
func NewWebhookHandler(cfg codersdk.NotificationsWebhookConfig, log slog.Logger) *WebhookHandler {
|
||||||
return &WebhookHandler{cfg: cfg, log: log, cl: &http.Client{}}
|
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() == "" {
|
if w.cfg.Endpoint.String() == "" {
|
||||||
return nil, xerrors.New("webhook endpoint not defined")
|
return nil, xerrors.New("webhook endpoint not defined")
|
||||||
}
|
}
|
||||||
|
|
||||||
title, err := markdown.PlaintextFromMarkdown(titleTmpl)
|
titlePlaintext, err := markdown.PlaintextFromMarkdown(titleMarkdown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("render title: %w", err)
|
return nil, xerrors.Errorf("render title: %w", err)
|
||||||
}
|
}
|
||||||
body, err := markdown.PlaintextFromMarkdown(bodyTmpl)
|
bodyPlaintext, err := markdown.PlaintextFromMarkdown(bodyMarkdown)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("render body: %w", err)
|
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) {
|
return func(ctx context.Context, msgID uuid.UUID) (retryable bool, err error) {
|
||||||
// Prepare payload.
|
// Prepare payload.
|
||||||
payload := WebhookPayload{
|
payload := WebhookPayload{
|
||||||
Version: "1.0",
|
Version: "1.1",
|
||||||
MsgID: msgID,
|
MsgID: msgID,
|
||||||
Title: title,
|
Title: titlePlaintext,
|
||||||
Body: body,
|
TitleMarkdown: titleMarkdown,
|
||||||
Payload: msgPayload,
|
Body: bodyPlaintext,
|
||||||
|
BodyMarkdown: bodyMarkdown,
|
||||||
|
Payload: msgPayload,
|
||||||
}
|
}
|
||||||
m, err := json.Marshal(payload)
|
m, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -28,17 +28,15 @@ func TestWebhook(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
titleTemplate = "this is the title ({{.Labels.foo}})"
|
titlePlaintext = "this is the title"
|
||||||
bodyTemplate = "this is the body ({{.Labels.baz}})"
|
titleMarkdown = "this *is* _the_ title"
|
||||||
|
bodyPlaintext = "this is the body"
|
||||||
|
bodyMarkdown = "~this~ is the `body`"
|
||||||
)
|
)
|
||||||
|
|
||||||
msgPayload := types.MessagePayload{
|
msgPayload := types.MessagePayload{
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
NotificationName: "test",
|
NotificationName: "test",
|
||||||
Labels: map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": "quux",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -61,6 +59,11 @@ func TestWebhook(t *testing.T) {
|
|||||||
assert.Equal(t, msgID, payload.MsgID)
|
assert.Equal(t, msgID, payload.MsgID)
|
||||||
assert.Equal(t, msgID.String(), r.Header.Get("X-Message-Id"))
|
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)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, err = w.Write([]byte(fmt.Sprintf("received %s", payload.MsgID)))
|
_, err = w.Write([]byte(fmt.Sprintf("received %s", payload.MsgID)))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@ -138,7 +141,7 @@ func TestWebhook(t *testing.T) {
|
|||||||
Endpoint: *serpent.URLOf(endpoint),
|
Endpoint: *serpent.URLOf(endpoint),
|
||||||
}
|
}
|
||||||
handler := dispatch.NewWebhookHandler(cfg, logger.With(slog.F("test", tc.name)))
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
retryable, err := deliveryFn(ctx, msgID)
|
retryable, err := deliveryFn(ctx, msgID)
|
||||||
|
@ -249,7 +249,7 @@ func TestWebhookDispatch(t *testing.T) {
|
|||||||
|
|
||||||
// THEN: the webhook is received by the mock server and has the expected contents
|
// THEN: the webhook is received by the mock server and has the expected contents
|
||||||
payload := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, sent)
|
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, *msgID, payload.MsgID)
|
||||||
require.Equal(t, payload.Payload.Labels, input)
|
require.Equal(t, payload.Payload.Labels, input)
|
||||||
require.Equal(t, payload.Payload.UserEmail, email)
|
require.Equal(t, payload.Payload.UserEmail, email)
|
||||||
|
@ -90,9 +90,11 @@ receiver.router.post("/v1/webhook", async (req, res) => {
|
|||||||
return res.status(400).send("Error: request body is missing");
|
return res.status(400).send("Error: request body is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, body } = req.body;
|
const { title_markdown, body_markdown } = req.body;
|
||||||
if (!title || !body) {
|
if (!title_markdown || !body_markdown) {
|
||||||
return res.status(400).send('Error: missing fields: "title", or "body"');
|
return res
|
||||||
|
.status(400)
|
||||||
|
.send('Error: missing fields: "title_markdown", or "body_markdown"');
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = req.body.payload;
|
const payload = req.body.payload;
|
||||||
@ -118,11 +120,11 @@ receiver.router.post("/v1/webhook", async (req, res) => {
|
|||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: "header",
|
type: "header",
|
||||||
text: { type: "plain_text", text: title },
|
text: { type: "mrkdwn", text: title_markdown },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "section",
|
type: "section",
|
||||||
text: { type: "mrkdwn", text: body },
|
text: { type: "mrkdwn", text: body_markdown },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -67,10 +67,10 @@ The process of setting up a Teams workflow consists of three key steps:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title_markdown": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"body": {
|
"body_markdown": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,11 +108,11 @@ The process of setting up a Teams workflow consists of three key steps:
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextBlock",
|
"type": "TextBlock",
|
||||||
"text": "**@{replace(body('Parse_JSON')?['title'], '"', '\"')}**"
|
"text": "**@{replace(body('Parse_JSON')?['title_markdown'], '"', '\"')}**"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextBlock",
|
"type": "TextBlock",
|
||||||
"text": "@{replace(body('Parse_JSON')?['body'], '"', '\"')}",
|
"text": "@{replace(body('Parse_JSON')?['body_markdown'], '"', '\"')}",
|
||||||
"wrap": true
|
"wrap": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user