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.
|
||||
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 {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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
|
||||
},
|
||||
{
|
||||
|
Reference in New Issue
Block a user